【发布时间】:2022-01-19 02:33:34
【问题描述】:
我碰巧按自然频率对德语单词列表进行了排序1。我对我的算法的内存性能不满意。
图形是用hp/D3.js 创建的。它显示了 V1、V2 和 V3 的运行时堆,如下面的代码所示。
我上传了完整的代码,包括关于如何使用性能分析(通过堆栈和 nix)on github here 运行的简短说明。它也完整地粘贴在下面。
版本 1 使用来自 Data.Text.IO 的严格 IO 读取两个大文件。从Data.Text.Lazy.IO 可以很好地看出带有 Lazy IO 的版本 2 和 3 有什么不同:有些东西会立即出现,而版本 2 和 3 会建立。
数据结构的大小
我可以根据these formulae 给出相当准确的尺寸,而且我知道文件中的内容,那里的德语单词的平均长度约为 16 个字符。这些数字不是从输出中解释的,而是独立计算的。
- mapFrequencies:550 MB (
HashMap Text Int) - ls:200 MB (
[Text]) - vec:167 MB (
Vector Text)
我不明白的地方
除此之外,我完全迷失了。我正在尝试理解这些问题:
-
为什么我的
{-# SCC foo #-}被忽略了?我无法控制分析中的成本中心。这发生在 GHC 8.8.4 和 GHC 9.2.1、nix/cabal 和 stack 上。 -
配置文件建议内存使用量峰值略高于 1 GB。但是,运行
top,我可以看到该算法确实使用了 2.6 GB。这几乎是金额的两倍。这些金额不应该相等吗? -
垃圾收集发生在哪里?我的怀疑是没有。第 2 版和第 3 版显示了一些 gargabe 收集,但只是内存使用量超过了第 1 版。
-
考虑到我选择的 hashmap、list 和 vector,我是否可以期待更精简的内存配置文件?仅添加 hashmap 和向量将达到 717 MB,不到我在
top中看到的一半。怎么去? -
还有其他更适合此类任务的设备吗?我为排序算法选择了向量。由于
Text,我无法移动到Storable、Unboxed或Primitive中的任何一个(至少我不知道如何)。 -
在运行时统计信息的摘要中(如下所示),它显示“生产力 43.5%”。我的猜测是,分析本身就是原因的一部分。但是,根据数字,是否也存在垃圾收集器的过度活动?
-- app/Main.hs
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Control.Category ((<<<))
import Control.Monad.ST (runST)
import Data.Functor ((<&>))
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
import Data.Maybe (catMaybes, fromMaybe)
import Data.Ord (Down (Down), comparing)
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.IO as Text
import qualified Data.Text.Lazy as Lazy
import qualified Data.Text.Lazy.IO as Lazy
import Data.Vector (Vector, freeze, thaw)
import qualified Data.Vector as Vector
import qualified Data.Vector.Algorithms.Tim as Tim
import System.IO (hFlush, stdout)
import GHC.Conc (pseq)
main :: IO ()
main = do
putStr ""
putStr "Running v1 ..."
hFlush stdout
u1 <- runV1
putStrLn $ u1 `seq` " done."
putStrLn ""
putStr "Running v2 ..."
hFlush stdout
u2 <- runV2
putStrLn $ u2 `seq` " done."
putStrLn ""
putStr "Running v3 ..."
hFlush stdout
u3 <- runV3
putStrLn $ u3 `seq` " done."
fileFrequencies :: FilePath
fileFrequencies = "deu_news_2020_freq.txt"
fileData :: FilePath
fileData = "german.utf8.dic"
fileSorted :: FilePath
fileSorted = "german.utf8.sorted.dic"
{- |
straightforward implementation, using Text-based IO
-}
runV1 :: IO ()
runV1 = do
mapFrequencies <- readFrequencies
ls <- Text.lines <$> Text.readFile fileData
let sorted = quicksort mapFrequencies $ {-# SCC vec #-} Vector.fromList ({-# SCC ls #-} ls)
Text.writeFile fileSorted $ Text.unlines $ {-# SCC lsSorted #-} Vector.toList ({-# SCC sorted #-} sorted)
where
{-# SCC readFrequencies #-}
readFrequencies :: IO (HashMap Text Int)
readFrequencies = do
ls <- Text.lines <$> Text.readFile fileFrequencies
pure $ {-# SCC hmap #-} mkHashMap ({-# SCC ls #-} ls)
{- |
why not Lazy? read the file line by line, no need to hold it all in memory
-}
runV2 :: IO ()
runV2 = do
mapFrequencies <- readFrequencies
ls <- fmap Lazy.toStrict . Lazy.lines <$> Lazy.readFile fileData
let sorted = quicksort mapFrequencies $ {-# SCC vec #-} Vector.fromList ({-# SCC ls #-} ls)
Text.writeFile fileSorted $ Text.unlines $ {-# sCC lsSorted #-} Vector.toList ({-# SCC sorted #-} sorted)
where
{-# SCC readFrequencies #-}
readFrequencies :: IO (HashMap Text Int)
readFrequencies = do
ls <- fmap Lazy.toStrict . Lazy.lines <$> Lazy.readFile fileFrequencies
pure $ {-# SCC hmap #-} mkHashMap ({-# SCC ls #-} ls)
{-|
trying to help with garbage collection, only making it worse
-}
runV3 :: IO ()
runV3 = do
mapFrequencies <- readFrequencies
ls <- fmap Lazy.toStrict . Lazy.lines <$> Lazy.readFile fileData
let -- alternatives:
-- Vector.fromListN (length ls) ls
-- Vector.generate (length ls) $ \i -> ls !! i
vec = {-# SCC vec #-} Vector.fromList ({-# SCC ls #-} ls)
-- the idea: ls can get garbage-collected ...
sorted = vec `seq` {-# SCC sorted #-} quicksort mapFrequencies vec
-- ... before we sort and write to the file
sorted `pseq` Lazy.writeFile fileSorted (Lazy.unlines $ Lazy.fromStrict <$> {-# SCC lsSorted #-} Vector.toList sorted)
where
readFrequencies :: IO (HashMap Text Int)
readFrequencies = do
ls <- fmap Lazy.toStrict . Lazy.lines <$> Lazy.readFile fileFrequencies
pure $ {-# SCC hmap #-} mkHashMap ({-# SCC ls #-} ls)
freq :: HashMap Text Int -> Text -> Int
freq m w = fromMaybe 0 $ HashMap.lookup w m
quicksort ::
HashMap Text Int -> Vector Text -> Vector Text
quicksort freqs vec = runST $ do
mvec <- thaw vec
Tim.sortBy (comparing $ Down <<< freq freqs) mvec
freeze mvec
mkHashMap :: [Text] -> HashMap Text Int
mkHashMap ls =
HashMap.fromList $
catMaybes $
ls <&> \l -> case Text.head l of
'#' -> Nothing
_ ->
let [w, f] = Text.splitOn "\t" l
in Just (w, read $ Text.unpack f)
运行时统计摘要(+RTS -s)
343,377,611,904 bytes allocated in the heap
1,345,257,485,736 bytes copied during GC
1,489,914,240 bytes maximum residency (1608 sample(s))
203,039,648 bytes maximum slop
2829 MiB total memory in use (0 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 328286 colls, 0 par 12.067s 12.117s 0.0000s 0.0114s
Gen 1 1608 colls, 0 par 1001.504s 1001.547s 0.6229s 1.0471s
INIT time 0.000s ( 0.000s elapsed)
MUT time 160.134s (160.481s elapsed)
GC time 663.692s (663.771s elapsed)
RP time 0.000s ( 0.000s elapsed)
PROF time 349.879s (349.893s elapsed)
EXIT time 0.000s ( 0.000s elapsed)
Total time 1173.705s (1174.145s elapsed)
%GC time 0.0% (0.0% elapsed)
Alloc rate 2,144,311,061 bytes per MUT second
Productivity 43.5% of total user, 43.5% of total elapsed
1词频信息由 Uni Leipzig 自然语言处理小组提供给我。它是从 3500 万个句子的语料库中生成的,分布在 Creative Commons Attribution-NonCommercial 4.0 International Public Licence 下。
【问题讨论】:
-
imgur.com/a/Rq35gug 按类型划分的堆配置文件。元组中有相当多的内存,仅在 V2 和 V3 中
标签: performance haskell io profiling