Примеры, когда функциональный код, оптимизированный для компилятора, работает лучше, чем императивный код

Одно из обещаний свободного от побочных эффектов, ссылочно прозрачного функционального программирования заключается в том, что такой код может быть всесторонне оптимизирован. Чтобы процитировать Википедию:

Неизменяемость данных во многих случаях может привести к эффективности выполнения, позволяя компилятору делать небезопасные предположения в императивном языке, таким образом увеличивая возможности для встроенного расширения.

Я хотел бы увидеть примеры, в которых компилятор функционального языка превосходит императивный компилятор, создавая более оптимизированный код.

Изменить: я попытался дать конкретный сценарий, но, видимо, это не было хорошей идеей. Поэтому я постараюсь объяснить это по-другому.

Программисты переводят идеи (алгоритмы) на языки, понятные машинам. В то же время одним из наиболее важных аспектов перевода является то, что люди также могут понять полученный код. К сожалению, во многих случаях приходится идти на компромисс: сжатый, читаемый код страдает низкой производительностью, и его необходимо оптимизировать вручную. Это подвержено ошибкам, требует много времени и делает код менее читаемым (вплоть до совершенно нечитаемым).

Основы функциональных языков, такие как неизменность и ссылочная прозрачность, позволяют компиляторам выполнять обширную оптимизацию, которая может заменить ручную оптимизацию кода и освобождение программистов от этого компромисса. Ищу примеры идей (алгоритмов) и их реализации, такие как:

  1. (функциональная) реализация близка к исходной идее и проста для понимания,
  2. он значительно оптимизирован компилятором языка, и
  3. трудно (или невозможно) написать такой же эффективный код на императивном языке без ручной оптимизации, которая снижает его краткость и удобочитаемость.

Прошу прощения, если это немного расплывчато, но я надеюсь, что идея ясна. Я не хочу давать лишних ограничений в ответах. Я открыт для предложений, если кто-то знает, как это лучше выразить.

Мой интерес не только теоретический. Я бы хотел использовать такие примеры (среди прочего), чтобы заинтересовать студентов функциональным программированием.

Поначалу меня не удовлетворили несколько примеров, предложенных в комментариях. Поразмыслив, я беру свои возражения назад, это хорошие примеры. Не стесняйтесь расширять их до полных ответов, чтобы люди могли комментировать и голосовать за них.


(Одним из классов таких примеров, скорее всего, будет распараллеленный код, который может использовать преимущества нескольких ядер ЦП. Часто на функциональных языках это можно сделать легко, не жертвуя простотой кода (например, в Haskell, добавив _ 1_ или pseq в соответствующих местах). Меня интересуют такие примеры тоже, но и в других, непараллельных.)


comment
Возможно, вас заинтересует статья Нила Митчелла о суперкомпиляции.   -  person Thomas M. DuBuisson    schedule 16.01.2013
comment
Ref.trans. и ленивая оптимизация (от O (n log n) до O (n)) функция min = head . sort   -  person josejuan    schedule 16.01.2013
comment
Это работа для достаточно умного компилятора, что также приводит к полной занятости разработчиков компилятора.   -  person Don Stewart    schedule 16.01.2013
comment
Как правило, если алгоритм идентичен, то вы выполняете сравнение среды выполнения и генератора кода - в этом случае иногда семантика языка улучшает ситуацию. Более интересно, когда язык позволяет использовать другой алгоритм ...   -  person Don Stewart    schedule 16.01.2013
comment
@DonStewart Конечно, также приветствуются примеры, когда функциональный язык допускает другой, лучший алгоритм.   -  person Petr    schedule 16.01.2013
comment
Это лучше спрашивать в теоретической CS, а не в SO, поскольку вам интересно узнать о фундаментальных ограничениях вычислительных моделей.   -  person Don Stewart    schedule 16.01.2013
comment
Пример @josejuan - не идеальный пример. Вместо того, чтобы нуждаться в отдельном алгоритме для получения минимального элемента в O (n), в haskell min. голова просто работает. Это простой, но тем не менее впечатляющий пример.   -  person Adam Bell    schedule 16.01.2013
comment
@ PetrPudlák, вы предполагаете, что у нас есть алгоритм A. Мой пример показывает, как плохой императивный алгоритм (сортировка по первому) оптимизируется log n раз в Haskell. Я показываю, как Haskell оптимизирует простое решение. Вы никогда не сможете сравнивать один с одним компиляторами C ++ и Haskell (например) именно потому, что они используют разные стратегии. Вы можете сравнивать результат оптимизации, а не оптимизации.   -  person josejuan    schedule 16.01.2013
comment
Отличный вопрос, и люди голосуют за его закрытие. Что, черт возьми, не так с этим миром ?!   -  person Nikita Volkov    schedule 16.01.2013
comment
@jberryman Довольно ясно сказано, что сравнение должно проводиться между императивным и функциональным семейством, то есть не между C ++ и Java, а между любым из них и Haskell. Открытый выбор императивного языка, очевидно, дан только для удобства отвечающего, поскольку в рамках вопроса это не имеет большого значения - в этом нет ничего неконструктивного. Выбор алгоритма остается открытым, поскольку он составляет часть вопроса, который, как мне кажется, сводится к следующему: каковы основные чистые алгоритмы, благодаря которым оптимизатор функционального языка сияет?   -  person Nikita Volkov    schedule 16.01.2013
comment
Если алгоритмы разные, значит, вы не тестируете одно и то же ...   -  person Don Stewart    schedule 16.01.2013
comment
@NikitaVolkov, как предлагает Дон, вы не собираетесь сравнивать один и тот же алгоритм. Например, быстрая сортировка - это обязательный алгоритм сортировки на месте. Вы можете реализовать это в haskell, но ничего не узнаете о ссылочной прозрачности и эффективности. Или вы можете реализовать обычный аналогичный функциональный вариант быстрой сортировки и сравнить их, но тогда вы ничего не узнаете, потому что алгоритмы действительно семантически совершенно разные.   -  person jberryman    schedule 16.01.2013
comment
Я предполагаю, что мой общий ответ на этот вопрос заключается в том, что я сильно подозреваю, что почти любой алгоритм с функциональной семантикой будет работать быстрее при реализации в haskell, чем на любом императивном языке, способном точно выразить алгоритм.   -  person jberryman    schedule 16.01.2013
comment
@ PetrPudlák, который можно кратко описать с помощью императивного алгоритма O (n) - если мне разрешено произвольно выбирать императивный алгоритм, я всегда могу утверждать, что глупо выбирать алгоритм, который работает хуже, чем скомпилированный функциональный код. Легко утверждать, что функциональное решение - это уродливый, унидиоматический взлом, тогда как можно сказать, что уродливые хаки всегда идиоматичны в C. Ответы на ваш вопрос обязательно действительно очень скользкие.   -  person AndrewC    schedule 17.01.2013
comment
@josejuan Я пересмотрел идею и передумал, ваш пример хороший. Еще лучше вариант, который получает 10 наименьших элементов списка (take 10 . sort). Возможно, можно было бы пойти еще дальше, чтобы реализовать ленивую быструю сортировку и использовать ее в качестве эффективного алгоритма выбора . Возможно, вы могли бы расширить свой комментарий до ответа, чтобы люди могли проголосовать за него.   -  person Petr    schedule 17.01.2013
comment
На этот вопрос ищу Эквивалент облегченных потоков Haskell на языке C Пока что единственный ответ - это подробное объяснение того, что невозможно сделать библиотеку потоков быстрее, чем потоки ОС, а потоки Haskell определенно легче и масштабируемы. Если ответчик прав, это был бы хороший ответ и на этот вопрос, но я думаю, что присяжных еще нет.   -  person AndrewC    schedule 17.01.2013
comment
@AndrewC Спасибо за ссылку. Я немного пересмотрел вопрос и его комментарии и соответственно обновил вопрос. Я думаю, что мое мнение было слишком ограниченным, и теперь я открыт для более широкого спектра ответов.   -  person Petr    schedule 17.01.2013


Ответы (4)


Бывают случаи, когда один и тот же алгоритм лучше оптимизируется в чистом контексте. В частности, объединение потоков позволяет использовать алгоритм, состоящий из последовательности циклов, которые могут иметь самую разнообразную форму: карты, фильтры, складки, разворачивания, составляющие единый цикл.

Эквивалентная оптимизация в обычной императивной настройке с изменяемыми данными в циклах должна обеспечить полный анализ эффекта, чего никто не делает.

Так что, по крайней мере, для класса алгоритмов, которые реализованы как конвейеры ана- и катаморфизмов в последовательностях, вы можете гарантировать результаты оптимизации, которые невозможны в императивных условиях.

person Don Stewart    schedule 16.01.2013
comment
А как насчет мемоизации? Я считаю, что есть много возможностей использовать это в чистом коде. - person Nikita Volkov; 16.01.2013
comment
@NikitaVolkov: Мемоизацию легко добавить, сложнее всего решить, когда она действительно поможет, а не тратить много памяти. - person C. A. McCann; 16.01.2013
comment
@ C.A.McCann Вы должны признать, что количество лотов - довольно сомнительная мера, особенно с учетом количества доступной памяти в наши дни. В любом случае, означает ли это, что нет никаких оптимизаций на основе мемоизации и планов по их внедрению в GHC? Просто любопытно. - person Nikita Volkov; 16.01.2013
comment
@NikitaVolkov, автоматическую оптимизацию мемоизации сложно сделать правильно, потому что вы не знаете, где ее можно разместить, - но программисты на Haskell легко достают библиотеку мемоизации, когда она нужна, а затем набирают memo где-нибудь в своем коде. Для меня это больше похоже на оптимизацию, ориентированную на пользователя, чем на кодирование (очевидно, вы должны понимать проблему и модель оценки Haskell, чтобы знать, куда ее поставить). - person luqui; 16.01.2013
comment
В связи с этим возникает вопрос, являются ли эти два алгоритма одним и тем же. Выражения внешне похожи, но они определенно не делают то же самое с данными. - person tmyklebu; 17.01.2013
comment
Вы можете делать аналогичные вещи на C ++ с помощью метапрограммирования шаблонов (см. Eigen, Blitz ++, std::valarray и т. Д.), Но это некрасиво. - person Alexandre C.; 17.01.2013

Совсем недавно вышедшая статья Haskell превосходит C с помощью обобщенного слияния потоков Джеффа Материка , Саймон Пейтон Джонс, Саймон Марлоу, Роман Лещинский (представленный на ICFP 2013) описывает такой пример. Аннотация (интересная часть выделена жирным шрифтом):

Слияние потоков [6] - это мощный метод автоматического преобразования высокоуровневых функций обработки последовательностей в эффективные реализации. Он с большим успехом использовался в библиотеках Haskell для управления массивами байтов, текстом Unicode и распакованными векторами. Однако некоторые операции, такие как добавление вектора, по-прежнему плохо работают в рамках стандартной структуры слияния потоков. Другие, такие как вычисление SIMD с использованием инструкций SSE и AVX, доступных на современных чипах x86, похоже, вообще не вписываются в структуру.

В этой статье мы представляем обобщенное объединение потоков, которое решает эти проблемы. Ключевой момент заключается в том, чтобы связать вместе несколько потоковых представлений, каждое из которых настроено для определенного класса потребителей потока. Мы также описываем представление потока, подходящее для эффективных вычислений с помощью инструкций SSE. Наши идеи реализованы в модифицированных версиях компилятора GHC и vector библиотеки. Тесты показывают, что высокоуровневый код Haskell, написанный с использованием нашего компилятора и библиотек, может создавать код, который быстрее, чем компилятор и ручная векторизация C.

person Petr    schedule 11.04.2013

Это просто примечание, а не ответ: gcc имеет атрибут pure, предполагающий, что он может учитывать чистоту; очевидные причины отмечены в руководстве здесь.

Я бы подумал, что «статическое одиночное присвоение» налагает определенную чистоту - см. Ссылки на http://lambda-the-ultimate.org/node/2860 или статью в Википедии.

person Michael    schedule 28.01.2013

make и различные системы сборки лучше работают для больших проектов, если предположить, что различные этапы сборки являются прозрачными по ссылкам; Таким образом, им нужно только повторно выполнить шаги, для которых изменились входные данные.

Для небольших и средних изменений это может быть намного быстрее, чем создание с нуля.

person MSN    schedule 30.01.2013
comment
Хотя в целом это хороший пример, я бы сказал, что он не дает ответа на мой вопрос Примеры, когда функциональный код, оптимизированный для компилятора, работает лучше, чем императивный код. - person Petr; 31.01.2013