List 切片和容器/列表之间的差异

List 切片和容器/列表之间的差异,list,go,slice,List,Go,Slice,我刚刚开始使用Go,我有一种情况,我需要创建一组只有在运行时才知道其大小/长度的实体。我最初认为使用列表非常合适,但很快就意识到切片是Go中惯用的数据结构。 出于好奇,我编写了以下基准测试 package main import ( "container/list" "testing" ) var N = 10000000 func BenchmarkSlices(B *testing.B) { s := make([]int, 1) for i := 0;

我刚刚开始使用Go,我有一种情况,我需要创建一组只有在运行时才知道其大小/长度的实体。我最初认为使用列表非常合适,但很快就意识到切片是Go中惯用的数据结构。 出于好奇,我编写了以下基准测试

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)
    }
}
假设
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)
    }
}
对于切片为何比列表性能更好,我目前唯一的猜测是,为一个大小为两倍的新数组分配内存并复制所有数据比每次插入时为单个元素分配内存要快。
我的猜测正确吗?我遗漏了什么吗?

您运行的基准错误。您应该首先设置初始数据结构,然后按照
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)
        }
    }
}
至少这一次,这种差异似乎是合理的,而不是万亿倍

注意,我还将
N
的值替换为1,这样
ns/op
实际上意味着每个操作
纳秒
,而不是
每N个操作纳秒
。然而,这也可能影响结果

现在谈谈你的问题:与简单地向预先分配的片中添加另一个int相比,在Go中实现的链表要承受额外的成本:list方法需要创建一个新的,将值包装在
接口{}
中,并重新分配一些指针

同时,附加到一个没有最大化其容量的片上,只会在CPU级别产生一些指令:将int移到内存位置,增加片的长度,就完成了


还有一个事实,底层分配器可能会在适当的位置重新分配片,从而完全避免复制现有的底层数组元素。

您谈到的
append
创建新数组--
append
仅适用于片,而不适用于数组。我不太确定您在这里问什么。
container/list
提供了双链接列表。如果需要双链接列表,请使用此选项。如果您不需要双链接列表,请不要使用它。性能不应考虑在内。@Flimzy谢谢您的留言。据我所知,引擎盖下没有阵列就不会有切片。因此,如果我理解正确,
append
将创建一个数组和一个切片并返回后者。这是正确的吗?我没有深入讨论细节,因为我认为这与我实际提出的问题无关。谢谢你指出这个细节,不过,我的措辞确实会误导一些人。@Flimzy这也是我最初的立场。然而,通过阅读一些代码和文章,我感觉人们倾向于使用切片来解决问题,即使列表更适合。这让我有点困惑,这就是为什么我开始怀疑性能是否是人们倾向于尽可能多地选择切片的原因之一。如果列表在数据结构方面有意义的话,你认为我应该使用列表,而不使用切片,尽管它们(显然)是惯用的吗?我使用切片而不是双链接列表,因为我几乎从不需要双链接列表。尽管我在大学期间花了大量的时间讨论链表和双链表,但在我的编程生涯中,我只需要一个双链表,也许只有一次。答案很好,投票率很高,但我认为在你的基准测试中,你不应该将
N
设为1。如果
N
较大,它将强制在切片情况下进行一些重新分配,这将使比较更接近。这是相关的,因为问题特别提到了有兴趣。哦,我误读了你的代码,基准案例彼此模糊。我认为这一点也不合理——为什么不每次重新初始化列表/切片,并在每次基准测试运行中追加N(N个大的)次?您必须在ns/op的基准测试中保持一致才有意义。如果您想对“分配+N个附件”的时间进行基准测试,那么您完全可以这样做。如果你想知道某人跑得有多快,你不需要测量他们在10秒内跑了多远,然后看看他们跑100倍需要多长时间,然后计算他们的平均速度。他们跑得越远,速度就越慢,所以如果他们在前10秒跑得快,他们就有更长的距离可以跑,平均速度也会变慢。这正是您的基准测试(以及测试stdlib)在这里所做的。@justinas非常感谢您的回复和修复基准测试代码:)创建
元素
的需要是我在“每次插入时为单个元素分配内存”中提到的一部分。确认一下:你是说创建一个
元素
太重了,比创建一个新的切片并在切片最大化时复制所有数据要慢吗?或者它不是确定性的,人们不应该担心它?
0
len = 2, cap = 2
1
len = 3, cap = 4
2
len = 4, cap = 4
3
len = 5, cap = 8
4
len = 6, cap = 8
5
len = 7, cap = 8
6
len = 8, cap = 8
7
len = 9, cap = 16
8
len = 10, cap = 16
9
len = 11, cap = 16
10
len = 12, cap = 16
11
len = 13, cap = 16
12
len = 14, cap = 16
13
len = 15, cap = 16
14
len = 16, cap = 16
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