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

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

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

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

Вот код Haskell:

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#?

15
задан Benjamin 30 January 2014 в 09:36
поделиться

5 ответов

Я предлагаю вам взглянуть на Fortress Programming Language .

Это фактический язык программирования, и не псевдокод, но он был разработан как можно ближе к исполняемому псевдокоду. В частности, для того , чтобы намереваться синтаксиса они прочитали и проанализировали сотни CS и математических работ, курсов, книг и журналов, чтобы найти общие узоры использования для псевдокода и других вычислительных/математических обозначений.

Вы можете использовать все эти исследования, просто глядя на исходный код Fortress и абстрагируя вещи, которые вам не нужны, поскольку ваша целевая аудитория является человеком, в то время как Fortress's является компилятором.

Вот актуальный пример запуска кода Fortress из NAS (NASA Advanced Supercomputing) Сопряженный градиентный параллельный эталонный тест . Для получения удовольствия сравните спецификацию эталонного теста с реализацией в Fortress и обратите внимание на почти 1:1 соответствие. Также сравните реализацию в паре других языков, таких как C или Fortran, и обратите внимание, как они не имеют абсолютно никакого отношения к спецификации (а также часто на порядок длиннее спецификации).

Должен подчеркнуть: это не псевдокод, это действительный рабочий код Крепости! Пример кода крепости http ://StartFortresss.Sun.Com/Projects/Community/raw-attachment/wiki/Wiki/DiscovestionQuestions/NAS-CG.png

Изменить: ссылка «Выше пример кода» закрыта. Возможно, подобный пример можно найти здесь: https://umbilicus.wordpress.com/2009/10/16/fortress-parallel-by-default/

-121--1421877-

Из вашего вопроса я думаю, что вы хотите отобразить форму для редактирования записи человека в базе данных.

Код контроллера

// Normally data object is retrieved from the database
// This is just to simplify the code
$person = array('id' => 1, 'name' => 'stephenc');

// Pass to the view
$this->load->view('my_view_name', array('person' => $person));

Просмотр кода

<?php echo form_label('Your name: ', 'name'); ?>
<?php echo form_input(array('name' => 'name', 'value' => $person['name'])); ?>

Не забудьте повторить то, что возвращается от form_label и form_input. Это может быть то, где ты ошибаешься.

-121--4998062-

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

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

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

28
ответ дан 30 November 2019 в 23:54
поделиться

Другой способ исправить код так, чтобы он работал в 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, который должен оптимизировать чистый код, который вы пишете). В любом случае, если у вас есть несколько тестов, вы можете попробовать!

9
ответ дан 30 November 2019 в 23:54
поделиться

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

Обратите внимание, что благодаря слиянию циклов вы можете добиться большего в Haskell. Написав функцию create в виде перечисления, компилятор может объединить этап создания и цикл сворачивания в один цикл, который не выделяет промежуточных структур данных. Возможность такого общего слияния уникальна для 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
    }

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

$ 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

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

22
ответ дан 30 November 2019 в 23:54
поделиться

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

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

print_float (test (create 1000000 []));;
7
ответ дан 30 November 2019 в 23:54
поделиться

Программа отлично и быстро работает в 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})
3
ответ дан 30 November 2019 в 23:54
поделиться
Другие вопросы по тегам:

Похожие вопросы: