Оптимизация GHC

Две функции Haskell ниже, по-видимому, различаются только тем, является ли индексная переменная неявной или явной, но разница в производительности на два порядка.

Эта функция занимает около 0,03 секунды для вычисления mfib 30:

let mfib = (map fib [0..] !!)
  where
    fib 0 = 0
    fib 1 = 1
    fib x = mfib (x-1) + mfib (x-2)

Эта функция занимает около 3 секунд для mfib 30:

let mfib i = map fib [0..] !! i
  where
    fib 0 = 0
    fib 1 = 1
    fib x = mfib (x-1) + mfib (x-2)

Я предполагаю, что это связано с встроенными правилами GHC и пытались добавить inline/noinline pragmas, чтобы получить соответствующую производительность.

EDIT: Я понимаю, как поиск в ленивом списке можно использовать для memoize функции fib и почему традиционное определение fib очень медленное. Я ожидал, что memoization будет работать во второй функции, а также в первой, и не понимаю, почему это не так.

Ответ 1

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

let mfib = let fib 0 = 0
               fib 1 = 1
               fib x = mfib (x-1) + mfib (x-2)
           in (!!) (map fib [0..])

против

let mfib = \i ->
               let fib 0 = 0
                   fib 1 = 1
                   fib x = mfib (x-1) + mfib (x-2)
               in map fib [0..] !! i

Обратите внимание, что во второй программе выражение map fib [0..] появляется внутри \i -> ..., поэтому оно (как правило, без оптимизации) оценивается для каждого значения i. См. Когда автоматическая запись в GHC Haskell?

Ответ 2

Нет, это не имеет никакого отношения к inlining. Разница в том, что mfib = (map fib [0..] !!) не имеет аргументов. Это все еще функция, конечно, но оценка этой функции не требует передачи каких-либо аргументов. В частности, оценка этого mfib будет генерировать список fib таким образом, чтобы его можно было повторно использовать для всех индексов.

OTOH, mfib i = map fib [0..] !! i означает, что весь блок where будет рассматриваться только тогда, когда вы фактически передаете аргумент i.

Эти два варианта отличаются друг от друга, если вы многократно оцениваете функцию много раз. К сожалению, для второй версии функции собственной рекурсии уже называет ее снова и снова! Поэтому mfib (x-1) + mfib (x-2) необходимо выполнить всю работу mfib (x-1), а затем снова всю работу mfib (x-2). Таким образом, mfib n берет более чем в два раза вычислительную стоимость mfib (n-1), поэтому mfib ∈ O (2 n).

Это невероятно расточительно, потому что большинство терминов в mfib (x-2) также уже находятся в mfib (x-1) и могут быть просто повторно использованы. Ну, именно то, что делает ваша первая версия, потому что она вычисляет список fib один раз и для всех индексов, поэтому оценка mfib (x-1) будет уже выполнять большую часть работы, которую затем можно просто перечитать с помощью mfib (x-2), уменьшая сложность полинома.