Go:通过切片切片访问数组时出现意外的性能(2D切片)

时间:2015-05-10 16:57:23

标签: performance memory-management optimization multidimensional-array go

我在Go中使用矩阵乘法进行了一些性能实验,并遇到了一些意想不到的结果。

版本1:

func newMatrix(n int) [][]int {
    m := make([][]int, n)
    buf := make([]int, n*n)

    for i := range m {
        m[i] = buf[i*n : (i+1)*n]
    }

    return m
}

func mult1(m1, m2, res [][]int) [][]int {
    for i := range m1 {
        for k := range m1[0] {
            for j := range m2[0] {
                res[i][j] += m1[i][k] * m2[k][j]
            }
        }
    }

    return res
}

从线性数组中,我创建了多个代表矩阵行的切片。

第2版:

func mult2(m1, m2, res []int, n int) []int {
    for i := 0; i < n; i++ {
        for k := 0; k < n; k++ {
            for j := 0; j < n; j++ {
                res[i*n+j] += m1[i*n+k] * m2[k*n+j]
            }
        }
    }

    return res
}

在这个版本中,我只是使用一个线性数组并从乘法索引到它。

乘以2个2048x2048矩阵可得到以下执行时间:

 version 1: 35.550813801s
 version 2: 19.090223468s

版本2几乎快两倍。

我使用下面的方法进行测量:

start := time.Now()
mult(m1, m2, m3)
stop := time.Now()

我知道使用切片会产生另一层间接性,这可能会影响缓存性能,但我并不认为它会有如此大的差异。很遗憾,我还没有找到适合Mac的好工具,可以分析Go中的缓存效率,所以我无法确定这是否会导致性能差异。

所以我想我问的是这种预期的行为还是我缺少的东西?

软件和硬件: 去版本1.4.2 darwin / amd64; OS X 10.10.3; 2 GHz四核i7。

2 个答案:

答案 0 :(得分:6)

版本1代码中的主要问题似乎是间接寻址。尽管两个版本中矩阵的内存布局相同,但使用间接寻址可能会导致:

  • 针对相同代码生成更多指令。编译器可能无法确定何时使用SIMD指令的打包版本(例如SSE,AVX)。您可以通过转储汇编代码来验证这一点,查找XMM或YMM寄存器,并检查寄存器上的指令是否已打包。
  • 您使编译器难以添加软件预取。由于间接寻址,编译器很难检测如何添加软件预取。您可以在汇编代码中查找vprefetch指令。
  • 由于间接寻址,硬件预取器的效率也会降低。首先需要访问行起始地址然后访问行元素,因此很难观察到硬件预取器应该只获取连续的地址。这只能通过像perf这样的分析来衡量。

因此,对于版本1,间接寻址是主要问题。我还建议在多次迭代中运行2个代码,以便r 实现缓存加温惩罚,由于我在上面的解释,版本1可能会更高。

答案 1 :(得分:-1)

不幸的是,我没有足够的声誉将其作为评论,但除了 VAndrei 的要点之外,值得注意的是,两个提供的示例使用for循环不同。第一个示例如何在s/i := range m1/i := 0; i < n; i++/之后执行?

检查&#34;列出mult1&#34;和&#34;列出mult2&#34;输出看起来像在pprof中。 有很好的教程可以非常快速地开始使用Go的pprof:Profiling Go Programs By Russ Cox