Как заполнить Data.Map в пространстве и времени эффективным способом
Возвращение в Хаскелл через четыре года после первого взгляда на него. Меня всегда так поражает выразительность, и я сбился с толку из-за моей неспособности предсказать производительность пространства/времени.
Как разогрев, я взял для перевода крошечной игрушечной программы, написанной на С++. Это о "обмане" в Scrabble. Вы вводите свою игру, и она выводит возможные слова, которые вы можете играть, с вашими письмами в одиночку или путем скрещивания буквы на доске.
Все это вращается вокруг словаря, который предварительно загружается при запуске. Затем слова сохраняются в виде списков на карте вместе с их анаграммами. Ключами являются строки отсортированных букв. Пример будет говорить более четко:
Key : "AEHPS" Value : ["HEAPS","PHASE","SHAPE"]
Версия С++ читает ~ 320000 слов словаря по одному, всего около 200 мс. Результирующая структура данных представляет собой хеш-карту, хранящуюся в array<99991, vector<string>>
, и занимает около 12 мегабайт памяти.
Версия Haskell считывает один и тот же словарь примерно за 5 секунд, а размер кучи программы вздувается до 400 мегабайт! Я изменил тип значения в Data.Map
от [String]
до [ByteString]
, чтобы сохранить некоторую память, и это привело к сокращению потребления памяти программы примерно до 290 мегабайт. Это все еще в 24 раза больше, чем моя версия на С++. Это больше, чем просто "накладные расходы", хотя Data.Map
является деревом вместо массива.
Поэтому я предполагаю, что я делаю что-то неправильно.
Весь модуль отображается здесь: (устаревшая ссылка)
Я полагаю, что моя проблема связана с тем, как Data.Map
создается постепенно, растут на предыдущих версиях самого себя? Или с самой структурой данных? Или что-то еще?
Я попробую другие решения, например Data.HashMap
, или заполнить Data.Map
с помощью fromListWith
. Тем не менее, я хотел бы рассказать о том, что происходит здесь. Спасибо за понимание!
Короткий ответ:
Использование Data.Map.Strict, заставляя оценивать элементы значения и сохраняя ключи как ByteStrings, тоже сделало чудо деления объема памяти почти на 3. Результат равен 100Meg, который только в два раза больше стандартного std::multimap<std::string, std::string>
в С++ для одного и того же набора данных. Однако нет ускорения. Git обновлен.
Спасибо всем, кто внес свой вклад, здесь есть интересные материалы!
Ответы
Ответ 1
Одна ошибка, которую вы делаете, еще не указана, заключается в том, что вы сохраняете неоцененные трюки формы B.pack word
в списках, которые являются значениями на вашей карте. Это означает, что вы сохраняете по существу весь входной файл в неэффективном формате String во время построения вашей карты со стоимостью 24 байта на символ в своем входном файле. Использование API Data.Map.Strict
не имеет никакого значения здесь, поскольку функции в этом API только заставляют элементы карты иметь слабую головную нормальную форму, которая для списка означает только то, является ли внешний конструктор []
или (:)
, и не оценивая ни один из элементов списка.
Еще одно улучшение, которое вы можете сделать, - использовать тип ShortByteString
, доступный в последних версиях bytestring (тот, который поставляется с GHC 7.8, является достаточно новым). Это специально предназначено для минимизации использования памяти при хранении множества коротких байтов, причем компромисс заключается в том, что большинство операций с ShortByteString
требуют копии.
Код примера кода András Kovács будет выглядеть следующим образом:
{-# LANGUAGE BangPatterns #-}
import Control.Applicative
import Data.List
import qualified Data.Map.Strict as M
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Short as B (ShortByteString, toShort, fromShort)
shortPack = B.toShort . B.pack
main = do
words <- lines <$> readFile "dict.txt"
print $ M.size $
M.fromListWith (++) $ map (\w -> let !x = shortPack w in (shortPack $ sort w, [x])) words
Каждое из этих изменений экономит около 30% максимального числа мест в моих тестах, в общей сложности более 50% экономии пространства.
Ответ 2
РЕДАКТИРОВАТЬ: удалены ошибочные бенчмаркинга и комментарии, оставлены лишь незначительные советы. См. Reid Barton ответ для прямого решения вопроса OP.
Если вам не нужно менять словарь во время выполнения, то DAWG-s - это почти самое эффективное в пространстве-времени решение может получить (по крайней мере, для игровых игр).
Например, мы можем генерировать и сериализовать DAWG из вашего словаря, который занимает всего 295 Kb, и поддерживает довольно эффективный поиск и сопоставление префикса:
import qualified Data.DAWG.Packed as D -- from my "packed-dawg" package
main = do
words <- lines <$> readFile "dict.txt"
D.toFile "dict.dawg" $ D.fromList words -- serialize as "dict.dawg"
Ответ 3
На моем ноутбуке работает около секунды:
import qualified Data.Map as M
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Data.List
type Dict = M.Map T.Text [T.Text]
newDict = M.empty
addWord:: T.Text -> Dict -> Dict
addWord word dict = M.insertWith (++) (T.pack $ sort $ T.unpack word) [word] dict
loadAnagramsFromFile fileName = do
full <- T.readFile fileName
let ls = T.lines full
return $ foldr addWord newDict lsct ls
Это используется Text, с бородавкой для сортировки. Может быть лучший способ сортировки текста.