我刚开始使用Go,我有一种情况需要制作一组实体,其大小/长度仅在运行时已知。我首先想到使用一个列表是一个很好的拟合,但很快就意识到切片是Go中惯用的数据结构。 好奇,我写了以下基准
lst = ['b-3', 'a-2', 'c-4', 'd-2']
res = sorted(lst, key=lambda x: tuple(f(a) for f, a in zip((int, str), reversed(x.split('-')))))
print(res)
['a-2', 'd-2', 'b-3', 'c-4']
给了我
package main
import (
"container/list"
"testing"
)
var N = 10000000
func BenchmarkSlices(B *testing.B) {
s := make([]int, 1)
for i := 0; i < N; i += 1 {
s = append(s, i)
}
}
func BenchmarkLists(B *testing.B) {
l := list.New()
for i := 0; i < N; i += 1 {
l.PushBack(i)
}
}
鉴于BenchmarkSlices-4 2000000000 0.03 ns/op
BenchmarkLists-4 1 1665489308 ns/op
将创建一个新数组并在旧数组变满时将所有数据从旧数组复制到新数组,我希望列表在上面的示例中比切片执行得更好。但是,我的期望显然是错误的,我正在努力理解为什么。
为了更好地了解append
在需要时创建新数组的方式,我写了以下内容:
append
给了我
package main
import "fmt"
func describe(s []int) {
fmt.Printf("len = %d, cap = %d\n", len(s), cap(s))
}
func main() {
s := make([]int, 2)
for i := 0; i < 15; i += 1 {
fmt.Println(i)
describe(s)
s = append(s, i)
}
}
我目前唯一的猜测就是为什么切片比列表表现更好的是为一个大小为double的新数组分配内存并且复制所有数据比每次插入时为单个元素分配内存要快。 我猜是正确的吗?我错过了什么吗?
答案 0 :(得分:4)
您运行基准错误。您应首先设置初始数据结构,然后像testing.B
实例所指示的那样多次运行基准测试操作。
我用以下代码替换了代码:
var N = 1
func BenchmarkSlices(B *testing.B) {
s := make([]int, 1)
for n := 0; n < B.N; n++ {
for i := 0; i < N; i++ {
s = append(s, i)
}
}
}
func BenchmarkLists(B *testing.B) {
l := list.New()
for n := 0; n < B.N; n++ {
for i := 0; i < N; i++ {
l.PushBack(i)
}
}
}
得到了这个结果:
BenchmarkSlices-4 100000000 14.3 ns/op
BenchmarkLists-4 5000000 275 ns/op
至少这次差异似乎是合理的,而不是万亿次。
请注意,我还将N
的值替换为1,以便ns/op
实际上代表nanoseconds per operation
而不是nanoseconds per N operations
。但是,这也可能会影响结果。
现在回答你的问题:与在Go中实现的链接列表相比,只需将另一个int添加到预先分配的切片中就会产生额外的成本:list方法需要创建一个新的Element,包装你的interface{}
中的值并重新分配一些指针。
同时附加到没有超出其容量的切片将导致CPU级别的一些指令:将int移动到存储器位置,增加切片的长度,并且您已完成。
还有一个事实是,底层分配器可能会重新分配切片,从而无需复制现有的底层数组元素。