我正在阅读标准haskell库的实现(^)的代码:
(^) :: (Num a, Integral b) => a -> b -> a
x0 ^ y0 | y0 < 0 = errorWithoutStackTrace "Negative exponent"
| y0 == 0 = 1
| otherwise = f x0 y0
where -- f : x0 ^ y0 = x ^ y
f x y | even y = f (x * x) (y `quot` 2)
| y == 1 = x
| otherwise = g (x * x) ((y - 1) `quot` 2) x
-- g : x0 ^ y0 = (x ^ y) * z
g x y z | even y = g (x * x) (y `quot` 2) z
| y == 1 = x * z
| otherwise = g (x * x) ((y - 1) `quot` 2) (x * z)
现在这个定义g的部分对我来说似乎很奇怪为什么不像这样实现它:
expo :: (Num a ,Integral b) => a -> b ->a
expo x0 y0
| y0 == 0 = 1
| y0 < 0 = errorWithoutStackTrace "Negative exponent"
| otherwise = f x0 y0
where
f x y | even y = f (x*x) (y `quot` 2)
| y==1 = x
| otherwise = x * f x (y-1)
但确实插入说3 ^ 1000000表明(^)比世博会快约0.04秒。
为什么
(^)
比expo
更快?
答案 0 :(得分:13)
作为编写代码的人,我可以告诉你为什么它很复杂。 :) 想法是尾递归以获得循环,并且还执行最小数量的乘法。我不喜欢复杂性,所以如果你找到更优雅的方式,请提交错误报告。
答案 1 :(得分:5)
如果递归调用的返回值按原样返回,则函数是尾递归的,无需进一步处理。在expo
中,f
不是尾递归的,因为otherwise = x * f x (y-1)
:f
的返回值在返回之前乘以x
。 f
中的g
和(^)
都是尾递归的,因为它们的返回值是未修改的。
为什么这很重要?尾递归函数可以比一般递归函数更有效地实现。因为编译器不需要为递归调用创建新的上下文(堆栈帧,你有什么),所以它可以重用调用者的上下文作为递归调用的上下文。这节省了调用函数的大量开销,就像内联函数比调用函数更有效。
答案 2 :(得分:2)
每当你在标准库中看到一个面包和黄油功能并且它实现得很奇怪时,其原因几乎总是如此:因为这样做会触发一些特殊的性能关键优化[可能在不同版本的编译器]&#34;。
这些奇怪的解决方法通常是“强迫”#34;编译器注意到一些特定的,重要的优化是可能的(例如,强制特定参数被认为是严格的,允许工人/包装器转换,无论如何)。通常有些人已经编制了他们的程序,注意到它的速度很慢,向GHC开发者抱怨,他们查看了编译后的代码和思想&#34;哦,GHC没有看到它可以内联第3工人职能...我该如何解决这个问题?&#34;结果是,如果您稍微重新编写代码,则会触发所需的优化。
你说你测试过它,速度差别不大。你没有说是什么类型。 (是指数Int
还是Integer
?基数怎么样?它很可能在一些模糊的情况下产生显着差异。)
为了保持严格/懒惰保证,偶尔也会实现功能。 (例如,图书馆规范说它必须以某种方式工作,并且以最明显的方式实现它将使该功能比规范声明更严格/更严格。)
我不知道这个特定的功能是什么,但我建议@chi可能会有所改进。