Haskell - эффективный эквивалент цикла for?

Я делал несколько экспериментов, и вот что я нашел. Рассмотрим следующую программу C:

int main()
{
   for(int i = 0;
       i < 1000000;
       ++i)
   {}
}

и следующую программу Haskell:

import System.IO

loop :: Int -> IO ()
loop n = if 0 == n then return () else loop (n-1)

main = loop 1000000

Вот вывод time для вышеуказанной программы C:

real    0m0.003s
user    0m0.000s
sys 0m0.000s

... и для программы Haskell:

real    0m0.028s
user    0m0.027s
sys 0m0.000s

Сначала я думал, что gcc обнаружил пустой цикл и оптимизировал его, но после увеличения количества итераций время работы программы также увеличилось. Вот выходы time для обеих программ, количество итераций которых равно 10000000:

C версия

real    0m0.024s
user    0m0.023s
sys 0m0.000s

версия Haskell

real    0m0.245s
user    0m0.247s
sys 0m0.000s

Как вы можете видеть, программа Haskell в 10 раз медленнее.

Вопрос: какова эффективная альтернатива циклу for в Haskell? Как мы только что видели, простая рекурсия замедляет работу программы примерно в 10 раз (и это, вероятно, с помощью оптимизации хвостовой рекурсии).

Ответ 1

Во-первых, вы перевели бы свой код C,

main = go 0
    where
        go :: Int -> IO ()
        go i | i < 1000000 = go (i+1)
             | otherwise   = return ()

который ghc оптимизирует для пустой программы. Он перемещает окончательное значение в регистр, сравнивается с ним и возвращает ():

Main_zdwa_info: 
    cmpq    $1000000, %r14          # imm = 0xF4240
    movl    $1000000, %eax          # imm = 0xF4240
    cmovlq  %rax, %r14
    movq    (%rbp), %rax
    movl    $ghczmprim_GHCziUnit_Z0T_closure+1, %ebx
    jmpq    *%rax  # TAILCALL

который при запуске:

$ time ./A
./A  0.00s user 0.00s system 88% cpu 0.004 total

не занимает времени.


В целом, однако, GHC испускает эквивалентные циклы например. GCC, для хвостовых рекурсивных функций. В частности, вы хотите скомпилировать числовые тесты с ghc -O2 -fllvm для достижения наилучших результатов. Если вы не хотите, чтобы ваши программы были оптимизированы, тогда ghc с удовольствием выполнит именно указанную вами программу, которая в этом случае включает в себя много избыточной работы, которая будет удалена на более высоких уровнях оптимизации.

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

Ответ 2

Для циклических конструкций вам часто приходится писать в стиле Worker/Wrapper, чтобы помочь оптимизировать точки размещения GHC, а не возвращаться на внешний уровень функции.

Григорий Джавадян сказал в комментарии, что исходная версия оптимизирована на -O3, я ожидаю, что эта версия будет обнаружена на -O2:

import System.IO

loop :: Int -> IO ()
loop n = go n
  where
    go n | n <= 0 = return ()
    go n          = go (n-1)

main = loop 1000000