我正在开发一个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表达式不计算为常量
所以我无法看到它的效果。
答案 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)
好的,所以我想我会更新我所做的事情以及迄今取得的成果。所做的更改:
我使用以下选项进行编译:
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的展开),或者因为它们只是更糟糕的选择。
我仍然对这些新调整提出了一些新问题。
突然间,我在内存使用方面遇到了这种特殊的问题。看看下面的堆配置文件:
为什么会这样?是因为UnboxedArray吗?那是什么意思?
当我通过C编译时,我收到以下警告:
警告:-fvia-C标志什么都不做;它将在未来的GHC版本中删除
这是真的吗?为什么然后,我是否看到使用它的更好的性能,而不是?
答案 3 :(得分:3)
看起来你已经做了相当多的调整;我很好奇没有明确的严格注释(BangPatterns
)和各种编译器编译指示(UNPACK
,INLINE
)的性能是什么......还有一个愚蠢的问题:什么是优化标记你在用吗?
无论如何,有两条建议可能非常糟糕:
Data.Word.Word64
替换为GHC.Word.Word64#
,确保word128Shift
正在使用Int#
等)以避免堆分配。当然,这是非便携式的。Data.Sequence
代替[]
无论如何,不要查看Core输出,而是尝试查看中间C文件(*.hc
)。它可能很难通过,但有时候很明显,编译器并不像你希望的那样尖锐。