Есть ли оптимизация, аналогичная развертыванию цикла для функционального программирования?

Отказ от ответственности: я мало знаю о конвейере компиляции ghc, но я надеюсь узнать о нем больше в этом посте, например, если сравнение императивного и функционального имеет отношение к компиляции кода.

Как вы знаете, развертывание цикла сокращает количество итераций в цикле за счет дублирования кода внутри него. Это повышает производительность, поскольку уменьшает количество переходов (и связанных с ними штрафов) и AFAIR, создает большие блоки кода, оставляя место для лучшего Регистрация переименования оптимизация.

Мне было интересно, может ли быть эквивалент Loop Unrolling для функционального программирования? Можем ли мы «развернуть» функцию, открыть/расширить ее определение, чтобы сначала уменьшить количество вызовов указанной функции и/или создать большие фрагменты кода, а затем оставить место для дополнительных оптимизаций переписывания кода (таких как переименование регистров или некоторые FP). эквивалент)?

Что-то, что могло бы «развернуть» или «расширить» определение функции, используя, например, оценку функции (возможно, в сочетании с некоторой тактикой), чтобы найти компромисс между пространством и временем.

Пример того, что я имел в виду:

 map1 _ [] = []
 map1 f (x:xs) = (f x): map f xs

развернулся бы

map2 _ [] = []
map2 f (x:x1:xs) = (f x):(f x1):map2 f xs
map2 f (x:xs) = (f x): map2 f xs

Еще раз:

map4 _ [] = []
map4 f (x:x1:x2:x3:xs) = (f x):(f x1):(f x2):(f x3):map4 f xs
map4 f (x:x1:x2:xs) = (f x):(f x1):(f x2):map4 f xs
map4 f (x:x1:xs) = (f x):(f x1):map4 f xs
map4 f (x:xs) = (f x): map4 f xs

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

Кажется, написать тест для этого несложно, поэтому после определения критерия для развертывания этого что у меня есть:

альбом ImgUr

Problem size 5*10^6

map  105.4 ms
map2 93.34 ms
map4 89.79 ms

Problem size 1*10^7

map  216.3 ms
map2 186.8 ms
map4 180.1 ms

Problem size 5*10^7

map  1050 ms
map2 913.7 ms
map4 899.8 ms

Что ж, кажется, раскрутка подействовала^1! map4 работает на 16% быстрее.

Тогда время для вопросов:

  1. Обсуждалось ли это раньше? Что-то подобное уже реализовано?
  2. Действительно ли уменьшенное количество оценок map4 улучшает скорость?
  3. Можно ли это автоматизировать?
  4. Могу ли я оценить по частям? т.е.: если (f x) полностью оценено, полностью оценить все до (f x4).
  5. Любая другая форма такого рода развертывания вступает в игру?
  6. К чему может привести инфляция размера функции?
  7. Любые недостатки, почему это не очень хорошая идея?

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


person MdxBhmt    schedule 30.09.2013    source источник
comment
Вы имеете в виду инлайнинг? Потому что это звучит так, как будто вы имеете в виду встраивание. В этом случае да, GHC агрессивно встраивает, как и большинство высокопроизводительных компиляторов, независимо от языка.   -  person Daniel Gratzer    schedule 30.09.2013
comment
Я не думаю, что встраивание делает то, о чем я говорю, хотя и похоже. AFAIU, встраивание будет встраивать f в (f x), эффективно повышая скорость за счет сокращения вызова функции за счет размера кода. Но IMO это не будет встроенной картой в (f x):(map f xs), так как ее поведение сильно отличается от f. Кроме того, мой тестовый код скомпилирован с ключом -O, так что даже если я ошибаюсь во встроенном поведении, он не срабатывает.   -  person MdxBhmt    schedule 30.09.2013
comment
Я не верю, что это имеет значение. Компиляция с O2 приводит к тому, что все они дают одинаковые результаты. Развертывание не уменьшает количество переходников (если map f (x:xs) производит один, то же самое делает и f x:map f xs). Я не уверен, что еще может быть затронуто.   -  person user2407038    schedule 02.10.2013


Ответы (2)


Вы компилировали с оптимизацией? Для меня с -O2 особой разницы между этими фрагментами нет: map1, map2 и map4 выполнялись за 279, 267 и 285 мс соответственно (и для сравнения, сам map выполнялся за 278 мс). Так что для меня это выглядит просто шумом измерения, а не улучшением.

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

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

А что касается «Есть какие-либо недостатки в том, почему это не очень хорошая идея?», Что ж, я могу сразу же придумать один:

*Main> map succ (1:undefined)
[2*** Exception: Prelude.undefined
*Main> map4 succ (1:undefined)
*** Exception: Prelude.undefined

Во многих ситуациях сделать функцию более строгой для повышения производительности — это нормально, но здесь выигрыш в производительности не так ясен для меня, и map часто используется в зависимости от лени.

person Daniel Wagner    schedule 30.09.2013
comment
Я скомпилировал с -O, и у меня был один запуск с O2, который дал аналогичные результаты, прежде чем писать пост. Я побегу снова. - person MdxBhmt; 30.09.2013
comment
pastebin.com/Cj5JUT1c Скомпилировано с O2, с n=5*10^6, я все еще вижу улучшение по сравнению с карта1. Что-то происходит с критерием, завышая расчетное время выполнения тестов для первого стенда, но это не влияет на среднее значение. Я посмотрю на этот разворачивающийся плагин. Да, я ожидал, что undefined выскочит где-нибудь поблизости, но, ааа, я думаю, что haskellers все равно знают, как с этим справиться. На ум приходит контролируемая строгость (шаблон, последовательность и тому подобное). - person MdxBhmt; 30.09.2013
comment
Черт, я ввел неправильный код в lpastie. Когда я менял карту 4 на карту 1 (чтобы увидеть, было ли улучшение скорости связано с тем, что карта 1 появилась первой, что сделало ее как-то медленнее), я забыл изменить ее во второй части. Попробуйте перезапустить код с правильной картой. - person MdxBhmt; 30.09.2013
comment
@MdxBhmt Я думаю, что хаскеллеры все равно знают, как с этим справиться. Работа со строгостью требует изменения кода. Просить всех, кто хочет использовать map, написать свою собственную модифицированную версию, не очень разумно. - person Daniel Wagner; 30.09.2013
comment
Я не предлагал менять карту. Сначала я пытаюсь задаться вопросом, имеет ли значение эта оптимизация, где и как ее можно применить, а затем мы можем обсудить, как она будет задействована, либо прагмой, либо определяемой компилятором — опять же, это в последнюю очередь. Я хочу понять, что происходит с функцией map4 vs map. - person MdxBhmt; 30.09.2013
comment
@MdxBhmt: если вы хотите понять map4 против map, лучше всего посмотреть на сгенерированное ядро. Скомпилируйте свой код с -ddump-simpl (и, возможно, некоторыми другими флагами, такими как -dsuppress-coercions) и посмотрите, что выдает ghc. Тем не менее, я обнаружил, что развертывание циклов иногда является полезной оптимизацией в Haskell, но чаще всего важнее другие факторы. - person John L; 30.09.2013

Наряду с уже упомянутым подключаемым модулем развертывания ghc, на трассировке GHC есть страница. в котором обсуждается пилинг/развертывание. Разделы «Открытые вопросы» и «Ссылки» особенно раскрывают источники материала для дальнейших исследований.

person Elliot Robinson    schedule 30.09.2013