Performance 在Haskell中从一元流中挤出更多性能
最直接的一元“流”只是一个一元操作列表Performance 在Haskell中从一元流中挤出更多性能,performance,haskell,stream,monads,ffi,Performance,Haskell,Stream,Monads,Ffi,最直接的一元“流”只是一个一元操作列表Monad m=>[ma]。该函数评估每个一元操作并收集结果。然而,事实证明,序列并不是很有效,因为它在列表上运行,而monad在最简单的情况下是实现融合的障碍 问题是:对于一元流,什么是最有效的方法? 为了调查这一点,我提供了一个玩具问题以及一些改进性能的尝试。源代码可以在上找到。下面给出的单一基准可能会误导更现实的问题,尽管我认为这是一种最坏的情况,即每个有用计算的最大可能开销 玩具问题 是一个最大长度16位(LFSR),在C中以一种过于复杂的方式实现,
Monad m=>[ma]
。该函数评估每个一元操作并收集结果。然而,事实证明,序列
并不是很有效,因为它在列表上运行,而monad在最简单的情况下是实现融合的障碍
问题是:对于一元流,什么是最有效的方法?
为了调查这一点,我提供了一个玩具问题以及一些改进性能的尝试。源代码可以在上找到。下面给出的单一基准可能会误导更现实的问题,尽管我认为这是一种最坏的情况,即每个有用计算的最大可能开销
玩具问题
是一个最大长度16位(LFSR),在C中以一种过于复杂的方式实现,在IO
monad中使用Haskell包装器“过于复杂”指的是不必要地使用struct
及其malloc
——这种复杂化的目的是使其更类似于现实情况,在这种情况下,您所拥有的只是一个围绕FFI的Haskell包装器,使用OO-ishnew
,set
,get
,将FFI包装成C结构,操作
语义(即命令式风格)。典型的Haskell程序如下所示:
import LFSR
main = do
lfsr <- newLFSR -- make a LFSR object
setLFSR lfsr 42 -- initialise it with 42
stepLFSR lfsr -- do one update
getLFSR lfsr >>= print -- extract the new value and print
C的实现并不意味着特别好或特别快。它只是提供了一个有意义的计算
1。Haskell列表
与C基线相比,在这个任务中,Haskell列表要慢73倍
=== RunAvg =========
Baseline: 1.874e-2
IO: 1.382488
factor: 73.77203842049093
这是实现():
(请注意,在这些较短的执行时间内,基准测试相当不准确。)
这是实现():
与往常一样,下面是实现():
我没想到会在Data.Vector
下找到一个好的单元流实现。除了提供来自向量的和concatVectors
,数据.向量.融合.流.一元
与来自数据.向量的向量
关系不大
查看分析报告显示,Data.Vector.Fusion.Stream.Monadic
有相当大的空间泄漏,但这听起来并不正确
4。列表不一定慢
对于非常简单的操作,列表一点也不可怕:
=== RunRepeat =======
Baseline: 1.8078e-2
IO: 3.6253e-2
factor: 2.0053656377917912
这里,for循环是在Haskell中完成的,而不是将其下推到C():
这只是重复调用stepLFSR
,而不将结果传回Haskell层。它给出了调用包装器和FFI的开销有何影响的指示
分析
上面的repeat
示例表明,大部分(但不是全部)性能损失来自调用包装器和/或FFI的开销。但我现在不确定该在哪里寻找调整。也许这和一元流一样好,事实上,这是关于削减FFI,现在
旁注
事实上,LFSR被选为一个玩具问题并不意味着Haskell不能有效地解决这些问题——参见所谓的问题
迭代16位LFSR 10M次是一件相当愚蠢的事情。最多需要2^16-1次迭代才能再次达到开始状态。在最大长度LFSR中,需要2^16-1次迭代
更新1
可以通过引入
Storable
然后使用alloca::Storable a=>(Ptr a->IO b)->IO b
repeatSteps :: Word32 -> Int -> IO Word32
repeatSteps start n = alloca rep where
rep :: Ptr LFSRStruct' -> IO Word32
rep p = do
setLFSR2 p start
(sequence_ . (replicate n)) (stepLFSR2 p)
getLFSR2 p
其中LFSRStruct'
是
data LFSRStruct' = LFSRStruct' CUInt
包装纸是
foreign import ccall unsafe "lfsr.h set_lfsr"
setLFSR2 :: Ptr LFSRStruct' -> Word32 -> IO ()
-- likewise for setLFSR2, stepLFSR2, ...
见和。就性能而言,这没有任何区别(在时间差异内)
在为RunRepeat.hs
破译GHC的汇编产品后,我得出了这样的结论:GHC不会内联调用C函数step\u lfsr(state\t*)
,而C编译器会,这使得这个玩具问题的完全不同
我可以通过禁止使用\uuuu属性((noinline))
pragma进行内联来证明这一点。总的来说,C可执行文件速度变慢,因此Haskell和C之间的差距缩小了
结果如下:
=== RunRepeat =======
#iter: 100000000
Baseline: 0.334414
IO: 0.325433
factor: 0.9731440669349967
=== RunRepeatAlloca =======
#iter: 100000000
Baseline: 0.330629
IO: 0.333735
factor: 1.0093942152684732
=== RunRepeatLoop =====
#iter: 100000000
Baseline: 0.33195399999999997
IO: 0.33791
factor: 1.0179422450098508
也就是说,外国金融机构呼叫lfsr\u步骤
不再受到处罚
=== RunAvg =========
#iter: 10000000
Baseline: 3.4072e-2
IO: 1.3602589999999999
factor: 39.92307466541442
=== RunAvgStreaming ===
#iter: 50000000
Baseline: 0.191264
IO: 0.666438
factor: 3.484388070938598
好的旧列表不会融合,因此性能会受到巨大影响,streaming
库也不是最佳的。但是Data.Vector.Fusion.Stream.Monadic
的性能在C的20%以内:
=== RunVector =========
#iter: 200000000
Baseline: 0.705265
IO: 0.843916
factor: 1.196594188000255
已经观察到GHC不内联FFI呼叫:
对于内联的好处如此之高的情况,即每个FFI调用的工作量如此之低的情况,可能值得研究。我会尝试在FFI级别将慢的部分本地化。例如,让FFI调用的函数是最终平凡的,返回0。打电话还是要花很多钱吗?重新打包这些参数要花多少钱?你的意思是只调用void f(…)
函数?例如,重新包装吸气剂?是的。可能是访问参数,或者打包返回值,导致性能下降;对于void nothing(){}
,它可能是可见的。OTOH调用外部过程只是为了产生副作用(不传递或接收数据),即使实现了流融合,也没有什么意义,所以我的观点可能没有意义。
do
setLFSR lfsr 42
replicateM_ nIter (stepLFSR lfsr)
getLFSR lfsr
repeatSteps :: Word32 -> Int -> IO Word32
repeatSteps start n = alloca rep where
rep :: Ptr LFSRStruct' -> IO Word32
rep p = do
setLFSR2 p start
(sequence_ . (replicate n)) (stepLFSR2 p)
getLFSR2 p
data LFSRStruct' = LFSRStruct' CUInt
foreign import ccall unsafe "lfsr.h set_lfsr"
setLFSR2 :: Ptr LFSRStruct' -> Word32 -> IO ()
-- likewise for setLFSR2, stepLFSR2, ...
=== RunRepeatAlloca =======
Baseline: 0.19811199999999998
IO: 0.33433
factor: 1.6875807623970283
=== RunRepeat =======
#iter: 100000000
Baseline: 0.334414
IO: 0.325433
factor: 0.9731440669349967
=== RunRepeatAlloca =======
#iter: 100000000
Baseline: 0.330629
IO: 0.333735
factor: 1.0093942152684732
=== RunRepeatLoop =====
#iter: 100000000
Baseline: 0.33195399999999997
IO: 0.33791
factor: 1.0179422450098508
=== RunAvg =========
#iter: 10000000
Baseline: 3.4072e-2
IO: 1.3602589999999999
factor: 39.92307466541442
=== RunAvgStreaming ===
#iter: 50000000
Baseline: 0.191264
IO: 0.666438
factor: 3.484388070938598
=== RunVector =========
#iter: 200000000
Baseline: 0.705265
IO: 0.843916
factor: 1.196594188000255