Haskell:两个版本代码之间的速度差异
我开始潜入哈斯克尔,试图解决一些小问题 我偶然发现“标准haskell友好型”解决方案和我的“非常丑陋且haskell不友好型”解决方案之间存在巨大的性能差异(~100-200x) 我相信哈斯凯勒的同事们,这种表现上的差异有一个很好的原因,我不知道这一点,可以在这个话题上教育我 问题:查找数字字符串中的最大5位数 两者在求解时使用相同的概念:生成所有5位数字并找到最大值 优雅快速的代码Haskell:两个版本代码之间的速度差异,haskell,optimization,profiling,Haskell,Optimization,Profiling,我开始潜入哈斯克尔,试图解决一些小问题 我偶然发现“标准haskell友好型”解决方案和我的“非常丑陋且haskell不友好型”解决方案之间存在巨大的性能差异(~100-200x) 我相信哈斯凯勒的同事们,这种表现上的差异有一个很好的原因,我不知道这一点,可以在这个话题上教育我 问题:查找数字字符串中的最大5位数 两者在求解时使用相同的概念:生成所有5位数字并找到最大值 优雅快速的代码 digit5 :: String -> Int digit5 = maximum . map (read
digit5 :: String -> Int
digit5 = maximum . map (read . take 5) . init . tails
丑陋且非常慢的代码(一旦字符串大)
我对这一点的肤浅理解是,fast代码使用的是预先可用的高阶函数。对于较慢的版本,使用递归,但我认为咬尾应该是可能的。但在天真的层面上,对我来说,两人似乎做了同样的事情
虽然较慢的函数对字符串进行比较,而不是将其转换为数字,但我也尝试将字符串转换为整数,但没有任何大的改进
我尝试过使用ghc(不带任何标志)和以下命令进行编译:
ghc
ghc -O2
ghc -O2 -fexcess-precision -optc-O3 -optc-ffast-math -no-
recomp
stack runhaskell
ghc -O3
为了再现性,我在代码中添加了一个链接,其中也包含测试向量:您的“慢”版本的问题是这一行:
| length xs < 5 = maxim
它将使整个事情只是线性的,它将与“优雅”的解决方案一样快。当然,这将导致额外的5次迭代,但通过降低总体复杂性,损失得到了更多的补偿
或者,您也可以通过过滤掉5个字符或更短的尾巴,使“优雅”解决方案同样缓慢:
digit5 = maximum . map (read . take 5) . filter ((>= 5) . length) . tails
有一些函数在列表上递归,并在每次迭代时调用
length
<代码>长度为O(n)。您不小心在运行时添加了一个O(n^2)项。只需跳过保护| length xs<5=maxim
就可以获得一个额外的模式digit5'[]maxim=maxim
给我0.004秒而不是1.7秒。这比优雅的解决方案还要快。(读取将始终消耗5字节的部分字符串,而字符串比较可能只需要比较第一个字符。)关于进一步提高速度的一些建议:对string
的字典顺序和对Integer
的顺序一致,因此您可以完全跳过读取
。您还可以使用一个奇特的技巧来避免过滤器:zipWith const(tails xs)(drop 4 xs)
将生成xs
的所有至少5个尾部(…或者您可以移动read
,如read.maximum.map(take 5)
,如果您想保持相同的类型。我已在以下计时测试中完成了此操作。)结果:digit5ReadFilter
,长度为10000的字符串为0.32s<编码>数字5ReadzipWith
,0.04sdigit5LexFilter
,0.27sdigit5LexZipwith
甚至没有注册(报告为0.00s)。这是我在学习Haskell时遇到的第一个严格错误:length xs<5
在这里太严格了,因为Int
太严格了。如果使用像Data.Nat
这样的惰性数字类型,它将在5个元素之后停止计算xs
:genericLength xs<(5::Nat)
。这同样适用于过滤器((>=(5::Nat)).genericLength)
。请尝试digit5=最大值。扫描(\ab->rem(10*a+b)100000)0。地图数码点
。我没有对它进行基准测试,但我认为缺少字符串操作应该会有所帮助。
| length xs < 5 = maxim
| null xs = maxim
digit5 = maximum . map (read . take 5) . filter ((>= 5) . length) . tails