Любопытно о проблемах производительности HashTable - PullRequest
50 голосов
/ 17 июня 2010

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

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

Ответы [ 3 ]

135 голосов
/ 17 июня 2010

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

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

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

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

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 , который поддерживает диапазон изменяемых хеш-таблиц .

26 голосов
/ 17 июня 2010

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

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

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


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


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

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

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

6 голосов
/ 19 июня 2010

Короче говоря, даже с исправлением в последнем GHC, Haskell по-прежнему не способен обеспечить словарь (изменяемый или неизменяемый), который был бы конкурентоспособен.

Хеш-таблицы Haskell были 32 × медленнее, чемальтернативы, такие как C ++ и .NET с GHC 6.10.Отчасти это было связано с ошибкой производительности в сборщике мусора GHC, которая была исправлена ​​для GHC 6.12.2 .Но результаты Саймона Марлоу там показывают только пятикратное улучшение производительности, которое все еще оставляет хэш-таблицы на Haskell во много раз медленнее, чем большинство альтернатив.

Чисто функциональные альтернативы также намного медленнее, чем приличная хеш-таблица.Например, IntMap на Haskell в 10 раз медленнее, чем хеш-таблица .NET .

с использованием F # 2010 и последней платформой Haskell 2010.2.0.0 (выпущенной вчера!) с GHC 6.12.3 на этой 2 ГГц E5405 Xeon под управлением 32-разрядной Windows Vista для вставки привязок 20M int-> int в пустую хеш-таблицу мы обнаружим, что Haskell все еще в 29 раз медленнее, чем F # в реальном времени, и более чем в 200 раз медленнеес точки зрения процессорного времени, поскольку Haskell сжигает все ядра:

GHC 6.12.3 Data.HashTable: 42.8s (new!)
.NET hash table:            1.47s

При условии, что вы запускаете только недолговечные микробенчмарки, вы можете отключить сборщик мусора GHC, как предлагает Дон Стюарт выше.Задавая вопрос о таком большом поколении питомников, что эта конкретная программа никогда не заполнит его, он сократил время для хэш-таблицы на Haskell до 1,5 с.Однако это полностью подрывает весь смысл создания детского поколения и значительно ухудшит производительность другого кода, поскольку недавно выделенные значения теперь всегда будут холодными в кэше (именно поэтому генерация детского ресурса обычно равна размеру кэша L2,на порядки меньше этого).

...