Haskell:两个版本代码之间的速度差异
Posted
技术标签:
【中文标题】Haskell:两个版本代码之间的速度差异【英文标题】:Haskell: speed difference between two versions of code 【发布时间】:2017-08-11 13:35:57 【问题描述】:通过尝试解决一些小问题,我开始深入研究 Haskell。
我偶然发现 “标准的 haskell 友好型” 解决方案和我的 “非常丑陋且对 haskell 不友好”之间存在很大的性能差异(~100-200 倍) em> 版本。
我相信对于 haskeller 同胞来说,这种性能差异有一个很好的理由,我错过了这一点,并且可以在这个主题上对我进行教育。
问题:在一个数字字符串中找出最大的5位数字
两者在求解中使用相同的概念:生成所有 5 位数字并找到最大值。
优雅快速的代码
digit5 :: String -> Int
digit5 = maximum . map (read . take 5) . init . tails
丑陋且非常慢的代码(一旦字符串很大)
digit5' :: String -> String -> String
-- xs - input string
-- maxim - current maximal value
digit5' xs maxim
| (length xs) < 5 = maxim
| y > maxim = digit5' (drop 1 xs) y -- use new detected maximum value
| otherwise = digit5' (drop 1 xs) maxim
where y = take 5 xs
digit5 :: String -> Int
digit5 xs
-- return the original string if the input size is smaller than 6
| length (xs) < 6 = read xs
| otherwise = read $ digit5' xs "00000"
对于小输入,我得到大致相同的执行时间,对于大输入,我开始看到非常大的差异(对于 44550 个元素的输入):
Computation time for ugly version: 2.047 sec
Computation time for nice version: 0.062 sec
我对此的肤浅理解是,快速代码使用的是预先可用的高阶函数。对于较慢的版本,使用递归,但我认为咬尾应该是可能的。但是在幼稚的层面上,对我来说两者似乎都在做同样的事情。
虽然较慢的函数对字符串进行比较而不是将它们转换为数字,但我也尝试将字符串转换为整数,但没有任何大的改进
我尝试使用不带任何标志的 ghc 并使用以下命令进行编译:
ghc
ghc -O2
ghc -O2 -fexcess-precision -optc-O3 -optc-ffast-math -no-
recomp
stack runhaskell
ghc -O3
为了重现性,我添加了一个包含测试向量的代码链接:https://pastebin.com/kuS5iKgd
【问题讨论】:
您有在列表上递归并在每次迭代时调用length
的函数。 length
是 O(n)。您不小心在运行时添加了 O(n^2) 项。
跳过守卫| length xs < 5 = maxim
以获得额外的模式digit5' [] maxim = maxim
给了我 0.004 秒而不是 1.7 秒。这甚至比优雅的解决方案还要快。 (读取总是会消耗部分字符串的 5 个字节,而字符串比较可能只需要比较第一个字符。)
【参考方案1】:
你的“慢”版本的问题是这一行:
| length xs < 5 = maxim
这会计算xs
的长度,因为Haskell链表是单链表,所以这个操作需要完整遍历整个链表,也就是O(n)。它发生在每次迭代中。并且有N次迭代。这使得整个过程 O(n^2)。
另一方面,“快速”代码只是线性的,在大输入时会生动地显示出来。
如果你只是用这个替换有问题的行:
| null xs = maxim
它将使整个事情变得线性,并且与“优雅”的解决方案一样快。当然,这会导致额外的 5 次迭代,但通过降低整体复杂性可以弥补这种损失。
或者,或者,您可以通过过滤掉 5 个字符或更短的尾部来使“优雅”解决方案同样缓慢:
digit5 = maximum . map (read . take 5) . filter ((>= 5) . length) . tails
【讨论】:
一些提高速度的建议:String
上的字典顺序和Integer
上的顺序一致,因此您可以完全跳过read
。你也可以用一个花哨的技巧来避免filter
:zipWith const (tails xs) (drop 4 xs)
将产生xs
的所有至少长度为 5 的尾部。
(...或者你可以移动read
,如read . maximum . map (take 5)
,如果你想保持相同的类型。我在下面的时序测试中这样做了。) 结果:@ 987654333@, 0.32s for a length-10000 String
; digit5ReadZipwith
, 0.04s; digit5LexFilter
, 0.27s; digit5LexZipwith
甚至没有注册(报告 0.00s)。
这是我在学习 Haskell 时遇到的第一个严格的 bug:length xs < 5
这里太严格了,因为Int
太严格了。如果您使用像 Data.Nat
这样的惰性数字类型,它将在 5 个元素后停止评估 xs
:genericLength xs < (5 :: Nat)
。 filter ((>= (5 :: Nat)) . genericLength)
也是如此。
试试digit5 = maximum . scanl (\a b -> rem (10 * a + b) 100000) 0 . map digitToInt
。我没有对其进行基准测试,但我想缺少字符串操作应该会有所帮助。以上是关于Haskell:两个版本代码之间的速度差异的主要内容,如果未能解决你的问题,请参考以下文章