2017-07-17 87 views
-1

对于下列功能:性能降低与函数调用

func CycleClock(c *ballclock.Clock) int { 
    for i := 0; i < fiveMinutesPerDay; i++ { 
     c.TickFive() 
    } 

    return 1 + CalculateBallCycle(append([]int{}, c.BallQueue...)) 
} 

其中c.BallQueue被定义为[]intCalculateBallCycle被定义为func CalculateBallCycle(s []int) int。我在for循环和return声明之间的性能下降很大。

我写了下面的基准测试。第一基准的整函数,所述第二基准的for循环,而第三基准的CalculateBallCycle功能:

func BenchmarkCycleClock(b *testing.B) { 
    for i := ballclock.MinBalls; i <= ballclock.MaxBalls; i++ { 
     j := i 
     b.Run("BallCount="+strconv.Itoa(i), func(b *testing.B) { 
      for n := 0; n < b.N; n++ { 
       c, _ := ballclock.NewClock(j) 

       CycleClock(c) 
      } 
     }) 
    } 
} 

func BenchmarkCycle24(b *testing.B) { 
    for i := ballclock.MinBalls; i <= ballclock.MaxBalls; i++ { 
     j := i 
     b.Run("BallCount="+strconv.Itoa(i), func(b *testing.B) { 
      for n := 0; n < b.N; n++ { 
       c, _ := ballclock.NewClock(j) 

       for k := 0; k < fiveMinutesPerDay; k++ { 
        c.TickFive() 
       } 
      } 
     }) 
    } 
} 

func BenchmarkCalculateBallCycle123(b *testing.B) { 
    m := []int{8, 62, 42, 87, 108, 35, 17, 6, 22, 75, 116, 112, 39, 119, 52, 60, 30, 88, 56, 36, 38, 26, 51, 31, 55, 120, 33, 99, 111, 24, 45, 21, 23, 34, 43, 41, 67, 65, 66, 85, 82, 89, 9, 25, 109, 47, 40, 0, 83, 46, 73, 13, 12, 63, 15, 90, 121, 2, 69, 53, 28, 72, 97, 3, 4, 94, 106, 61, 96, 18, 80, 74, 44, 84, 107, 98, 93, 103, 5, 91, 32, 76, 20, 68, 81, 95, 29, 27, 86, 104, 7, 64, 113, 78, 105, 58, 118, 117, 50, 70, 10, 101, 110, 19, 1, 115, 102, 71, 79, 57, 77, 122, 48, 114, 54, 37, 59, 49, 100, 11, 14, 92, 16} 

    for n := 0; n < b.N; n++ { 
     CalculateBallCycle(m) 
    } 
} 

使用123个球,这给出以下结果:

BenchmarkCycleClock/BallCount=123-8     200   9254136 ns/op 
BenchmarkCycle24/BallCount=123-8     200000    7610 ns/op 
BenchmarkCalculateBallCycle123-8     3000000    456 ns/op 

寻找这个,基准之间存在巨大差异。我预计第一个基准大约需要~8000 ns/op,因为这将是各部分的总和。

Here是github存储库。

编辑:

我发现,从基准,从运行的程序,结果的结果差异很大。我采取了什么@yazgazan发现并main.go模拟修改了基准功能多少有些BenchmarkCalculateBallCycle123main_test.go

func Benchmark() { 
    for i := ballclock.MinBalls; i <= ballclock.MaxBalls; i++ { 
     if i != 123 { 
      continue 
     } 

     start := time.Now() 

     t := CalculateBallCycle([]int{8, 62, 42, 87, 108, 35, 17, 6, 22, 75, 116, 112, 39, 119, 52, 60, 30, 88, 56, 36, 38, 26, 51, 31, 55, 120, 33, 99, 111, 24, 45, 21, 23, 34, 43, 41, 67, 65, 66, 85, 82, 89, 9, 25, 109, 47, 40, 0, 83, 46, 73, 13, 12, 63, 15, 90, 121, 2, 69, 53, 28, 72, 97, 3, 4, 94, 106, 61, 96, 18, 80, 74, 44, 84, 107, 98, 93, 103, 5, 91, 32, 76, 20, 68, 81, 95, 29, 27, 86, 104, 7, 64, 113, 78, 105, 58, 118, 117, 50, 70, 10, 101, 110, 19, 1, 115, 102, 71, 79, 57, 77, 122, 48, 114, 54, 37, 59, 49, 100, 11, 14, 92, 16}) 

     duration := time.Since(start) 

     fmt.Printf("Ballclock with %v balls took %s;\n", i, duration) 
    } 
} 

这给了输出:

Ballclock with 123 balls took 11.86748ms; 

正如你所看到的,总时间为11.86 ms,所有这些都花在CalculateBallCycle函数中。当运行的程序运行在11867480 ms/op附近时,什么会导致基准运行在456 ns/op

+1

前两个测试功能有嵌套的for循环,而最后一个不... –

+0

@ dev.bmax这不是一个问题。基准测试在'b.Run()'调用中完成。 – JRLambert

+0

@JRLambert你可以做一个能够重现这个问题的主旨吗?或者告诉我们'TickFive'中发生了什么事情,以便我们可以尝试并重现它? – yazgazan

回答

2

你写CalcualteBallCycle()修改设计切片。

我不能到这种做法的正确性说话,但它为什么BenchmarkCalculateBallCycle123基准时间是如此不同。

在第一次运行它意料之中的事情,但在随后的运行中会产生完全不同的,因为你传递不同的数据作为输入。

基准此修改后的代码:

func BenchmarkCalculateBallCycle123v2(b *testing.B) { 
    m := []int{8, 62, 42, 87, 108, 35, 17, 6, 22, 75, 116, 112, 39, 119, 52, 60, 30, 88, 56, 36, 38, 26, 51, 31, 55, 120, 33, 99, 111, 24, 45, 21, 23, 34, 43, 41, 67, 65, 66, 85, 82, 89, 9, 25, 109, 47, 40, 0, 83, 46, 73, 13, 12, 63, 15, 90, 121, 2, 69, 53, 28, 72, 97, 3, 4, 94, 106, 61, 96, 18, 80, 74, 44, 84, 107, 98, 93, 103, 5, 91, 32, 76, 20, 68, 81, 95, 29, 27, 86, 104, 7, 64, 113, 78, 105, 58, 118, 117, 50, 70, 10, 101, 110, 19, 1, 115, 102, 71, 79, 57, 77, 122, 48, 114, 54, 37, 59, 49, 100, 11, 14, 92, 16} 
    for n := 0; n < b.N; n++ { 
     tmp := append([]int{}, m...) 
     CalculateBallCycle(tmp) 
    } 
} 

这工作,解决此问题通过使m的副本,让CalculateBallCycle修改一个本地副本。

运行时间变得更像人:

BenchmarkCalculateBallCycle123-8   3000000   500 ns/op 
BenchmarkCalculateBallCycle123v2-8   100  10483347 ns/op 
+0

*脸部的Palm *,谢谢。我应该意识到这一切正在发生。 – JRLambert

0

在您的CycleClock功能中,您正在复制c.BallQueue切片。您可以显著使用CalculateBallCycle(c.BallQueue)代替(假设CalculateBallCycle不修改切片)

例如提高性能:

func Sum(values []int) int { 
    sum := 0 
    for _, v := range values { 
     sum += v 
    } 

    return sum 
} 

func BenchmarkNoCopy(b *testing.B) { 
    for n := 0; n < b.N; n++ { 
     Sum(m) 
    } 
} 

func BenchmarkWithCopy(b *testing.B) { 
    for n := 0; n < b.N; n++ { 
     Sum(append([]int{}, m...)) 
    } 
} 

// BenchmarkNoCopy-4  20000000   73.5 ns/op 
// BenchmarkWithCopy-4  5000000   306 ns/op 
// PASS 
+0

我做了改变,并没有提高,但还没到那个我希望的程度。随着变化,现在的基准标记是'8930347 ns/op',仍然比我所希望的大约高出1000倍。 – JRLambert

+0

此外,'CalculateBallCycle'将修改切片,但这是预期和想要的副作用。 – JRLambert

0

有在测试中一个微妙的错误。

两种方法BenchmarkCycleClockBenchmarkCycle24在for循环中运行基准,将封闭传递给b.Run。在这些关闭的内部,您使用如下这样的循环变量i来初始化时钟:ballclock.NewClock(i)

问题是,您的匿名函数的所有实例共享相同的变量。并且,当测试运行器运行该函数时,循环将结束,所有时钟将使用相同的值进行初始化:ballclock.MaxBalls

这可以使用一个局部变量修正:

for i := ballclock.MinBalls; i <= ballclock.MaxBalls; i++ { 
    i := i 
    b.Run("BallCount="+strconv.Itoa(i), func(b *testing.B) { 
     for n := 0; n < b.N; n++ { 
      c, _ := ballclock.NewClock(i) 

      CycleClock(c) 
     } 
    }) 
} 

线i := ii的电流值(你的匿名函数的每个实例不同)的副本。

+0

感谢您指出这一点,但这只是在使用'testing.T'并在提供的'func'中调用't.Parallel()'时的一个问题。 'b.Run()'按顺序运行,所以传递给'b.Run()'的'func'中的'i'的值是正确的值。将基准更改为使用局部变量不会影响基准的结果。 [文件](https://golang.org/pkg/testing/#hdr-Subtests_and_Sub_benchmarks) – JRLambert