FSharp запускает мой алгоритм медленнее, чем Python

Несколько лет назад я решил проблему с помощью динамического программирования:

https://www.thanassis.space/fillupDVD.html

Решение было закодировано в Python.

В рамках расширения моих горизонтов я недавно начал изучать OCaml/F #. Какой лучший способ проверить воды, чем сделать прямой порт императивного кода, который я написал в Python, чтобы F # - и начать оттуда, продвигаясь к функциональному программному решению.

Результаты этого первого, прямого порта... обескураживают:

В Python:

  bash$ time python fitToSize.py
  ....
  real    0m1.482s
  user    0m1.413s
  sys     0m0.067s

В FSharp:

  bash$ time mono ./fitToSize.exe
  ....
  real    0m2.235s
  user    0m2.427s
  sys     0m0.063s

(в случае, если вы заметили "моно" выше: я тестировал и под Windows, с Visual Studio - с той же скоростью).

Я... озадачен, мягко говоря. Python выполняет код быстрее, чем F #? Скомпилированный двоичный файл, использующий среду выполнения .NET, запускает SLOWER, чем интерпретируемый код Python?!?!

Я знаю о затратах на запуск виртуальных машин (в данном случае - моно) и о том, как JIT улучшают вещи для таких языков, как Python, но все же... Я ожидал ускорения, а не замедления!

Я сделал что-то неправильно, возможно?

Я загрузил код здесь:

https://www.thanassis.space/fsharp.slower.than.python.tar.gz

Обратите внимание, что код F # является более или менее прямым, поэтапным переводом кода Python.

P.S. Конечно, есть и другие преимущества, например. безопасность статического типа, предлагаемая F #, но если результирующая скорость императивного алгоритма хуже при F #... Я разочарован, мягко говоря.

РЕДАКТИРОВАТЬ: прямой доступ, как указано в комментариях:

код Python: https://gist.github.com/950697

код FSharp: https://gist.github.com/950699

Ответ 1

Д-р Джон Харроп, с которым я связался по электронной почте, объяснил, что происходит:

Проблема в том, что программа была оптимизирована для Python. Это обычно, когда программист более знаком с одним языком, чем с другим, конечно. Вам просто нужно изучить другой набор правил, которые определяют, как оптимизировать программы F #... Несколько вещей выскочили на меня, такие как использование цикла "for я in 1..n do", а не цикл "for я = 1 to n do" (который быстрее вообще, но не значим здесь), многократно делая List.mapi в списке, чтобы имитировать индекс массива (который излишне распределял промежуточные списки без необходимости) и использование F # TryGetValue для словаря, который выделяется без необходимости (.NET TryGetValue, который принимает ref быстрее, но не так много здесь)

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

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

Джон преобразовал мой код (назад:-)) в его версию array и работает со скоростью 100x.

Мораль истории:

  • Словарь F # нуждается в работе... при использовании кортежей в качестве ключей, скомпилированный F # медленнее интерпретируемых хэш-таблиц Python!
  • Очевидно, но никакого вреда в повторении: Чистый код иногда означает... гораздо медленнее код.

Спасибо, Джон, очень ценю.

EDIT: тот факт, что замена словаря Array делает F # окончательно запущенным на скоростях, с которых должен работать компилируемый язык, не отрицает необходимость исправления в скорости словаря (надеюсь, F # люди из MS читают это). Другие алгоритмы зависят от словарей/хэшей и не могут быть легко переключены на использование массивов; когда программа использует словарь, возможно, является ошибкой. Если, как некоторые говорили в комментариях, проблема не в F #, а в .NET Dictionary, то я бы сказал, что это... ошибка в .NET!

EDIT2: самое ясное решение, которое не требует, чтобы алгоритм переключался на массивы (некоторые алгоритмы просто не поддаются этому) заключается в том, чтобы изменить это:

let optimalResults = new Dictionary<_,_>()

в это:

let optimalResults = new Dictionary<_,_>(HashIdentity.Structural)

Это изменение заставляет код F # работать в 2,7 раза быстрее, и, наконец, он удаляет Python (на 1,6 раза быстрее). Странно, что кортежи по умолчанию используют структурное сравнение, поэтому в принципе сравнения, сделанные Словарем по ключам, одинаковы (с или без Структурного). Д-р Харроп полагает, что разница в скорости может быть отнесена к виртуальной отправке: "AFAIK,.NET мало что делает для оптимизации виртуальной диспетчеризации, а стоимость виртуальной диспетчеризации чрезвычайно высока на современном оборудовании, потому что это" вычисленный goto ", который перескакивает программу противостоять непредсказуемому местоположению и, следовательно, подрывает логику прогнозирования ветвей и почти наверняка приведет к покраснению и перезагрузке всего контура ЦП".

Простыми словами, и, как предложил Дон Симе (посмотреть на нижние 3 ответа), "будьте откровенны в отношении использования структурного хеширования при использовании ссылочного типа ключи в сочетании с коллекциями .NET". (Д-р Харроп в комментариях ниже также говорит, что мы всегда должны использовать Структурные сравнения при использовании коллекций .NET).

Уважаемая команда F # в MS, если есть способ автоматически исправить это, пожалуйста.

Ответ 2

Как отметил Джон Харроп, просто построение словарей с использованием Dictionary(HashIdentity.Structural) дает значительное улучшение производительности (фактор 3 на моем компьютере). Это почти наверняка минимально инвазивное изменение, которое нужно сделать для повышения производительности, чем Python, и сохраняет ваш код идиоматическим (в отличие от замены кортежей с помощью структур и т.д.) И параллельным реализации Python.

Ответ 3

Изменить: Я ошибался, это не вопрос типа значения vs reference type. Проблема производительности связана с хэш-функцией, как объясняется в других комментариях. Я держу свой ответ здесь, потому что есть интересная дискуссия. Мой код частично исправил проблему с производительностью, но это не чистое и рекомендуемое решение.

-

На моем компьютере я сделал ваш образец дважды в два раза быстрее, заменив кортеж на структуру. Это означает, что эквивалентный код F # должен работать быстрее, чем ваш код Python. Я не согласен с комментариями о том, что .NET hashtables медленны, я считаю, что нет существенной разницы с реализацией Python или других языков. Кроме того, я не согласен с "Вы не можете перевести код 1-в-1, ожидайте, что он будет быстрее": F # -код обычно будет быстрее, чем Python для большинства задач (статическая типизация очень полезна для компилятора). В вашем примере большую часть времени тратится на поиск хеш-таблиц, поэтому можно предположить, что оба языка должны быть почти такими же быстрыми.

Я думаю, что проблема с производительностью связана с сборкой gabage (но я не проверял с помощью профилировщика). Причина, по которой использование кортежей может быть медленнее здесь, чем структуры обсуждались в вопросе SO (Почему новый тип Tuple в .NET 4.0 ссылочный тип (класс), а не тип значения (struct)) и страницу MSDN (Построение кортежей):

Если они являются ссылочными типами, это означает, что может быть много мусора генерируется, если вы меняете элементы в кортеже в плотной петле. [...] Корзины F # были ссылочными типами, но у команды было чувство, что они могли бы реализовать производительность улучшение, если два и, возможно, три, Элементные кортежи были типами значений вместо. Некоторые команды, которые создали вместо этого внутренние кортежи использовали значение ссылочных типов, поскольку их сценарии были очень чувствительны к создавая множество управляемых объектов.

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

Чтобы воспроизвести мой 50% -ный ускорение, вот полный код: http://pastebin.com/nbYrEi5d

Короче говоря, я заменил кортеж таким типом:

type Tup = {x: int; y: int}

Кроме того, это похоже на деталь, но вы должны переместить List.mapi (fun i x -> (i,x)) fileSizes из замкнутого цикла. Я считаю, что Python enumerate фактически не выделяет список (поэтому справедливо распределять список только один раз в F # или использовать модуль Seq или использовать изменяемый счетчик).