Performance Data.ByteString.Builder周围包装的空间泄漏
我在Data.ByteString.Builder周围有一个包装器类型,它允许我跟踪正在构建的ByteString的长度(cf.): 运行此代码需要几秒钟,并消耗大约250MB的内存。使用Performance Data.ByteString.Builder周围包装的空间泄漏,performance,haskell,memory,Performance,Haskell,Memory,我在Data.ByteString.Builder周围有一个包装器类型,它允许我跟踪正在构建的ByteString的长度(cf.): 运行此代码需要几秒钟,并消耗大约250MB的内存。使用Builder执行相同的任务要快得多,只需40KB。内存配置文件显示,所有额外空间都被BuildStep和Builder的实例占用,直接使用Builder时不会发生这种情况 是什么让这段代码如此低效?为什么在使用Builder时不会发生这种情况 编辑: Michael在下面的回答让我了解了零件的实际评估方法。
Builder
执行相同的任务要快得多,只需40KB。内存配置文件显示,所有额外空间都被BuildStep
和Builder
的实例占用,直接使用Builder
时不会发生这种情况
是什么让这段代码如此低效?为什么在使用Builder
时不会发生这种情况
编辑:
Michael在下面的回答让我了解了零件的实际评估方法。
在玩了更多的游戏之后,我用以下方式重写了测试代码:
makeStuff !acc 0 = acc
makeStuff !acc i = makeStuff (acc <> char 'x') (i - 1)
stuff = makeStuff mempty 10000000
-- stuffOld = mconcat $ replicate 10000000 $ char 'x'
main = hPutLBuilder stdout stuff
makeStuff!acc 0=acc
制造!acc i=制造品(acc字符'x')(i-1)
stuff=MakeSuff mempty 10000000
--Stuffld=mconcat$replicate 10000000$char'x'
main=hPutLBuilder标准输出
使用此定义,Builder
和LBuilder
的性能和内存使用完全相同(即可怕:-)。因此,在使用Builder
时,原始版本看起来非常快,因为编译器可以在编译时以某种方式将mconcat$replicate n$char c
重写为类似B.lazyByteString$L.replicate n(toAscii c)
的内容,而不是在堆上运行时编写10000000个函数。我试图通过查看生成的核心来确认这一点。我可以说:
stuffOld
的定义是对相对较短的函数的调用,该函数对Data.ByteString.Builder.Internal
中的类型执行某些操作
stuff
的定义是调用makeStuff
- 所说的核心并不意味着只有凡人才能理解
因此,我猜这只是一个病态的基准测试,我的应用程序中的实际性能问题在其他地方。一个问题是强制计算mconcat部分
表达式强制计算其lbLength
,这反过来将强制计算所有单个char'x'
值,这就是空间泄漏的原因。但是,我发现要使代码的性能与原始的Builder
相同,唯一的方法是在B.Builder
周围使用newtype
。甚至只是datalbuilder=LBuilder!B.Builder
引入了大量的开销。Hm.char'x'
真的需要多次评估吗?换句话说,是否真的存在单独的char'x'
值,或者只有一个值被多次引用?这不是一个容易回答的问题,它与GHC的共享规则有关。将char'x'
提升到顶级定义可能会强制共享该值。
parts = replicate 10000000 $ char 'x'
main = hPutLBuilder stdout $ mconcat parts
makeStuff !acc 0 = acc
makeStuff !acc i = makeStuff (acc <> char 'x') (i - 1)
stuff = makeStuff mempty 10000000
-- stuffOld = mconcat $ replicate 10000000 $ char 'x'
main = hPutLBuilder stdout stuff