Переполнение стека в OCaml и F#, но не в Haskell

Я для интереса сравнивал разные языки на скорость выполнения следующей программы: для i от 1 до 1000000 сумма произведения i*(sqrt i)

Одна из моих реализаций (не единственная) строит список [1..1000000] и затем сворачивает с помощью определенной функции.

Программа отлично и быстро работает в Haskell (даже при использовании foldl, а не foldl'), но переполняет стек в OCaml и F#.

Вот код Хаскеля:

test = foldl (\ a b -> a + b * (sqrt b)) 0

create 0 = []
create n = n:(create (n-1))

main = print (test (create 1000000))

А вот и OCaml:

let test = List.fold_left (fun a b -> a +. (float_of_int b) *. (sqrt (float_of_int b))) 0. ;;

let rec create = function
    | 0 -> []
    | n -> n::(create (n-1)) ;;

print_float (test (create 1000000));;

Почему стек реализации OCaml/F# переполняется?


person FDP    schedule 20.02.2010    source источник


Ответы (5)


Код Haskell для create оценивает список лениво, т. е. по мере того, как элементы нужны foldl. Весь список сразу не нужен.

Напротив, F# и OCaml строго оценивают список create, т. е. код пытается сгенерировать весь список за один раз, прежде чем передать его в fold_left.

Одной из возможностей F# является использование выражения seq в функции create: это лениво генерирует список так же, как в Haskell. (Я не уверен, что в OCaml есть аналогичная функция.)

person Tim Robinson    schedule 20.02.2010
comment
OCaml имеет lazy и Lazy.force. Раньше это была очевидная реализация на стороне ML, которую любой мог написать со ссылкой на замыкание. Специальная поддержка была интегрирована в среду выполнения в версии 3.0? и дополнительная поддержка (сопоставление с образцом) в 3.11.0. В настоящее время GC удаляет косвенность через некоторое время после принудительной приостановки. ralyx.inria.fr/2008/Raweb/gallium/uid48.html - person Pascal Cuoq; 22.02.2010

Во-первых, если вы сравниваете производительность для числовых данных, списки — не лучший выбор. Попробуйте такой пакет, как vector для быстрых массивов.

И обратите внимание, что вы можете добиться еще большего успеха в Haskell благодаря слиянию циклов. Написав функцию создания в виде перечисления, компилятор может объединить этап создания и цикл свертывания в один цикл, который не выделяет промежуточных структур данных. Подобная возможность общего слияния уникальна для GHC Haskell.

Я буду использовать векторную библиотеку (слияние циклов на основе потоков):

import qualified Data.Vector as V

test = V.foldl (\ a b -> a + b * sqrt b) 0

create n = (V.enumFromTo 1 n)

main = print (test (create 1000000))

Теперь, прежде, с вашим кодом компилятор не может удалить все списки, и мы получаем внутренний цикл, например:

$wlgo :: Double# -> [Double] -> Double#

$wlgo =
  \ (ww_sww :: Double#) (w_swy :: [Double]) ->
    case w_swy of _ {
      [] -> ww_sww;
      : x_aoY xs_aoZ ->
        case x_aoY of _ { D# x1_aql ->
        $wlgo
          (+##
             ww_sww (*## x1_aql (sqrtDouble# x1_aql)))
          xs_aoZ
        }
    }

$wcreate :: Double# -> [Double]

$wcreate =
  \ (ww_swp :: Double#) ->
    case ==## ww_swp 0.0 of _ {
      False ->
        :
          @ Double
          (D# ww_swp)
          ($wcreate (-## ww_swp 1.0));
      True -> [] @ Double
    }

Обратите внимание на два цикла: create, генерирующий (ленивый) список, и fold, потребляющий его. Из-за лени стоимость этого списка дешева, поэтому он выглядит прилично:

$ time ./C
4.000004999999896e14
./C  0.06s user 0.00s system 98% cpu 0.058 total

Однако при слиянии мы получаем только один цикл!

main_$s$wfoldlM_loop :: Double# -> Double# -> Double#

main_$s$wfoldlM_loop =
  \ (sc_sYc :: Double#) (sc1_sYd :: Double#) ->
    case <=## sc_sYc 1000000.5 of _ {
      False -> sc1_sYd;
      True ->
        main_$s$wfoldlM_loop
          (+## sc_sYc 1.0)
          (+##
             sc1_sYd (*## sc_sYc (sqrtDouble# sc_sYc)))

GHC сократил шаги создания и тестирования до одного цикла без использования списков. Всего 2 дубля в регистрах. И с вдвое меньшим количеством циклов он работает почти в два раза быстрее:

$ ghc D.hs -Odph -fvia-C -optc-O3 -optc-march=native -fexcess-precision --make
$ time ./D
4.000005000001039e14
./D  0.04s user 0.00s system 95% cpu 0.038 total

Это хороший пример силы, которую обеспечивают гарантии чистоты — компилятор может очень агрессивно переупорядочивать ваш код.

person Don Stewart    schedule 20.02.2010
comment
Странно, у меня моя версия выполняется 44 мс, а ваша 76! - person FDP; 22.02.2010
comment
Требуется патч к векторной библиотеке для плавкого двойника (получите его из версии вектора darcs, http://code.haskell.org/vector) - person Don Stewart; 22.02.2010
comment
@Farnand Pajot: вы использовали командную строку дона для компиляции кода? Я думаю, что слияние циклов происходит только тогда, когда включена оптимизация. - person liwp; 22.02.2010
comment
Да, я также использовал эти оптимизации для простой хвостовой рекурсии (без списка), и время выполнения в 3 раза лучше! - person FDP; 22.02.2010

Еще один способ исправить код так, чтобы он работал в F#, — использовать выражения последовательности, которые также генерируются лениво и таким образом, чтобы не вызывать StackOverflow (это не будет работать в OCaml, поскольку выражения последовательности специфичны для F#). Вот код:

let test nums = Seq.fold (fun a b -> 
  a + (float b) * (sqrt (float b))) 0.0 nums

let rec create count = seq {
  match count with
  | 0 -> do ()
  | n -> yield n
         yield! create (n-1) }

printf "%f" (test (create 1000000));; 

Я не уверен в производительности, однако компилятор определенно оптимизирует функцию create, так что итерация по сгенерированной последовательности должна быть достаточно быстрой. Однако целью выражений последовательности в основном является удобочитаемость (поскольку F# не является чистым, он дает вам много места для ручной оптимизации, если они вам действительно нужны, в отличие от Haskell, которому нужно оптимизировать чистый код, который вы пишете). В любом случае, если у вас есть тесты, вы можете попробовать!

person Tomas Petricek    schedule 20.02.2010
comment
И чтобы заставить версию OCaml работать лениво, как версию Haskell, вы можете использовать модуль Lazy_list, который является частью BatteriesIncluded. batteries.forge.ocamlcore.org/doc. предварительный просмотр%3Abatteries-beta1/ - person aneccodeal; 20.02.2010
comment
Спасибо за дополнение — в F# тоже есть модуль LazyList (его можно найти в PowerPack), но он используется реже, чем выражения последовательности. - person Tomas Petricek; 20.02.2010

В этой форме ваша функция create не является хвостовой рекурсией. Вы можете переписать его в хвостовой рекурсивной форме, которая не вызывает переполнения стека:

let rec create n l =
    match n with 
    | 0 -> l
    | n -> create (n-1) (n::l)

print_float (test (create 1000000 []));;
person Stringer    schedule 20.02.2010
comment
Это правда, но по какой-то причине это сильно влияет на производительность (по крайней мере, для Haskell), время выполнения умножается на 3. - person FDP; 20.02.2010
comment
@Fernand: В Haskell лучше использовать исходную функцию из-за ленивой оценки. В отсутствие ленивых вычислений лучше — фактически почти необходимо — использовать хвостовую рекурсивную версию. - person Michał Marczyk; 20.02.2010
comment
В этой версии OCaml по-прежнему не создает весь список в памяти перед вычислением сгиба? В Haskell список создается по мере его использования, поэтому он никогда не бывает в памяти сразу. - person MtnViewMark; 20.02.2010
comment
@mtnviewmark: В OCaml вы можете использовать модуль LazyList, который является частью BatteriesIncluded, чтобы он работал очень похоже на версию Haskell: batteries.forge.ocamlcore.org/doc.preview%3Abatteries-beta1/ - person aneccodeal; 20.02.2010

Программа отлично и быстро работает в Haskell (даже при использовании foldl, а не foldl'), но переполняет стек в OCaml и F#.

Ваш Haskell использует ленивые списки, но ваш OCaml/F# использует строгие списки, поэтому ваши программы несопоставимы.

FWIW, решение, использующее последовательности по требованию в F #:

Seq.sumBy (fun i -> let i = float i in i * sqrt i) (seq {1..1000000})
person J D    schedule 11.05.2010