FSharp работает мой алгоритм медленнее, чем Python - PullRequest
40 голосов
/ 01 мая 2011

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

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, запускает МЕДЛЕННО, чем интерпретируемый код Python?!?!

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

Возможно, я сделал что-то не так?

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

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

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

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

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

Код Питона: https://gist.github.com/950697

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

Ответы [ 3 ]

47 голосов
/ 01 мая 2011

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

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

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

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

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

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

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

Спасибо,Джон - высоко ценится.

РЕДАКТИРОВАТЬ : тот факт, что замена Dictionary на Array делает F #, наконец, работающим на скоростях, ожидаемых для запуска скомпилированного языка, не отменяет необходимостиисправить в скорости словаря (я надеюсь, что F # люди из MS читают это).Другие алгоритмы зависят от словарей / хэшей и не могут быть легко переключены на использование массивов;заставлять программы страдать от «скоростей интерпретатора» всякий раз, когда кто-то использует словарь, возможно, является ошибкой.Если, как некоторые говорили в комментариях, проблема не в F #, а в словаре .NET, я бы сказал, что это ... ошибка в .NET!

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

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

на следующее:

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

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

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

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

8 голосов
/ 02 мая 2011

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

5 голосов
/ 01 мая 2011

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

-

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

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

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

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

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...