实施(^)

时间:2016-07-26 11:20:57

标签: performance haskell

我正在阅读标准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更快?

3 个答案:

答案 0 :(得分:13)

作为编写代码的人,我可以告诉你为什么它很复杂。 :) 想法是尾递归以获得循环,并且还执行最小数量的乘法。我不喜欢复杂性,所以如果你找到更优雅的方式,请提交错误报告。

答案 1 :(得分:5)

如果递归调用的返回值按原样返回,则函数是尾递归的,无需进一步处理。在expo中,f不是尾递归的,因为otherwise = x * f x (y-1)f的返回值在返回之前乘以xf 中的g(^)都是尾递归的,因为它们的返回值是未修改的。

为什么这很重要?尾递归函数可以比一般递归函数更有效地实现。因为编译器不需要为递归调用创建新的上下文(堆栈帧,你有什么),所以它可以重用调用者的上下文作为递归调用的上下文。这节省了调用函数的大量开销,就像内联函数比调用函数更有效。

答案 2 :(得分:2)

每当你在标准库中看到一个面包和黄油功能并且它实现得很奇怪时,其原因几乎总是如此:因为这样做会触发一些特殊的性能关键优化[可能在不同版本的编译器]&#34;。

这些奇怪的解决方法通常是“强迫”#34;编译器注意到一些特定的,重要的优化是可能的(例如,强制特定参数被认为是严格的,允许工人/包装器转换,无论如何)。通常有些人已经编制了他们的程序,注意到它的速度很慢,向GHC开发者抱怨,他们查看了编译后的代码和思想&#34;哦,GHC没有看到它可以内联第3工人职能...我该如何解决这个问题?&#34;结果是,如果您稍微重新编写代码,则会触发所需的优化。

你说你测试过它,速度差别不大。你没有说是什么类型。 (是指数Int还是Integer?基数怎么样?它很可能在一些模糊的情况下产生显着差异。)

为了保持严格/懒惰保证,偶尔也会实现功能。 (例如,图书馆规范说它必须以某种方式工作,并且以最明显的方式实现它将使该功能比规范声明更严格/更严格。)

我不知道这个特定的功能是什么,但我建议@chi可能会有所改进。