功能调用会降低性能

时间:2017-07-17 14:50:15

标签: optimization go slice

对于以下功能:

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中的基准函数,模仿BenchmarkCalculateBallCycle123中的main_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函数中。当正在运行的程序在456 ns/op左右运行时,什么会导致基准在11867480 ms/op中运行?

3 个答案:

答案 0 :(得分: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

答案 1 :(得分: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

答案 2 :(得分: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 := i存储当前值i的副本(对于您的匿名函数的每个实例都不同)。