Документация функции seq
говорит следующее:
Примечание по порядку оценки: выражение
seq a b
не гарантирует, чтоa
будет оцениваться доb
. Единственная гарантия, предоставляемаяseq
, заключается в том, что обаa
иb
будут оцениваться до того, какseq
вернет значение. В частности, это означает, чтоb
можно оценить доa
. Если вам нужно гарантировать определенный порядок оценки, вы должны использовать функциюpseq
из пакета "parallel".
Итак, у меня есть ленивая версия функции sum
с аккумулятором:
sum :: Num a => [a] -> a
sum = go 0
where
go acc [] = acc
go acc (x:xs) = go (x + acc) xs
Очевидно, что это очень медленно в больших списках. Теперь я переписываю эту функцию с помощью seq
:
sum :: Num a => [a] -> a
sum = go 0
where
go acc [] = acc
go acc (x:xs) = let acc' = x + acc
in acc' `seq` go acc' xs
И я вижу огромное увеличение производительности! Но интересно, насколько он надежный? Получил ли я это по везению? Потому что GHC может сначала оценить рекурсивный вызов (согласно документации) и все еще накапливать thunks. Похоже, мне нужно использовать pseq
, чтобы гарантировать, что acc'
всегда оценивается до рекурсивного вызова. Но с pseq
я вижу снижение производительности по сравнению с версией seq
. Числа на моей машине (для расчета sum [1 .. 10^7]
:
- наивный:
2.6s
-
seq
:0.2s
-
pseq
:0.5s
Я использую GHC-8.2.2 и компилирую команду stack ghc -- File.hs
.
После того, как я попытался скомпилировать с stack ghc -- -O File.hs
, разница в производительности команды между seq
и pseq
исчезла. Теперь они работают в 0.2s
.
Итак, моя реализация демонстрирует свойства, которые я хочу? Или у GHC есть некоторые особенности реализации? Почему pseq
медленнее? Существует ли какой-то пример, где seq a b
имеет разные результаты в зависимости от порядка оценки (тот же код, но разные флаги компилятора/разные компиляторы/etc.)?