Выполнение подвыражения один раз

Хорошо, так что это меня беспокоило какое-то время, поэтому я подумал, что приду и спрошу кого-нибудь, кто мог бы действительно узнать ответ.

Предположим, что у меня есть следующая функция:

foobar x y = expensive x + cheap y

Предположим, что эта часть программы принимает в качестве входных данных foobar 5 и выполняет эту функцию миллионы раз в замкнутом цикле. Ясно, что я хочу, чтобы expensive 5 вычислялся один раз, а не миллион раз.

Я мог оставить код таким, какой он есть, или я могу изменить его на

foobar x = let k = expensive x in \ y -> k + cheap y

Мне это интересно...

  • Является ли GHC достаточно умным, чтобы устранить дублируемую работу? (I.e., делает ли первая версия то, что я хочу?)

  • Если нет, действительно ли вторая версия исправляет проблему? (I.e., оптимизатор просто преобразует его обратно в тот же код, что и первая версия?)

Ответ 1

Is GHC smart enough to eliminate the duplicated work by itself? (I.e., does the first version do what I want already?)

Я думаю, что еще один способ задать вопрос: будет ли GHC inline foobar x y, чтобы expensive x делился между вычислениями?

Я спросил аналогичный вопрос, который прояснил несколько вещей, но оставил меня немного неудовлетворенным. Насколько я понимаю, определение того, как и когда делать inline или eta-expand/уменьшать вещи (и при столь тонком изменении поведения/семантики строгости) действительно сложно для компилятора, поэтому GHC в значительной степени зависит от того, как вы синтаксически определили вашу функцию.

Я думаю, что короткий ответ заключается в том, что GHC может преобразовать вашу первую функцию во вторую, но единственный способ убедиться в том, что вы должны писать свои функции, поэтому синтаксис дает подсказки компилятора о том, как вы хотите, чтобы все было привязано к получить доступ, который вы хотите, а затем предоставить INLINE прагмы. Здесь еще интересное обсуждение этой проблемы

Ответ 2

Интуитивно мой ответ был бы отрицательным, и да. Но позвольте мне ответить на ваш вопрос, попробовав его. Рассмотрим этот код:

import Debug.Trace

expensive :: Int -> Int
expensive x = trace ("expensive evaluated for " ++ show x) $ x
{-# NOINLINE expensive #-}

cheap :: Int -> Int
cheap x = x
{-# NOINLINE cheap #-}

foobar x y = expensive x + cheap y

foobar' x = let k = expensive x in \ y -> k + cheap y

part f = sum [f i| i<-[0..10]]

main = do
    print $ part (foobar 5)
    print $ part (foobar' 5)

Если мы запустим этот результат, получим

$ ./Test 
expensive evaluated for 5
110
expensive evaluated for 5
110

чтобы компилятор был достаточно умен, чтобы оптимизировать исходную версию. Но почему? Поскольку он ввел определение foobar в main, то заметил, что он мог бы выгрузить выражение expensive 5 из вызова part. Если мы отключим вставку для foobar и foobar' (или, альтернативно, не используем -O), она больше не работает:

$ ./Test 
expensive evaluated for 5
expensive evaluated for 5
expensive evaluated for 5
expensive evaluated for 5
expensive evaluated for 5
expensive evaluated for 5
expensive evaluated for 5
expensive evaluated for 5
expensive evaluated for 5
expensive evaluated for 5
expensive evaluated for 5
110
expensive evaluated for 5
110

Итак, хотя GHC может в некоторых ситуациях делать правильные вещи, вы всегда должны проверить, действительно ли это так, если вы хотите положиться на нее. Либо используйте такие инструменты, как Debug.Trace, либо просматривая ядро ​​(-ddump-simpl).

Ответ 3

Читая одну из различных статей STG, похоже, что это так называемая полная трансформация лени. Кажется, что [в момент написания статьи] GHC применяет это преобразование, но не все время, так как это может привести к утечке пространства.

Канонический пример:

foo x = map f [1..1000000]

Если мы преобразуем это в

foo x = map f big

big = [1..1000000]

теперь у нас один гигантский CAF, висящий навсегда - что, вероятно, не то, что планировал программист! (Я лично был укушен именно таким образом, на самом деле...)