最直接的monadic' stream'只是一个monadic动作列表Monad m => [m a]
。 sequence :: [m a] -> m [a]
函数评估每个monadic操作并收集结果。事实证明,sequence
效率不高,因为它在列表上运行,monad是在最简单的情况下实现融合的障碍。
问题是:monadic流最有效的方法是什么?
为了研究这个问题,我提出了一个玩具问题以及一些提高性能的尝试。源代码可以在github上找到。下面提出的单一基准可能会误导更现实的问题,尽管我认为这是一种最糟糕的情况,即每次有用计算的最大开销。
玩具问题
是最大长度为16位Linear Feedback Shift Register(LFSR),在C中以稍微过于复杂的方式实现,在IO
monad中使用Haskell包装器。 '过精心'指的是不必要地使用struct
及其malloc
- 这种复杂化的目的是使它更像现实情况,你所拥有的是围绕FFI的Haskell包装器到C {{ 1}}具有OO-ish struct
,new
,set
,get
语义(即非常多的命令式)。典型的Haskell程序如下所示:
operate
默认任务是计算LFSR的10&000; 000和000次迭代的值(双精度)的平均值。此任务可以是一组测试的一部分,用于分析此16位整数流的“随机性”。
0。基线
基线是平均超过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实现:
n
C实现并不意味着特别好或快。它只是提供了一个有意义的计算。
1。 Haskell列出
与C基线相比,在此任务中,Haskell列表的速度要慢73倍。
double avg(state_t* s, int n) {
double sum = 0;
for(int i=0; i<n; i++, sum += step_get_lfsr(s));
return sum / (double)n;
}
这是实施(RunAvg.hs
):
=== RunAvg =========
Baseline: 1.874e-2
IO: 1.382488
factor: 73.77203842049093
2。使用streaming
库
这使我们达到基线的9倍,
step1 :: LFSR -> IO Word32
step1 lfsr = stepLFSR lfsr >> getLFSR lfsr
avg :: LFSR -> Int -> IO Double
avg lfsr n = mean <$> replicateM n (step1 lfsr) where
mean :: [Word32] -> Double
mean vs = (sum $ fromIntegral <$> vs) / (fromIntegral n)
(请注意,在这些较短的执行时间内,基准测试相当不准确。)
这是实施(RunAvgStreaming.hs
):
=== RunAvgStreaming ===
Baseline: 1.9391e-2
IO: 0.168126
factor: 8.670310969006241
第3。使用Data.Vector.Fusion.Stream.Monadic
到目前为止,这提供了最佳性能,在基线的3倍之内,
import qualified Streaming.Prelude as S
avg :: LFSR -> Int -> IO Double
avg lfsr n = do
let stream = S.replicateM n (fromIntegral <$> step1 lfsr :: IO Double)
(mySum :> _) <- S.sum stream
return (mySum / fromIntegral n)
像往常一样,这里是实现(RunAvgVector.hs
):
=== RunVector =========
Baseline: 1.9986e-2
IO: 4.9146e-2
factor: 2.4590213149204443
我没有期望在import qualified Data.Vector.Fusion.Stream.Monadic as V
avg :: LFSR -> Int -> IO Double
avg lfsr n = do
let stream = V.replicateM n (step1' lfsr)
V.foldl (+) 0.0 stream
下找到一个好的monadic流实现。除了提供Data.Vector
和fromVector
之外,concatVectors
与来自Data.Vector.Fusion.Stream.Monadic
的{{1}}几乎没有关系。
查看分析报告显示Vector
有相当大的空间泄漏,但听起来并不合适。
4。列表不一定很慢
对于非常简单的操作,列表并不可怕:
Data.Vector
这里,for循环在Haskell中完成,而不是将其推到C(RunRepeat.hs
):
Data.Vector.Fusion.Stream.Monadic
这只是重复调用=== RunRepeat =======
Baseline: 1.8078e-2
IO: 3.6253e-2
factor: 2.0053656377917912
而不将结果传递回Haskell层。它指示了调用包装器和FFI的开销会产生什么影响。
分析
上面的do
setLFSR lfsr 42
replicateM_ nIter (stepLFSR lfsr)
getLFSR lfsr
示例表明,大多数但不是全部(?)的性能损失来自调用包装器和/或FFI的开销。但我现在还不确定在哪里寻找调整。也许这就像monadic溪流一样好,事实上这就是削减FFI,现在......
图片的标题说明
更新1
尝试删除stepLFSR
来电可以通过引入
repeat
然后使用withForeignPtr
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'
请参阅RunRepeatAlloca.hs和src/LFSR.hs。在性能方面,这没有区别(在时间差异内)。
data LFSRStruct' = LFSRStruct' CUInt
答案 0 :(得分:1)
在解释GHC的RunRepeat.hs
组装产品后,我得出了这样的结论:GHC不会内联调用C函数step_lfsr(state_t*)
,而C编译器会,这对于这个玩具问题来说差别很大。
我可以通过禁止使用__attribute__ ((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_step
的FFI呼叫不再有任何惩罚。
=== 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呼叫:"How to force GHC to inline FFI calls?" 。
对于内联的好处如此之高的情况,即每个FFI调用的工作负载如此之低,可能值得研究inline-c
。