【问题标题】:Any idea why Go seems to be relatively slow on a recursive fibonacci?知道为什么 Go 在递归斐波那契上似乎相对较慢吗?
【发布时间】:2018-05-08 19:46:52
【问题描述】:

我刚刚偶然发现了这个不错的小仓库,它比较了几种编译和解释语言的简单递归斐波那契函数:https://github.com/drujensen/fib。这似乎很公平,因为它在任何地方都没有做任何优化技巧。我知道有更好的方法来使用 Go 的强大功能,但我只是想知道,为什么 Go 似乎比其他编译和静态类型语言慢得多?我可以在我的机器上用 11s 确认它看起来与 Go 非常相似。

【问题讨论】:

    标签: performance go fibonacci


    【解决方案1】:

    TL;DR 到目前为止,我的结论是,只要 Go 中没有尾调用优化,我们可能应该主要避免递归而支持迭代算法。如果不使用递归,Go 似乎非常快:-)

    出于好奇,我将另一个(更简单的)迭代算法与 Peter 的版本进行了比较(顺便说一句,您需要将 i < n 更改为 i <= n 以获得正确的 46 斐波那契)。有趣的是,如果不使用已编译的变体,main.go 的顺序很重要。第二个函数调用速度更快。我们需要使用基准来获得客观的结果,就像这样:

    go test -bench .
    BenchmarkFibIt-4        100000000               18.5 ns/op
    BenchmarkFibP-4         50000000                29.1 ns/op
    BenchmarkFib-4                 1        12008314197 ns/op
    

    通过不使用变量f,而是直接使用x,它变得更快了一点;-) 令我惊讶的是,运行main.go 的未编译变体几乎与已编译变体一样快,有时甚至更快!

    到目前为止,我的结论是,只要 Go 中没有尾调用优化,我们可能应该主要避免递归而支持迭代算法。

    main.go:

    package main
    
    import (
        "fmt"
        "log"
        "time"
    )
    
    func fib(n int) uint64 {
        if n <= 1 {
            return 1
        }
        return fib(n-1) + fib(n-2)
    }
    
    func fibIt(n int) uint64 {
        var x, y uint64
        x, y = 0, 1
        for i := 0; i < n; i++ {
            // c <- x
            x, y = y, x+y
        }
        return x
    }
    
    func fibP(n int) uint64 {
        f := uint64(0)
        a, b := uint64(0), uint64(1)
        for i := 0; i <= n; i++ {
            f, a, b = a, b, a+b
            if a > b {
                break
            }
        }
        return f
    }
    
    func main() {
        var start time.Time
        var elapsed time.Duration
    
        start = time.Now()
        fibIt(46)
        elapsed = time.Since(start)
        fmt.Println("Iterative Fibonacci of 46 took", elapsed)
    
        start = time.Now()
        fibP(46)
        elapsed = time.Since(start)
        fmt.Println("Peter's Iterative Fibonacci of 46 took", elapsed)
    
        start = time.Now()
        fib(46)
        elapsed = time.Since(start)
        fmt.Println("Recursive Fibonacci of 46 took", elapsed)
    }
    

    main_test.go:

    package main
    
    import (
        "testing"
    )
    
    func BenchmarkFibIt(b *testing.B) {
        for i := 0; i < b.N; i++ {
            fibIt(46)
        }
    }
    
    func BenchmarkFibP(b *testing.B) {
        for i := 0; i < b.N; i++ {
            fibP(46)
        }
    }
    
    func BenchmarkFib(b *testing.B) {
        for i := 0; i < b.N; i++ {
            fib(46)
        }
    }
    

    【讨论】:

      【解决方案2】:

      原因是递归计算的组合爆炸。在算法 101 中,他们通常会解释为什么 Dru Jensen 的递归算法是计算斐波那契数的糟糕方法:http://www.cs.toronto.edu/~gfb/csc104/2016W/Lectures/CSC104.2016W.Week-7.Lecture.Fibonacci.I.pdffib 过程每次被调用时都会调用自己两次。按照设计,Go 没有尾递归:Tail call。按照设计,Go 从每个 goroutine 的一个非常小的堆栈开始,它必须爆炸式增长。没有 Go 程序员愿意使用这种算法,它比下一个最慢的算法慢 382,​​358,169 倍,比最快的慢 18,593,103,127 倍,因此会牺牲其他地方的性能的优化是没有意义的。

      以下是一些 Go 基准测试结果:

      $ go test fib_test.go -bench=.
      BenchmarkDruJensen-8             1    9482482595 ns/op
      BenchmarkPeterSO1-8       50000000            24.8 ns/op
      BenchmarkPeterSO2-8     2000000000             0.51 ns/op
      

      fib_test.go:

      package main
      
      import (
          "fmt"
          "testing"
      )
      
      // Dru Jensen: https://github.com/drujensen/fib
      func fib(n uint64) uint64 {
          if n <= 1 {
              return 1
          } else {
              return fib(n-1) + fib(n-2)
          }
      }
      
      func BenchmarkDruJensen(b *testing.B) {
          for i := 0; i < b.N; i++ {
              fib(46)
          }
      }
      
      // PeterSO
      func fibonacci1(n int) uint64 {
          f := uint64(0)
          a, b := uint64(0), uint64(1)
          for i := 0; i < n; i++ {
              f, a, b = a, b, a+b
              if a > b {
                  break
              }
          }
          return f
      }
      
      func BenchmarkPeterSO1(b *testing.B) {
          for i := 0; i < b.N; i++ {
              fibonacci1(46)
          }
      }
      
      var fibonaccis = []uint64{
          0,
          1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597,
          2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418,
          317811, 514229, 832040, 1346269, 2178309, 3524578, 5702887, 9227465,
          14930352, 24157817, 39088169, 63245986, 102334155, 165580141,
          267914296, 433494437, 701408733, 1134903170, 1836311903, 2971215073,
          4807526976, 7778742049, 12586269025, 20365011074, 32951280099,
          53316291173, 86267571272, 139583862445, 225851433717, 365435296162,
          591286729879, 956722026041, 1548008755920, 2504730781961, 4052739537881,
          6557470319842, 10610209857723, 17167680177565, 27777890035288,
          44945570212853, 72723460248141, 117669030460994, 190392490709135,
          308061521170129, 498454011879264, 806515533049393, 1304969544928657,
          2111485077978050, 3416454622906707, 5527939700884757, 8944394323791464,
          14472334024676221, 23416728348467685, 37889062373143906,
          61305790721611591, 99194853094755497, 160500643816367088,
          259695496911122585, 420196140727489673, 679891637638612258,
          1100087778366101931, 1779979416004714189, 2880067194370816120,
          4660046610375530309, 7540113804746346429, 12200160415121876738,
      }
      
      // PeterSO
      func fibonacci2(n int) uint64 {
          return fibonaccis[n]
      }
      
      func BenchmarkPeterSO2(b *testing.B) {
          for i := 0; i < b.N; i++ {
              fibonacci2(46)
          }
      }
      

      【讨论】:

      • 这似乎没有解决所问的问题,即“为什么 相对 慢”(相对于链接存储库中的其他语言)
      • 我猜,这意味着 C、C++、Swift 等实际上确实有尾递归,因此在同一时间范围内。这将导致一个问题,为什么 Go 直到今天才拥有它。 Ardan Labs 提到未来可能会有尾调用优化:goinggo.net/2013/09/recursion-and-tail-calls-in-go_26.html
      • 我认为答案解决了为什么。至少它声明 goroutines 以短堆栈大小开始,并且可能必须快速扩展它们,据我了解,这是相对繁重的操作。
      猜你喜欢
      • 2012-03-28
      • 2010-12-03
      • 2021-04-18
      • 2014-04-02
      • 2012-12-29
      • 1970-01-01
      • 2017-11-18
      • 2014-01-08
      相关资源
      最近更新 更多