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-ish
new
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