Haskell: Data.HashSet (из неупорядоченного контейнера) Производительность для больших наборов

Данные

Прежде всего, давайте сгенерируем некоторые данные, чтобы у нас были конкретные данные для обсуждения:

python -c 'for f in xrange(4000000):  print f' > input.txt

это сгенерирует файл input.txt содержит числа от 0 до 3999999, каждая на своей строке. Это означает, что у нас должен быть файл с 4 000 000 строк, добавляющий до 30 888 890 байт, что составляет примерно 29 МБ.

Все как список

Хорошо, давайте загрузим все в память как [Text]:

import Data.Conduit
import Data.Text (Text)
import Control.Monad.Trans.Resource (runResourceT)
import qualified Data.Conduit.Binary as CB
import qualified Data.Conduit.Text as CT
import qualified Data.Conduit.List as CL

main :: IO ()
main = do
hs <- (runResourceT
          $ CB.sourceFile "input.txt"
         $$ CT.decode CT.utf8
         =$ CT.lines
         =$ CL.fold (\b a -> a `seq` b `seq` a:b) [])
print $ head hs

и запустить его:

[1 of 1] Compiling Main             ( Test.hs, Test.o )
Linking Test ...
"3999999"
2,425,996,328 bytes allocated in the heap
972,945,088 bytes copied during GC
280,665,656 bytes maximum residency (13 sample(s))
5,120,528 bytes maximum slop
533 MB total memory in use (0 MB lost due to fragmentation)

Tot time (elapsed)  Avg pause  Max pause
Gen  0      4378 colls,     0 par    0.296s   0.309s     0.0001s    0.0009s
Gen  1        13 colls,     0 par    0.452s   0.661s     0.0508s    0.2511s

INIT    time    0.000s  (  0.000s elapsed)
MUT     time    0.460s  (  0.465s elapsed)
GC      time    0.748s  (  0.970s elapsed)
EXIT    time    0.002s  (  0.034s elapsed)
Total   time    1.212s  (  1.469s elapsed)

%GC     time      61.7%  (66.0% elapsed)

Alloc rate    5,271,326,694 bytes per MUT second

Productivity  38.3% of total user, 31.6% of total elapsed


real    0m1.481s
user    0m1.212s
sys 0m0.232s

работает в 1.4s, занимает 533 МБ памяти. По состоянию памяти Haskell Wiki, 4M Text экземпляры должны занимать 6 слов + 2N байтов памяти. Я на 64 бит, поэтому одно слово составляет 8 байтов. Это означает, что оно должно быть (6 * 8 байт * 4000000) + (2*26888890) байт = 234 МБ. (26888890 - все байты в input.txt это не символы новой строки). Для списка, который будет содержать их все, нам понадобится дополнительная память (1 + 3N) слов + N * sizeof(v). sizeof(v) должно быть 8, потому что это будет указатель на Text, Список должен затем использовать (1 + 3 * 4000000) * 8 байт + 4000000 * 8 байт = 122 МБ. Таким образом, всего (список + строки) мы ожидаем, что будет использовано 356 МБ памяти. Я не знаю, куда ушла разница в 177 МБ (50%) нашей памяти, но давайте пока проигнорируем это.

Большой хэш-набор

Наконец, мы перейдем к варианту использования, который мне действительно интересен: хранение всех слов в большом Data.HashSet, Для этого я немного изменил программу

import Data.Conduit
import Data.Text (Text)
import Control.Monad.Trans.Resource (runResourceT)
import qualified Data.Conduit.Binary as CB
import qualified Data.Conduit.Text as CT
import qualified Data.Conduit.List as CL
import qualified Data.HashSet as HS

main :: IO ()
main = do
hs <- (runResourceT
          $ CB.sourceFile "input.txt"
         $$ CT.decode CT.utf8
         =$ CT.lines
         =$ CL.fold (\b a -> a `seq` b `seq` HS.insert a b) HS.empty)
print $ HS.size hs

если мы запустим это снова

$ ghc -fforce-recomp -O3 -rtsopts Test.hs && time ./Test +RTS -sstderr
[1 of 1] Compiling Main             ( Test.hs, Test.o )
Linking Test ...
4000000
6,544,900,208 bytes allocated in the heap
6,314,477,464 bytes copied during GC
442,295,792 bytes maximum residency (26 sample(s))
8,868,304 bytes maximum slop
1094 MB total memory in use (0 MB lost due to fragmentation)

Tot time (elapsed)  Avg pause  Max pause
Gen  0     12420 colls,     0 par    5.756s   5.869s     0.0005s    0.0034s
Gen  1        26 colls,     0 par    3.068s   3.633s     0.1397s    0.6409s

INIT    time    0.000s  (  0.000s elapsed)
MUT     time    3.567s  (  3.592s elapsed)
GC      time    8.823s  (  9.502s elapsed)
EXIT    time    0.008s  (  0.097s elapsed)
Total   time   12.399s  ( 13.192s elapsed)

%GC     time      71.2%  (72.0% elapsed)

Alloc rate    1,835,018,578 bytes per MUT second

Productivity  28.8% of total user, 27.1% of total elapsed


real    0m13.208s
user    0m12.399s
sys 0m0.646s

это довольно плохо: используется 13 с и 1094 МБ памяти. Страница памяти занимает список из 4,5N слов + N * sizeof (v) для хэш-набора, который должен стать (4,5 * 4000000 * 8 байтов) + (4000000 * 8 байтов) = 167 МБ. Добавляя хранилище для укусов (234 МиБ), я бы ожидал 401 МиБ, что более чем вдвое, и вдобавок это выглядит довольно медленно:(.

Мысленный эксперимент: ручное управление памятью

В качестве мысленного эксперимента: используя язык, на котором мы можем вручную управлять макетом памяти и реализовывать HashSet с открытой адресацией, я бы ожидал, что следующие размеры будут такими. Справедливости ради, я ожидаю, что строки все еще будут в UTF-16 (что Data.Text делает). Учитывая, что всего 26888890 символов (без перевода строки), строки в UTF-16 должны быть примерно 53777780 байт (2 * 26888890) = 51 МБ. Нам нужно будет хранить длину для каждой строки, которая будет 8 байтов * 4000000 = 30 МБ. И нам понадобится место для хэш-набора (4000000 * 8 байт), опять же 30 МБ. Учитывая, что хэш-наборы обычно увеличиваются экспоненциально, можно ожидать 32 МБ или 64 МБ в худшем случае. Давайте рассмотрим наихудший случай: 64 МБ для таблицы + 30 МБ для длин строки + 51 МБ для фактических данных строки, всего 145 МБ.

Итак, учитывая, что Data.HashSet не является специализированной реализацией для хранения строк, вычисленные 401 МиБ не будут слишком плохими, но фактически используемые 1094 МиБ кажутся немного бесполезными.

Вопросы наконец-то:)

И вот мы наконец добрались

  • Где ошибка в моих расчетах?
  • Есть ли какая-то проблема в моей реализации, или 1094MiB действительно лучшее, что мы можем получить?

Версии и прочее

  • Я, наверное, должен использовать ByteString с вместо Text так как мне нужны только символы ascii
  • Я нахожусь на GHC 7.10.1 и unordered-containers-0.2.5.1

Для сравнения: 4 000 000 Int s:

import Data.List (foldl')
import qualified Data.HashSet as HS

main = do
    let hs = foldl' (\b a -> a `seq` b `seq` HS.insert a b) (HS.empty :: HS.HashSet Int) [1..4000000]
    print $ HS.size hs

выглядит не лучше

[...]
798 MB total memory in use (0 MB lost due to fragmentation)
[...]
real    0m9.956s

это почти 800 МБ для 4M Ints!

0 ответов

Другие вопросы по тегам