F # против OCaml: переполнение стека - PullRequest
61 голосов
/ 24 сентября 2011

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

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

Я реализовал свое решение в 30 OCaml сначала и опробовал его:

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

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

Аккуратно,мой результат такой же, как и у реализации Леонардо, в D и C ++ .По сравнению с реализацией leonardo 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

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

Итак, эта программа ...

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

Почему?

РЕДАКТИРОВАТЬ: Странность продолжается - Использование «--optimize + --checked-» делаетпроблема исчезнет, ​​, но только под ArchLinux / Mono ;в Windows XP и Windows 7 / 64bit переполняется даже оптимизированная версия двоичного стека.

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

Ответы [ 2 ]

72 голосов
/ 30 сентября 2011

Резюме:

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

Затем пришло время портировать на F #.

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

Затем я написал в Stack Overflow, но некоторые решили закрыть вопрос (вздох).

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

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

Это объясняет, почему программа работала под Linux, а не под Windows (программа использовала более 1 МБ стека). Это не объясняет, почему оптимизированная версия работает намного лучше в Mono, чем неоптимизированная: 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 , чтобы установить в ваших двоичных файлах больший размер стека или, что еще лучше, написать свой код так, чтобы он не зависел от использования слишком большого количества стеков.
  • OCaml может быть лучше в устранении хвостовой рекурсии, чем F # - или сборщик мусора справляется с этой конкретной задачей лучше.
  • Не отчаивайтесь из-за ... грубых людей, закрывающих ваши вопросы о переполнении стека, хорошие люди в конце концов будут противодействовать им - если вопросы действительно хорошие: -)

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

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

8 голосов
/ 30 сентября 2011

Позвольте мне попытаться суммировать ответ.

Необходимо сделать 3 замечания:

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

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

Оптимизация хвоста CLR 1

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

F # хвостовые вызовы 3

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

РЕДАКТИРОВАТЬ: оказывается, что это можно сделать (см. Этот пост в блоге http://www.bluebytesoftware.com/blog/2006/07/04/ModifyingStackReserveAndCommitSizesOnExistingBinaries.aspx) Я бы не рекомендовал это делать, но в экстремальных ситуациях, по крайней мере, это возможно.

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

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

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

...