Документация функции 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.)?