Любопытно узнать о проблемах с производительностью HashTable

Я читал, что хеш-таблицы в Haskell имели проблемы с производительностью (в Haskell-Cafe в 2006 г. и Flying Frog Consultancy blog в 2009 году), и поскольку мне нравится Haskell, это меня беспокоило.

Это было год назад, как обстоят дела сейчас (июнь 2010)? Исправлена ​​ли «проблема с хэш-таблицей» в GHC?


person Alessandro Stamatto    schedule 17.06.2010    source источник
comment
Хороший вопрос +1 за то, что подумал спросить.   -  person Robert Massaioli    schedule 17.06.2010
comment
Мой первоначальный эксперимент доступен здесь, и вы можете легко воспроизвести результаты для себя (вы обнаружите, что GHC 6.12 лучше, но все еще не может выразить какое-либо решение, близкое к производительности обычной хэш-таблицы, например, в C++ или .NET) : flyingfrogblog.blogspot.com/2009 /04/   -  person J D    schedule 18.06.2010
comment
@Jon Смотрите мой комментарий в сообщении Дона ниже. Версия C++, использующая класс Boost unordered_map, была лишь примерно на 15% быстрее, чем версия с хеш-таблицей Haskell (использующая большие настройки кучи по умолчанию) в моей системе.   -  person Brad Larsen    schedule 19.06.2010
comment
@BradfordLarsen Я снова проверил это в июле 2010 года и обнаружил, что F # по-прежнему до 26 раз быстрее, чем Haskell. flyingfrogblog.blogspot.co.uk/ 2010/07/   -  person J D    schedule 08.04.2012


Ответы (2)


Проблема заключалась в том, что сборщику мусора необходимо обходить изменяемые массивы указателей («массивы в коробках») в поисках указателей на данные, которые могут быть готовы к освобождению. Коробочные, изменяемые массивы являются основным механизмом реализации хеш-таблицы, поэтому эта конкретная структура выявила проблему обхода сборщика мусора. Это свойственно многим языкам. Симптом — чрезмерная сборка мусора (до 95% времени, затрачиваемого на сборку мусора).

Исправление состояло в том, чтобы внедрить "маркировку карт" в GC для изменяемые массивы указателей, что произошло в конце 2009 года. Вы не должны видеть чрезмерного GC при использовании изменяемых массивов указателей в Haskell сейчас. В простых тестах вставка хеш-таблиц для больших хэшей улучшилась в 10 раз.

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

Если вы используете хэш-таблицы в Haskell, сейчас вы не должны наблюдать никаких проблем. Вот, например, простая программа для создания хеш-таблиц, которая вставляет в хеш 10 миллионов целых чисел. Я проведу бенчмаркинг, поскольку исходная цитата не содержит кода или бенчмарков.

import Control.Monad
import qualified Data.HashTable as H
import System.Environment

main = do
  [size] <- fmap (fmap read) getArgs
  m <- H.new (==) H.hashInt
  forM_ [1..size] $ \n -> H.insert m n n
  v <- H.lookup m 100
  print v

С GHC 6.10.2, до исправления, вставка 10M целых чисел:

$ time ./A 10000000 +RTS -s
...
47s.

В GHC 6.13 после исправления:

./A 10000000 +RTS -s 
...
8s

Увеличение области кучи по умолчанию:

./A +RTS -s -A2G
...
2.3s

Избегайте хеш-таблиц и используйте IntMap:

import Control.Monad
import Data.List
import qualified Data.IntMap as I
import System.Environment

main = do
  [size] <- fmap (fmap read) getArgs
  let k = foldl' (\m n -> I.insert n n m) I.empty [1..size]
  print $ I.lookup 100 k

И мы получаем:

$ time ./A 10000000 +RTS -s        
./A 10000000 +RTS -s
6s

Или, альтернативно, используя массив judy (который представляет собой оболочку Haskell, вызывающую код C через интерфейс внешней функции):

import Control.Monad
import Data.List
import System.Environment
import qualified Data.Judy as J

main = do
  [size] <- fmap (fmap read) getArgs
  j <- J.new :: IO (J.JudyL Int)
  forM_ [1..size] $ \n -> J.insert (fromIntegral n) n j
  print =<< J.lookup 100 j

Запустив это,

$ time ./A 10000000 +RTS -s
...
2.1s

Итак, как видите, проблема GC с хеш-таблицами исправлена, и всегда были другие библиотеки и структуры данных, которые идеально подходили. В общем, это не проблема.

Примечание. Начиная с 2013 г. вам, вероятно, следует просто использовать пакет hashtables, который поддерживает диапазон изменяемых хеш-таблиц изначально.

person Don Stewart    schedule 17.06.2010
comment
Было написано, что он будет объединен с 6.12.2. - person Maciej Piechotka; 17.06.2010
comment
Хорошая работа, как обычно, и полный охват (обозначил проблему и показал на примере, почему это больше не проблема). +1 - person Robert Massaioli; 17.06.2010
comment
Вау, идеальный ответ. Сообщество Stack Overflow отличное! Я пока не могу голосовать (недостаточно репутации), иначе я бы обязательно проголосовал! Большое спасибо за ответ, это было потрясающе! - person Alessandro Stamatto; 17.06.2010
comment
Как всегда, прекрасная демонстрация уникального, передового анализа и оптимизации GHC, . - person C. A. McCann; 17.06.2010
comment
Как оказалось, Дон Стюарт также является прекрасным двигателем доказательства, хотя и несколько косвенно! - person Norman Ramsey; 18.06.2010
comment
Дон, тебе нужна более быстрая машина! - person Norman Ramsey; 18.06.2010
comment
Если IntMap быстрее, чем хеш-таблица, зачем вообще использовать хеш-таблицу? Хэш-таблица полезна только тогда, когда она O (1). - person Gabe; 18.06.2010
comment
Различные характеристики масштабирования по сравнению с обобщенными попытками. - person Don Stewart; 19.06.2010
comment
Из любопытства я запустил эти микротесты на своей собственной системе и набросал версию на C++, использующую хеш-таблицу Boost. По сравнению с версией хеш-таблицы Haskell, работающей с большей кучей, время ее обработки было ненамного быстрее: C++ 1,613 с, Haskell 1,862 с. Я удивлен! - person Brad Larsen; 19.06.2010
comment
Твои данные устарели, Джон. Вот о чем этот пост. - person Don Stewart; 19.06.2010
comment
Возможно, вы, ребята, должны запустить этот тест Haskell и эквивалентный F # на одной машине, чтобы выяснить это. - person Jules; 20.06.2010
comment
Какие программы и параметры компилятора вы используете? - person Jules; 24.06.2010
comment
Тесты показывают, что Haskell медленнее; это кажется бесспорным. Реальный вопрос: имеет ли значение более низкая производительность хеш-таблиц в Haskell для вашей конкретной задачи? Если нет, смело выбирайте Haskell, а если да, то выбирайте что-нибудь другое. Производительность — это только одна характеристика языковой реализации. Насколько это важно, полностью зависит от того, чего вы пытаетесь достичь с его помощью. - person Duncan Bayne; 07.04.2011

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

Джон Харроп выдвинул несколько интересных утверждений о Haskell. Позвольте мне предложить вам поискать в Google Groups и других источниках доказательства опыта Харропа в Haskell, Lisp и других функциональных языках. Вы также можете прочитать работу Криса Окасаки и Энди Гилла о деревьях Патрисии в Haskell, чтобы узнать, как оценивается их опыт. Вы также можете узнать, чьи претензии, если таковые имеются, были проверены третьей стороной. Затем вы сможете сами решить, насколько серьезно относиться к утверждениям разных людей о производительности различных функциональных языков.

Ой, и не кормите тролля.


P.S. Для вас было бы вполне разумно провести свои собственные эксперименты, но, возможно, в этом нет необходимости, поскольку верный Дон Стюарт в своем прекрасном ответе представляет несколько хороших микротестов. Вот дополнение к ответу Дона:


Приложение: использование кода Дона Стюарта на процессоре AMD Phenom 9850 Black Edition с тактовой частотой 2,5 ГГц и 4 ГБ ОЗУ в 32-разрядном режиме с ghc -O,

  • С кучей по умолчанию IntMap на 40% быстрее, чем хэш-таблица.
  • С кучей 2G хэш-таблица на 40% быстрее, чем IntMap.
  • Если я перейду к десяти миллионам элементов с кучей по умолчанию, IntMap в четыре раза быстрее, чем хеш-таблица (время процессора), или в два раза быстрее по времени настенных часов.

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

person Norman Ramsey    schedule 17.06.2010
comment
+1 за предложение немного больше исследований. В таком сообществе, как Haskell, основанном на исследованиях (например, GHC), это правильный путь. - person Robert Massaioli; 17.06.2010
comment
Почему вы используете хеш-таблицы в Haskell? Цель состоит в том, чтобы Haskell был чисто функциональным языком с хорошим синтаксисом, гибкостью и производительностью. Если вы ищете высокопроизводительную изменяемую структуру данных (т. е. не чисто функциональную), то Haskell может вам подойти, но, возможно, для вашей проблемы есть лучшие решения, поскольку Haskell не специализируется на решении этой проблемы. - person yfeldblum; 19.06.2010
comment
@Justice: я не хочу использовать хеш-таблицы в Haskell. Меня вполне устраивают стандартные деревья Патрисии и другие функциональные структуры. Вся эта ветка началась потому, что Джон Харроп, похоже, ведет какую-то частную войну против Хаскелла (возможно, поэтому этот вопрос сейчас заблокирован). Одна из тактик Харропа состояла в том, чтобы сказать, что хеш-таблицы в Haskell ужасны, чисто функциональные структуры ужасны и т. д. и т. д. Многие из нас не доверяют качеству его информации и не хотят, чтобы люди принимали его за чистую монету. Вот о чем этот вопрос. - person Norman Ramsey; 20.06.2010