2017-08-11 62 views
3

我已经开始通过尝试解决一些小问题来潜入Haskell。Haskell:代码的两个版本之间的速度差

我于“标准哈斯克尔友好”解决方案,我“极丑和Haskell不友好”版本之间有很大的性能差异(〜100-200x)绊倒了。

我敢肯定,对于同性恋者来说,这种表现上的差异有一个很好的理由,我错过了,可以教我关于这个话题。

问题:查找数字串

内的最高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 

对于重复性我加入的链接代码contaning也测试向量的缘故:https://pastebin.com/kuS5iKgd

+5

您有递归地在一个列表,并调用函数'length'在每一次迭代。长度是O(n)。您不小心向运行时添加了O(n^2)项。 – Carl

+2

只是跳过警卫'|长度为xs <5 = maxim'为额外的模式'digit5'[] maxim = maxim'给我0.004秒而不是1.7秒。这比优雅的解决方案还要快。 (读取总是会消耗部分字符串的5个字节,而字符串比较可能只需要比较第一个字符)。 – Krom

回答

9

与“慢”的版本问题是THI s的线路:

| 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 
+2

一些关于进一步加速的建议:String中的字典顺序和Integer中的顺序一致你可以完全跳过'read'。你也可以用一个奇特的技巧来避免'filter':'zipWith const(tail xs)(drop 4 xs)'将产生'xs'的所有长度至少为5的尾部。 –

+0

(...或者你可以移动'read',如'read。maximum。map(take 5)',如果你想保持相同的类型,我在下面的时间测试中做了这个。)结果: 'digit5ReadFilter',长度为10000的String为0.32s; 'digit5ReadZipwith',0.04s; 'digit5LexFilter',0.27s; 'digit5LexZipwith'甚至没有注册(报告0.00s)。 –

+0

这是我在学习Haskell时遇到的第一个严重错误:由于'Int'太严格,所以'length xs <5'在这里太严格了。如果使用像Data.Nat这样的惰性数字类型,它将在5个元素后停止计算'xs':'genericLength xs <(5 :: Nat)'。 'filter((> =(5 :: Nat))。genericLength)'也是一样。 –