Когда в GHC Haskell автоматическая автоматизация?
Я не могу понять, почему m1 явно memoized, а m2 не в следующем:
m1 = ((filter odd [1..]) !!)
m2 n = ((filter odd [1..]) !! n)
m1 10000000 занимает около 1,5 секунд при первом вызове, а часть этого на последующих вызовах (предположительно, кэширует список), тогда как m2 10000000 всегда занимает такое же количество времени (перестраивая список с каждым вызовом). Любая идея, что происходит? Существуют ли какие-либо эмпирические правила относительно того, когда и когда GHC будет меморировать функцию? Спасибо.
Ответы
Ответ 1
GHC не запоминает функции.
Однако он вычисляет любое заданное выражение в коде не чаще одного раза в раз, когда его окружающее лямбда-выражение вводится, или не более того, если оно находится на верхнем уровне. Определение, где лямбда-выражения могут быть немного сложными, когда вы используете синтаксический сахар, как в вашем примере, поэтому давайте преобразуем их в эквивалентный десугатный синтаксис:
m1' = (!!) (filter odd [1..]) -- NB: See below!
m2' = \n -> (!!) (filter odd [1..]) n
(Примечание: отчет Haskell 98 фактически описывает левый операторский раздел, такой как (a %)
, как эквивалент \b -> (%) a b
, но GHC присваивает его значению (%) a
. Это технически разные, потому что их можно отличить seq
. Я думаю, что, возможно, я отправил билет GHC Trac об этом.)
Учитывая это, вы можете видеть, что в m1'
выражение filter odd [1..]
не содержится ни в одном лямбда-выражении, поэтому оно будет вычисляться только один раз за запуск вашей программы, а в m2'
, filter odd [1..]
будет вычислен каждый раз, когда вводится лямбда-выражение, т.е. при каждом вызове m2'
. Это объясняет разницу в времени, которое вы видите.
На самом деле, некоторые версии GHC с определенными опциями оптимизации будут иметь больше значений, чем указано выше. Это может быть проблематично в некоторых ситуациях. Например, рассмотрим функцию
f = \x -> let y = [1..30000000] in foldl' (+) 0 (y ++ [x])
GHC может заметить, что y
не зависит от x
и переписывает функцию на
f = let y = [1..30000000] in \x -> foldl' (+) 0 (y ++ [x])
В этом случае новая версия гораздо менее эффективна, потому что ей нужно будет прочитать около 1 ГБ из памяти, где хранится y
, а исходная версия будет работать в постоянном пространстве и вписываться в кеш процессора. Фактически, под GHC 6.12.1 функция f
почти вдвое быстрее скомпилирована без оптимизации, чем скомпилирована с помощью -O2
.
Ответ 2
m1 вычисляется только один раз, поскольку это константная аппликативная форма, а m2 не является CAF и поэтому вычисляется для каждой оценки.
См. вики GHC в CAFs: http://www.haskell.org/haskellwiki/Constant_applicative_form
Ответ 3
Существует существенное различие между двумя формами: ограничение мономорфизма применяется к m1, но не m2, так как m2 явно задает аргументы. Таким образом, тип m2 является общим, но m1 является специфическим. Типы, которые они назначены:
m1 :: Int -> Integer
m2 :: (Integral a) => Int -> a
Большинство компиляторов и интерпретаторов Haskell (все из которых я знаю на самом деле) не memoize полиморфных структур, поэтому внутренний список m2 воссоздается каждый раз, когда он вызывал, где m1 не является.
Ответ 4
Я не уверен, потому что я совершенно незнаком с самим Haskell, но, похоже, что эта вторая функция параметризована, а первая - нет. Характер функции заключается в том, что ее результат зависит от входного значения, а в функциональной парадигме особенно зависит ТОЛЬКО от ввода. Очевидная импликация заключается в том, что функция без параметров всегда возвращает одно и то же значение независимо от того, что.
В этом случае оптимизатор-механик компилятора GHC использует этот факт для вычисления значения такой функции только один раз для всей исполняемой программы. Он делает это лениво, конечно, но тем не менее делает это. Я сам это заметил, когда написал следующую функцию:
primes = filter isPrime [2..]
where isPrime n = null [factor | factor <- [2..n-1], factor `divides` n]
where f `divides` n = (n `mod` f) == 0
Затем, чтобы проверить это, я вошел в GHCI и написал: primes !! 1000
. Это заняло несколько секунд, но, наконец, я получил ответ: 7927
. Затем я позвонил primes !! 1001
и получил ответ мгновенно. Точно так же в одно мгновение я получил результат для take 1000 primes
, потому что Haskell должен был вычислить весь список тысяч элементов, чтобы вернуть 1001-й элемент раньше.
Таким образом, если вы можете написать свою функцию таким образом, чтобы она не принимала никаких параметров, вы, вероятно, захотите ее.;)