Подтвердить что ты не робот

Построение гистограммы с помощью haskell, во много раз медленнее, чем с python

Я собирался проверить наивную классификацию заливов. Одна его часть собиралась построить гистограмму данных обучения. Проблема в том, что через пару лет я использую большие данные обучения, список рассылки haskell-cafe, и в папке содержится более 20 тыс. Файлов.

Для создания гистограммы с помощью python требуется более двух минут, а с haskell - чуть более 8 минут. Я использую Data.Map(insertWith '), перечисления и текст. Что еще я могу сделать, чтобы ускорить работу программы?

Haskell:

import qualified Data.Text as T
import qualified Data.Text.IO as TI
import System.Directory
import Control.Applicative
import Control.Monad (filterM, foldM)
import System.FilePath.Posix ((</>))
import qualified Data.Map as M
import Data.Map (Map)
import Data.List (foldl')
import Control.Exception.Base (bracket)
import System.IO (Handle, openFile, hClose, hSetEncoding, IOMode(ReadMode), latin1)
import qualified Data.Enumerator as E
import Data.Enumerator (($$), (>==>), (<==<), (==<<), (>>==), ($=), (=$))
import qualified Data.Enumerator.List as EL
import qualified Data.Enumerator.Text as ET



withFile' ::  (Handle -> IO c) -> FilePath -> IO c
withFile' f fp = do
  bracket
    (do
      h ← openFile fp ReadMode
      hSetEncoding h latin1
      return h)
    hClose
    (f)

buildClassHistogram c = do
  files ← filterM doesFileExist =<< map (c </> ) <$> getDirectoryContents c
  foldM fileHistogram M.empty files

fileHistogram m file = withFile' (λh → E.run_ $ enumHist h) file
  where
    enumHist h = ET.enumHandle h $$ EL.fold (λm' l → foldl' (λm'' w → M.insertWith' (const (+1)) w 1 m'') m' $ T.words l) m

Python:

for filename in listdir(root):
    filepath = root + "/" + filename
    # print(filepath)
    fp = open(filepath, "r", encoding="latin-1")
    for word in fp.read().split():
        if word in histogram:
            histogram[word] = histogram[word]+1
        else:
            histogram[word] = 1

Изменить: добавлены импорт

4b9b3361

Ответ 1

Вы можете попробовать использовать императивные хэш-карты из пакета hashtables: http://hackage.haskell.org/package/hashtables Я помню, что когда-то я получил умеренное ускорение по сравнению с Data.Map. Я бы не ожидал ничего впечатляющего.

UPDATE

Я упростил ваш код на Python, чтобы проверить его на одном большом файле (100 миллионов строк):

import sys
histogram={}
for word in sys.stdin.readlines():
    if word in histogram:
        histogram[word] = histogram[word]+1
    else:
        histogram[word] = 1
print histogram.get("the")

Принимает 6,06 секунды

Перевод Haskell с использованием hashtables:

{-# LANGUAGE OverloadedStrings #-}
import qualified Data.ByteString.Char8 as T
import  qualified Data.HashTable.IO as HT
main = do
  ls <- T.lines `fmap` T.getContents
  h <- HT.new :: IO (HT.BasicHashTable T.ByteString Int)
  flip mapM_ ls $ \w -> do
    r <- HT.lookup h w 
    case r of 
      Nothing -> HT.insert h w (1::Int)
      Just c  -> HT.insert h w (c+1)
  HT.lookup h "the" >>= print 

Запуск с большой областью выделения: histogram +RTS -A500M Принимает 9,3 секунды, с 2,4% GC. Все еще довольно медленнее, чем Python, но не так уж плохо.

В соответствии с Руководство пользователя GHC вы можете изменить параметры RTS при компиляции:

GHC позволяет вам изменять стандартные параметры RTS для программы при компиляции времени, используя флаг -with-rtsopts (раздел 4.12.6, "Опции, влияющие linking" ). Общим для этого является предоставление вашей программы по умолчанию кучи и/или размер стека, который больше, чем значение по умолчанию. Например, установить -H128m -K64m, связать с -with-rtsopts = "- H128m -K64m".

Ответ 2

В реализациях Haskell и Python используются карты с различными сложностями. Словари Python - это хеш-карты, поэтому ожидаемое время для каждой операции (тест на членство, поиск и вставка) - это O (1). В версии Haskell используется Data.Map, которая представляет собой сбалансированное двоичное дерево поиска, поэтому одни и те же операции принимают O (lg n). Если вы измените версию Haskell, чтобы использовать другую реализацию карты, скажем, хеш-таблицу или какой-то трюк, она должна стать намного быстрее. Тем не менее, я недостаточно знаком с различными модулями, реализующими эти структуры данных, чтобы сказать, что лучше всего. Я бы начал с категории данных в Hackage и поискал тот, который вам нравится. Вы также можете найти карту, которая позволяет деструктивные обновления, такие как STArray.

Ответ 3

Нам нужна дополнительная информация:

  • Сколько времени требуется, чтобы обе программы обрабатывали слова из ввода, без структуры данных для поддержания подсчетов?

  • Сколько различных слов есть, поэтому мы можем судить о том, является ли дополнительная стоимость log N для сбалансированных деревьев соображением?

  • Что говорит профайлер GHC? В частности, сколько времени потрачено на выделение? Возможно, что версия Haskell проводит большую часть времени, выделяя узлы дерева, которые быстро устаревают.

  • ОБНОВЛЕНИЕ. Я пропустил этот строчный "текст", который может означать Data.Text. Вы можете сравнивать и апельсины. Кодировка Python Latin1 использует один байт за char. Хотя он пытается быть эффективным, Data.Text должен допускать возможность использования более 256 символов. Что произойдет, если вы перейдете на String или лучше, Data.ByteString?

В зависимости от того, что говорят эти индикаторы, вот несколько вещей, которые можно попробовать:

  • Если анализ ввода является узким местом, попробуйте управлять всеми вашими вводами и анализами из Data.ByteString вместо Text.

  • Если структура данных является узким местом, Bentley и Sedgewick тройные деревья поиска являются чисто функциональными, но конкурируют с хеш-таблицами. В Hackage есть пакет TernaryTrees.