在Haskell中压缩monadic流中的更多性能

时间:2018-06-11 06:37:50

标签: performance haskell stream monads ffi

最直接的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 structnewsetget语义(即非常多的命令式)。典型的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.VectorfromVector之外,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. 选择LFSR作为玩具问题这一事实并不意味着Haskell无法有效地完成这些工作 - 请参阅SO问题"Efficient bit-fiddling in a LFSR implementation "
  2. 迭代16位LFSR 10M次是一件相当愚蠢的事情。最多需要 2 ^ 16-1次迭代才能再次达到起始状态。在最大长度LFSR中,它将完全 2 ^ 16-1次迭代。
  3. 更新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.hssrc/LFSR.hs。在性能方面,这没有区别(在时间差异内)。

    data LFSRStruct' = LFSRStruct' CUInt
    

1 个答案:

答案 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