Go:多个len()调用vs性能?

时间:2014-10-29 15:36:36

标签: algorithm go

目前我正在实施一些排序算法。由于它具有算法的性质,因此使用len()方法对一些数组/切片的长度进行了大量调用。

现在,给出以下代码(Mergesort算法的一部分):

  for len(left) > 0 || len(right) > 0 {
        if len(left) > 0 && len(right) > 0 {
            if left[0] <= right[0] {
                result = append(result, left[0])
                left = left[1:len(left)]
            } else {
                result = append(result, right[0])
                right = right[1:len(right)]
            }
        } else if len(left) > 0 {
            result = append(result, left[0])
            left = left[1:len(left)]
        } else if len(right) > 0 {
            result = append(result, right[0])
            right = right[1:len(right)]
        }
    }

我的问题是:这些多个len()调用是否会对算法的性能产生负面影响?为rightleft切片的长度制作临时变量是否更好?或者编译器本身会这样做吗?

2 个答案:

答案 0 :(得分:34)

有两种情况:

  • 本地切片:长度将被缓存且没有开销
  • 全局切片或传递(通过引用):长度无法缓存且存在开销

本地切片没有开销

对于本地定义的切片,长度被缓存,因此没有运行时开销。您可以在以下程序的程序集中看到这一点:

func generateSlice(x int) []int {
    return make([]int, x)
}

func main() {
    x := generateSlice(10)
    println(len(x))
}

使用go tool 6g -S test.go编译,除其他外,还会产生以下几行:

MOVQ    "".x+40(SP),BX
MOVQ    BX,(SP)
// ...
CALL    ,runtime.printint(SB)

这里发生的是第一行通过从x的开头获取位于40个字节的值来检索x的长度,最重要的是将此值缓存在BX中,然后用于len(x)的每次出现。偏移的原因是数组具有以下结构(source):

typedef struct
{               // must not move anything
    uchar   array[8];   // pointer to data
    uchar   nel[4];     // number of elements
    uchar   cap[4];     // allocated number of elements
} Array;

nellen()访问的内容。您也可以在code generation中看到这一点。

全局和引用的切片有开销

对于共享值,不可能缓存长度,因为编译器必须假设切片在调用之间发生变化。因此,编译器必须编写每次都直接访问length属性的代码。例如:

func accessLocal() int {
    a := make([]int, 1000) // local
    count := 0
    for i := 0; i < len(a); i++ {
        count += len(a)
    }
    return count
}

var ag = make([]int, 1000) // pseudo-code

func accessGlobal() int {
    count := 0
    for i := 0; i < len(ag); i++ {
        count += len(ag)
    }
    return count
}

比较两个函数的汇编产生了一个关键的区别,即一旦变量是全局的,就不再缓存对nel属性的访问,并且会有运行时开销:

// accessLocal
MOVQ    "".a+8048(SP),SI // cache length in SI
// ...
CMPQ    SI,AX            // i < len(a)
// ...
MOVQ    SI,BX
ADDQ    CX,BX
MOVQ    BX,CX            // count += len(a)

// accessGlobal
MOVQ    "".ag+8(SB),BX
CMPQ    BX,AX            // i < len(ag)
// ...
MOVQ    "".ag+8(SB),BX
ADDQ    CX,BX
MOVQ    BX,CX            // count += len(ag)

答案 1 :(得分:5)

尽管你得到了很好的答案,但如果不断地调用len(a),我的表现会变得更差,例如在这个测试中http://play.golang.org/p/fiP1Sy2Hfk

package main

import "testing"

func BenchmarkTest1(b *testing.B) {
    a := make([]int, 1000)
    for i := 0; i < b.N; i++ {
        count := 0
        for i := 0; i < len(a); i++ {
            count += len(a)
        }
    }
}

func BenchmarkTest2(b *testing.B) {
    a := make([]int, 1000)
    for i := 0; i < b.N; i++ {
        count := 0
        lena := len(a)
        for i := 0; i < lena; i++ {
            count += lena
        }
    }
}

当以go test -bench=.运行时,我得到:

BenchmarkTest1   5000000               668 ns/op
BenchmarkTest2   5000000               402 ns/op

所以这里显然有一个惩罚,可能是因为编译器在编译时进行了更糟糕的优化。