我在Haskell中编写了一个应用程序,它执行以下操作:
这个应用程序的第一个版本是我可以编写的最简单,最天真的版本,但我注意到空间使用似乎单调增加。
结果,我切换到conduit
,现在我的主要功能如下:
conduitFilesFilter :: ProjectFilter -> Path Abs Dir -> IO [Path Abs File]
conduitFilesFilter projFilter dirname' = do
(_, allFiles) <- listDirRecur dirname'
C.runConduit $
C.yieldMany allFiles
.| C.filterMC (filterMatchingFile projFilter)
.| C.sinkList
现在我的应用程序限制了内存使用,但它仍然很慢。出于这个原因,我有两个问题。
1)
我使用stack new
生成骨架来创建此应用程序,默认情况下使用ghc选项-threaded -rtsopts -with-rtsopts=-N
。
令我惊讶的是(对我来说)应用程序使用它可用的所有处理器(目标机器中大约40个),当我实际运行它时。但是,我没有编写要并行运行的应用程序的任何部分(实际上我考虑过它)。
并行运行什么?
2)
此外,大多数JSON文件都非常大(10mb),并且可能有500k个要遍历。这意味着我的程序由于所有Aeson解码而非常慢。我的想法是并行运行我的filterMatchingFile
部分,但是查看stm-conduit
库,我无法看到一种明显的方法可以在少数几个处理器上并行运行这个中间操作。 / p>
有人可以建议使用stm-conduit
或其他方法巧妙地将我的功能并行化吗?
修改
我意识到我可以将readFile -> decodeObject -> runFilterFunction
分解为conduit
的不同部分,然后我可以在stm-conduit
处使用有界通道。也许我会试一试......
我使用+RTS -s
运行了我的应用程序(我将其重新配置为-N4
),我看到以下内容:
115,961,554,600 bytes allocated in the heap
35,870,639,768 bytes copied during GC
56,467,720 bytes maximum residency (681 sample(s))
1,283,008 bytes maximum slop
145 MB total memory in use (0 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 108716 colls, 108716 par 76.915s 20.571s 0.0002s 0.0266s
Gen 1 681 colls, 680 par 0.530s 0.147s 0.0002s 0.0009s
Parallel GC work balance: 14.99% (serial 0%, perfect 100%)
TASKS: 10 (1 bound, 9 peak workers (9 total), using -N4)
SPARKS: 0 (0 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)
INIT time 0.001s ( 0.007s elapsed)
MUT time 34.813s ( 42.938s elapsed)
GC time 77.445s ( 20.718s elapsed)
EXIT time 0.000s ( 0.010s elapsed)
Total time 112.260s ( 63.672s elapsed)
Alloc rate 3,330,960,996 bytes per MUT second
Productivity 31.0% of total user, 67.5% of total elapsed
gc_alloc_block_sync: 188614
whitehole_spin: 0
gen[0].sync: 33
gen[1].sync: 811204
答案 0 :(得分:1)
从您的程序说明中,没有理由增加内存使用量。我认为这是因错过懒惰计算而导致的意外内存泄漏。这可以通过堆分析轻松检测到:https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/profiling.html#hp2ps-rendering-heap-profiles-to-postscript。其他可能的原因是运行时不会将所有内存释放回操作系统。直到某个阈值,它将保持与处理的最大文件成比例的内存。如果通过进程RSS大小进行跟踪,这可能看起来像是内存泄漏。
-A32m
选项会增加托儿所的大小。它允许程序在触发垃圾收集之前分配更多内存。统计数据显示,GC期间保留的内存非常少,因此更少发生,程序花费更多时间进行实际工作。
答案 1 :(得分:0)
Michael Snoyman在Haskell Cafe上提示,他指出我的第一个版本没有真正利用Conduit的流媒体功能,我重新编写了我的应用程序的Conduit版本(不使用stm-conduit)。这是一个很大的改进:我的第一个管道版本是在所有数据上运行的,我没有意识到这一点。
我还增加了托儿所的规模,通过减少垃圾收集来提高我的工作效率。
我修改后的功能最终看起来像这样:
module Search where
import Conduit ((.|))
import qualified Conduit as C
import Control.Monad
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Trans.Resource (MonadResource)
import qualified Data.ByteString as B
import Data.List (isPrefixOf)
import Data.Maybe (fromJust, isJust)
import System.Path.NameManip (guess_dotdot, absolute_path)
import System.FilePath (addTrailingPathSeparator, normalise)
import System.Directory (getHomeDirectory)
import Filters
sourceFilesFilter :: (MonadResource m, MonadIO m) => ProjectFilter -> FilePath -> C.ConduitM () String m ()
sourceFilesFilter projFilter dirname' =
C.sourceDirectoryDeep False dirname'
.| parseProject projFilter
parseProject :: (MonadResource m, MonadIO m) => ProjectFilter -> C.ConduitM FilePath String m ()
parseProject (ProjectFilter filterFunc) = do
C.awaitForever go
where
go path' = do
bytes <- liftIO $ B.readFile path'
let isProj = validProject bytes
when (isJust isProj) $ do
let proj' = fromJust isProj
when (filterFunc proj') $ C.yield path'
我的主要只是运行管道并打印通过过滤器的那些:
mainStreamingConduit :: IO ()
mainStreamingConduit = do
options <- getRecord "Search JSON Files"
let filterFunc = makeProjectFilter options
searchDir <- absolutize (searchPath options)
itExists <- doesDirectoryExist searchDir
case itExists of
False -> putStrLn "Search Directory does not exist" >> exitWith (ExitFailure 1)
True -> C.runConduitRes $ sourceFilesFilter filterFunc searchDir .| C.mapM_ (liftIO . putStrLn)
我像这样运行(通常没有统计数据):
stack exec search-json -- --searchPath $FILES --name NAME +RTS -s -A32m -n4m
在不增加幼儿园规模的情况下,我的生产力提高了30%左右。但是,如上所述,它看起来像这样:
72,308,248,744 bytes allocated in the heap
733,911,752 bytes copied during GC
7,410,520 bytes maximum residency (8 sample(s))
863,480 bytes maximum slop
187 MB total memory in use (27 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 580 colls, 580 par 2.731s 0.772s 0.0013s 0.0105s
Gen 1 8 colls, 7 par 0.163s 0.044s 0.0055s 0.0109s
Parallel GC work balance: 35.12% (serial 0%, perfect 100%)
TASKS: 10 (1 bound, 9 peak workers (9 total), using -N4)
SPARKS: 0 (0 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)
INIT time 0.001s ( 0.006s elapsed)
MUT time 26.155s ( 31.602s elapsed)
GC time 2.894s ( 0.816s elapsed)
EXIT time -0.003s ( 0.008s elapsed)
Total time 29.048s ( 32.432s elapsed)
Alloc rate 2,764,643,665 bytes per MUT second
Productivity 90.0% of total user, 97.5% of total elapsed
gc_alloc_block_sync: 3494
whitehole_spin: 0
gen[0].sync: 15527
gen[1].sync: 177
我仍然想知道如何并行化filterProj . parseJson . readFile
部分,但是现在我对此感到满意。