目前我正在实施一些排序算法。由于它具有算法的性质,因此使用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()调用是否会对算法的性能产生负面影响?为right
和left
切片的长度制作临时变量是否更好?或者编译器本身会这样做吗?
答案 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;
nel
是len()
访问的内容。您也可以在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
所以这里显然有一个惩罚,可能是因为编译器在编译时进行了更糟糕的优化。