F# vs OCaml: переполнение стека



недавно я нашел презентацию о F# для программистов Python, и после просмотра его, решил реализовать решение "муравьиной головоломки" самостоятельно.



есть муравей, который может ходить на плоской сетке. Муравей может двигаться по одному пространству за раз влево, вправо, вверх или вниз. То есть, из клетки (X, г) муравей может пройти в клетки (х+1, г), (х-1, У), (Х, Y+1), и (Х, Y-1). Точки, где сумма цифр координат x и y больше 25 недоступны для муравья. Например, точка (59,79) недоступна, потому что 5 + 9 + 7 + 9 = 30, что больше, чем 25. Вопрос в том, сколько точек может получить доступ муравей, если он начинается с (1000, 1000), включая (1000, 1000) себя?



я реализовал свое решение в 30 строках данные, используемые первого, и попытался это:



$ ocamlopt -unsafe -rectypes -inline 1000 -o puzzle ant.ml
$ time ./puzzle
Points: 148848

real 0m0.143s
user 0m0.127s
sys 0m0.013s


аккуратно, мой результат такой же, как и у реализация Леонардо, В D и C++. По сравнению с реализация Леонардо C++, версия OCaml работает примерно в 2 раза медленнее, чем C++. Это нормально, учитывая, что Леонардо использовал очередь для удаления рекурсии.



Я тут перевел код на F# ... и вот что я получил:



Thanassis@HOME /g/Tmp/ant.fsharp
$ /g/Program Files/FSharp-2.0.0.0/bin/fsc.exe ant.fs
Microsoft (R) F# 2.0 Compiler build 2.0.0.0
Copyright (c) Microsoft Corporation. All Rights Reserved.

Thanassis@HOME /g/Tmp/ant.fsharp
$ ./ant.exe

Process is terminated due to StackOverflowException.
Quit

Thanassis@HOME /g/Tmp/ant.fsharp
$ /g/Program Files/Microsoft F#/v4.0/Fsc.exe ant.fs
Microsoft (R) F# 2.0 Compiler build 4.0.30319.1
Copyright (c) Microsoft Corporation. All Rights Reserved.

Thanassis@HOME /g/Tmp/ant.fsharp
$ ./ant.exe

Process is terminated due to StackOverflowException


переполнение стека... с обоих версий F# у меня есть в моей машине...
Из любопытства я взял сгенерированный двоичный файл (ant.exe) и запустить его под Arch Linux / Mono:



$ mono -V | head -1
Mono JIT compiler version 2.10.5 (tarball Fri Sep 9 06:34:36 UTC 2011)

$ time mono ./ant.exe
Points: 148848

real 1m24.298s
user 0m0.567s
sys 0m0.027s


Удивительно, но он работает под моно 2.10.5 (т. е. нет переполнения стека) - но это занимает 84 секунды, т. е. в 587 раз медленнее, чем OCaml - oops.



Так эта программа...




  • работает нормально под OCaml

  • не работает вообще под .NET / F#

  • работает, но очень медленно, под Mono/F#.


почему?



EDIT: странности продолжаются - с помощью "оптимизации --+ --проверено-" проблема исчезнет, но только под ArchLinux / Mono ; под Windows XP и Windows 7 / 64bit даже оптимизированная версия двоичного стека переполняется.



окончательное редактирование: я сам нашел ответ-см. ниже.

529   2  

2 ответов:

резюме:

  • Я написал простую реализацию алгоритма... это не хвост-рекурсивный.
  • я скомпилировал его с OCaml под Linux.
  • Он работал нормально, а закончил в 0,14 секунды.

Это было тогда время для порта на F#.

  • Я перевел код (прямой перевод) для F#.
  • я скомпилировал под Windows, и запустить его - я получил переполнение стека.
  • Я взял на бинарных под Linux, и запустить его под моно.
  • это сработало, но работает очень медленно (84 секунды).

Я тогда написал в Stack Overflow-но некоторые люди решили закрыть вопрос (вздох).

  • Я пробовал компилировать с --optimize+ -- checked -
  • двоичный стек все еще переполнен под окнами...
  • ...но запустите нормально (и закончите через 0,5 секунды) под Linux/Mono.

пришло время проверить размер стека: под Окна,еще один пост SO указал, что по умолчанию он установлен на 1MB. Под Linux, "uname-s" и компиляция тестовой программы ясно показал, что это 8 МБ.

это объясняло, почему программа работала под Linux, а не под Windows (программа использовала более 1 МБ стека). Это не объяснило, почему оптимизированная версия работает намного лучше под моно, чем неоптимизированная: 0,5 секунды против 84 секунд (хотя кажется, что --optimize+ установите по умолчанию, см. комментарий кита с извлечением " Expert F#"). Вероятно, это связано с сборщиком мусора Mono, который каким-то образом был доведен до крайности 1-й версией.

разница между Linux / OCaml и Linux/Mono / F# время выполнения (0,14 против 0,5) из-за простого способа я измерил его: "время ./двоичный. .."также измеряет время запуска, что важно для Mono/.NET (ну, важно для этой простой маленькой проблемы).

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

новая версия работает нормально под Windows, а также, и закончил в 0.5 секунд.

Итак, мораль истории:

  • остерегайтесь использования стека, особенно если вы используете его много и работаете под Windows. Используйте EDITBIN с помощью / STACK option чтобы установить двоичные файлы на большие размеры стека, или еще лучше, напишите свой код таким образом, который не зависит от использования слишком большого стека.
  • OCaml может быть лучше при устранении хвостовой рекурсии, чем F# - или это сборщик мусора делает лучшую работу в этой конкретной проблеме.
  • не отчаивайтесь ...грубые люди закрывают ваши вопросы переполнения стека, хорошие люди будут противодействовать им в конце концов - если вопросы действительно хороши : -)

P.S. Некоторые дополнительные данные от доктора Джона Харропа:

...вам просто повезло,что OCaml не переполнялся. Вы уже определили, что фактические размеры стека различаются между платформами. Другой аспект той же проблемы заключается в том, что разные языковые реализации ешьте пространство стека с разной скоростью и имеют разную производительность характеристики при наличии глубоких стеков. Данные, используемые, моно .Чистая все используют разные данные представления и алгоритмы GC, которые влияют результаты исследования... (A) OCaml использует помеченные целые числа для различения указателей, давая компактные рамки стека, и будет пересекать все на стеке ищу указатели. Пометка по существу передает только достаточно информации для времени выполнения OCaml, чтобы иметь возможность пересекать кучу (b) Mono обрабатывает слова на стеке консервативно как указатели: если, как указатель, слово будет указывать в выделенный кучей блок, то этот блок считается достижимый. (c) я не знаю алгоритм .NET, но я не удивлюсь, если он съел стек пространство быстрее и все еще проходило каждое слово в стеке (это, конечно страдает патологической производительностью от GC, если несвязанный поток имеет глубокая стопка!)... Кроме того, использование выделенных кучами кортежей означает, что вы будете заполнять поколение питомника (например, gen0) быстро и поэтому, вызывая сборщик мусора, чтобы пройти эти глубокие стеки часто...

позвольте мне попытаться обобщить ответ.

есть 3 пункта, которые должны быть сделаны:

  • проблема: переполнение стека происходит на рекурсивную функцию
  • это происходит только под windows: на linux, для проверенного размера proble, он работает
  • тот же (или аналогичный) код в OCaml работает
  • оптимизация+ флаг компилятора, для проверенного размера proble, работает

очень часто исключение переполнения стека является результатом рекурсивный ВАЛЛ. Если вызов находится в хвостовой позиции, компилятор может распознать его и применить оптимизацию tailcall, поэтому рекурсивные вызовы не будут занимать пространство стека. Оптимизация Tailcall может происходить в F#, в CRL или в обоих:

оптимизация хвоста CLR1

F# рекурсия (более общая) 2

F# tail calls 3

правильное объяснение "сбой в windows, а не в linux", как другой сказал, что по умолчанию зарезервировано пространство стека на двух ОС. Или лучше, зарезервированное пространство стека, используемое компиляторами под двумя ОС. По умолчанию VC++ резервирует только 1 МБ пространства стека. Среда CLR (вероятно) компилируется с VC++, поэтому она имеет это ограничение. Зарезервированное пространство стека может быть увеличено во время компиляции, но я не уверен, что его можно изменить на скомпилированных исполняемых файлах.

EDIT: оказывается, это можно сделать (см. Этот пост в блоге http://www.bluebytesoftware.com/blog/2006/07/04/ModifyingStackReserveAndCommitSizesOnExistingBinaries.aspx) Я бы не рекомендовал его, но в экстремальных ситуациях, по крайней мере, это возможно.

OCaml версия может работать, потому что она была запущена под Linux. Однако, было бы интересно протестировать также версию OCaml под Windows. Я знаю, что компилятор OCaml более агрессивен при оптимизации хвостового вызова, чем F#.. может ли он даже извлечь хвостовую функцию из вашего оригинальный код?

мое предположение о "-- optimize+ " заключается в том, что он все равно заставит код повторяться, поэтому он все равно не будет работать под Windows, но смягчит проблему, заставив исполняемый файл работать быстрее.

наконец, окончательное решение заключается в использовании хвостовой рекурсии (путем перезаписи кода или реализации агрессивной оптимизации компилятора); это хороший способ избежать проблемы переполнения стека с рекурсивными функциями.

Comments

    Ничего не найдено.