如何让我的Haskell程序更快?与C的比较

时间:2011-10-25 18:07:05

标签: performance haskell profiling

我正在开发一个SHA3候选者JH的实现。我正处于算法通过NIST提供的所有KAT(已知答案测试)的地步,并且还使其成为Crypto-API的实例。因此,我开始研究它的表现。但我对Haskell很新,并且在分析时并不知道该寻找什么。

目前,我的代码总是慢于用C语言编写的参考实现,所有输入长度都是10倍(此处的C代码为http://www3.ntu.edu.sg/home/wuhj/research/jh/jh_bitslice_ref64.h)。

我的Haskell代码可在此处找到:https://github.com/hakoja/SHA3/blob/master/Data/Digest/JHInternal.hs

现在我不希望你浏览我的所有代码,而只是想要一些关于几个函数的技巧。我已经运行了一些性能测试,这是GHC生成的性能文件(的一部分):

Tue Oct 25 19:01 2011 Time and Allocation Profiling Report  (Final)

   main +RTS -sstderr -p -hc -RTS jh e False

total time  =        6.56 secs   (328 ticks @ 20 ms)
total alloc = 4,086,951,472 bytes  (excludes profiling overheads)

COST CENTRE                    MODULE               %time %alloc

roundFunction                  Data.Digest.JHInternal  28.4   37.4
word128Shift                   Data.BigWord.Word128  14.9   19.7
blockMap                       Data.Digest.JHInternal  11.9   12.9
getBytes                       Data.Serialize.Get     6.7    2.4
unGet                          Data.Serialize.Get     5.5    1.3
sbox                           Data.Digest.JHInternal   4.0    7.4
getWord64be                    Data.Serialize.Get     3.7    1.6
e8                             Data.Digest.JHInternal   3.7    0.0
swap4                          Data.Digest.JHInternal   3.0    0.7
swap16                         Data.Digest.JHInternal   3.0    0.7
swap8                          Data.Digest.JHInternal   1.8    0.7
swap32                         Data.Digest.JHInternal   1.8    0.7
parseBlock                     Data.Digest.JHInternal   1.8    1.2
swap2                          Data.Digest.JHInternal   1.5    0.7
swap1                          Data.Digest.JHInternal   1.5    0.7
linearTransform                Data.Digest.JHInternal   1.5    8.6
shiftl_w64                     Data.Serialize.Get     1.2    1.1

Detailed breakdown omitted ...

现在快速了解JH算法:

这是一种散列算法,它由压缩函数F8组成,只要存在输入块(长度为512位),它就会重复。这就是SHA功能的运作方式。 F8功能由E8功能组成,该功能可应用42次圆形功能。圆函数本身由三部分组成: 一个sbox,一个线性转换和一个排列(在我的代码中称为swap)。

因此,大部分时间花在圆函数上是合理的。我仍然想知道如何改进这些部件。例如:blockMap函数只是一个实用函数,将函数映射到4元组中的元素。那为什么表现如此糟糕?任何建议都是受欢迎的,而不仅仅是单一功能,即您是否会为改善性能而进行结构性改变?

我已经尝试过查看Core输出,但不幸的是,这已经超出了我的想象。

我最后附上了一些堆配置文件,以备可能感兴趣。

编辑:

我忘了提及我的设置和构建。我在x86_64 Arch Linux机器上运行它,GHC 7.0.3-2(我认为),带有编译选项:

  

ghc --make -O2 -funbox-strict-fields

在通过C或LLVM进行编译时Linux平台上不幸there seems to be a bug,给我错误:

  

错误:XXXX的.size表达式不计算为常量

所以我无法看到它的效果。

enter image description here

enter image description here

4 个答案:

答案 0 :(得分:20)

  • 切换到未装箱的矢量(来自数组,用于常量)
  • 使用unsafeIndex代替安全索引(即!
  • 产生边界检查和数据依赖性
  • 像对Block1024一样解包Block512(或至少使用UnboxedTuples
  • 使用unsafeShift{R,L},这样您就不会检查班次值(进入GHC 7.4)
  • 展开roundFunction,以便您拥有一个相当丑陋且冗长的e8功能。这在pureMD5中很有意义(滚动版本比展开版本更漂亮但速度更慢)。您可以use TH to do这个并保持代码小。如果你这样做,那么你就不需要constants,因为这些值在代码中是显式的,并且会产生更多缓存友好的二进制文件。
  • 打开Word128值。
  • Word128定义您自己的附加内容,请勿抬起Integer。请参阅LargeWord了解an example如何做到这一点。
  • rem not mod
  • 使用优化进行编译(-O2)并尝试llvm(-fllvm

编辑:并将您的git repo与基准测试结合起来,以便我们可以帮助您更轻松;-)。包括crypto-api实例的好工作。

答案 1 :(得分:8)

下图显示列表占用了大量内存。除非潜伏在其他模块中,否则它们只能来自e8。也许你必须咬紧牙关并将其作为一个循环而不是折叠,但对于初学者,由于Block1024是一对,foldl'在动态上没有做太多的评估(除非严格性分析仪已经变得更好了)。尝试制作更严格的data Block1024 = B1024 !Block512 !Block512,也许它还需要{-# UNPACK #-}个pragma。在roundFunction中,使用rem代替mod(这只会产生轻微影响,但速度会快一些)并严格限制let绑定。在swapN函数中,您可以获得更好的性能,以W x y形式给出常量,而不是128位十六进制数。 我无法保证这些改变会有所帮助,但这只是一瞥之后看起来最有希望的。

答案 2 :(得分:4)

好的,所以我想我会更新我所做的事情以及迄今取得的成果。所做的更改:

  • 从Array切换到UnboxedArray(使Word128成为实例类型)
  • 在e8中使用UnboxedArray + fold而不是列表和(前奏)折叠
  • 使用unsafeIndex而不是!
  • 将Block1024的类型更改为实际数据类型(类似于Block512),并解压缩其参数
  • 在Arch Linux上将GHC更新到版本7.2.1,从而修复了通过C或LLVM编译的问题
  • 在某些地方切换到rem,但不在 roundFunction 。当我在那里执行时,编译时间突然耗费了大量时间,运行时间变慢了10倍!有谁知道为什么会这样?它只发生在GHC-7.2.1,而不是GHC-7.0.3

我使用以下选项进行编译:

  

ghc-7.2.1 --make -O2 -funbox-strict-fields main.hs ./Tests/testframe.hs -fvia-C -optc-O2

结果呢?大约减少50%的时间。在~1010 MB的输入上,代码现在使用3分钟,而之前的6-7分钟。 C版使用42秒。

我尝试的东西,但没有带来更好的性能:

  • 展开e8这样的功能:

      

    e8!h = go h 0

         

    去哪里!x!n

          | n == 42   = x
          | otherwise = go h' (n + 1)
          where !h' = roundFunction x n
    
  • 尝试分解swapN函数以直接使用基础Word64:

      

    swap1(W xh hl)=

         shiftL (W (xh .&. 0x5555555555555555) (xl .&. 0x5555555555555555)) 1 
         .|. 
         shiftR (W (xh .&. 0xaaaaaaaaaaaaaaaa) (xl .&. 0xaaaaaaaaaaaaaaaa)) 1 
    
  • 尝试使用LLVM后端

所有这些尝试都表现出比我目前更糟糕的表现。我不知道是不是因为我做错了(特别是e8的展开),或者因为它们只是更糟糕的选择。

我仍然对这些新调整提出了一些新问题。

  1. 突然间,我在内存使用方面遇到了这种特殊的问题。看看下面的堆配置文件: enter image description here enter image description here

    为什么会这样?是因为UnboxedArray吗?那是什么意思?

  2. 当我通过C编译时,我收到以下警告:

      

    警告:-fvia-C标志什么都不做;它将在未来的GHC版本中删除

    这是真的吗?为什么然后,我是否看到使用它的更好的性能,而不是?

答案 3 :(得分:3)

看起来你已经做了相当多的调整;我很好奇没有明确的严格注释(BangPatterns)和各种编译器编译指示(UNPACKINLINE)的性能是什么......还有一个愚蠢的问题:什么是优化标记你在用吗?

无论如何,有两条建议可能非常糟糕:

  1. 尽可能使用未装箱的基元类型(例如,将Data.Word.Word64替换为GHC.Word.Word64#,确保word128Shift正在使用Int#等)以避免堆分配。当然,这是非便携式的。
  2. 尝试Data.Sequence代替[]
  3. 无论如何,不​​要查看Core输出,而是尝试查看中间C文件(*.hc)。它可能很难通过,但有时候很明显,编译器并不像你希望的那样尖锐。