将一个元素附加到nil切片会将容量增加两倍

时间:2016-07-23 16:13:48

标签: go slice

我有一个零片:

<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
A <input data-bind="value: a, valueUpdate: 'afterkeydown'"> = (B x C) <span data-bind="text: b()*c()"></span><br />
B <input data-bind="value: b, valueUpdate: 'afterkeydown'"> = (A / C) <span data-bind="text: a()/c()"></span><br />
C <input data-bind="value: c, valueUpdate: 'afterkeydown'"> = (A / B) <span data-bind="text: a()/b()"></span><br />

我将一个元素添加到:

var s1 []int // len(s1) == 0, cap(s1) == 0

为什么在nil切片上附加一个元素会使容量增加2?

使用s2 := append(s1, 1) // len(s2) == 1, cap(s2) == 2 打印切片显示以下内容:

fmt.Printf

我也很困惑为什么重新切片[] // s1 [1] // s2 会显示一个零,这个零不在原始切片中,也不会附加到原始切片中:

s2[0:2]

4 个答案:

答案 0 :(得分:15)

Go可以免费为您提供超出您要求的容量。这通过减少所需的分配(以及可能的复制)数量来提高性能。容量只是在需要进行另一次分配之前预留的空间量。

如果你向这个切片添加5个元素,至少在我的实验中,容量是8.这不应该是令人惊讶的,但也不应该依赖。在不同平台或编译器的不同版本上,只要容量“足够大”(等于或大于长度),实际结果可能会有所不同。

切片的上部索引范围是defined as its capacity

  

对于数组或字符串,如果0 <=低&lt; =高&lt; = len(a),则索引在范围内,否则它们超出范围。对于切片,上限索引是切片容量上限(a)而不是长度。常量索引必须是非负的,并且可以通过int类型的值表示;对于数组或常量字符串,常量索引也必须在范围内。如果两个指数都是常数,则它们必须满足低<=高。如果索引在运行时超出范围,则会发生运行时混乱。

这就是为什么阅读超过长度不会导致恐慌。即便如此,你也不应该把那些零视为切片的一部分。它们可以通过切片进行索引,但fmt.Printf(s2)将无法正确显示它们,因为它们不是切片的一部分。不要这样下标。

一般来说,你想看长度,而不是容量。容量主要是可读的,有助于性能优化。

答案 1 :(得分:4)

我认为,这里有一些关于容量和长度的混淆。当您打印切片并在切片中看到零个或一个元素时,您正在查看的是长度,即切片实际包含的值的数量。除非您使用内置的cap()查找,否则基础数组的容量通常是隐藏的。

在引擎盖下,切片实际上是固定长度的数组。当切片中的空间不足时,Go必须通过创建一个新的(更长的)数组并将所有值从旧数组中复制来使其变大。如果您要为切片添加大量值,那么每次为新值分配内存(并复制所有旧值)将会非常缓慢,因此有时候Go会假设您要去添加更多元素并继续进行并分配比所需更多的内存,这样您就不必经常复制内容。这个额外的内存可以在下次调用append时使用,并且可以在必须扩展之前存储在切片中的值的数量称为 capacity 。换句话说,切片的容量是切片支持阵列的长度,切片的长度与容量无关。

当您向切片附加单个值时,Go会看到它必须为此值分配空间,因此它会分配两倍于实际需要的空间量,将长度增加1,将容量增加2。

你提到的切片是因为切片作用于底层数组:Go允许你切片超出切片的长度,你只是不能超越其容量(底层数组的长度)。例如,让我们在一个简单的nil片上尝试一些事情:

var s []int
fmt.Println(len(s), cap(s)) // Prints `0 0` because this is a nil slice
s = append(s, 1)
fmt.Println(len(s), cap(s)) // Prints `1 2`
fmt.Println(s[0:2])         // Prints `[1 0]` (only the first value is part of the slice, the second position in the underlying array is a zero value that is waiting to be used when the slices length grows)
fmt.Println(s[0:3])         // panic: slice bounds out of range (because we've exceeded the slices capacity)

答案 2 :(得分:1)

切片零值

切片的零值为零。但这并不意味着你无法用它做任何事情。在Go中,当值为nil时,实际上可以使用类型。例如,即使指针接收器为零,也可以调用struct方法。这是一个例子:

package main

import "fmt"

type foo struct {
}

func (f *foo) bar() {
    fmt.Println(1)
}

func main() {
    var f *foo
    fmt.Println(f)
    f.bar()
}

playground

同样适用于切片。 lencapappend即使您通过了零片,也都可以正常工作。在append的情况下,它基本上为您创建一个新切片,指向一个包含该值的数组。

切片容量增长

添加元素并且需要为其分配更多空间时,不会仅为一个元素分配空间。这非常缺乏。相反,你分配的不仅仅是实际需要的。

究竟分配了多少取决于实现,并且未在语言规范中定义。通常容量加倍但是在Go的情况下,至少从v1.5开始,它向上舍入到分配的内存块。您可以找到源代码here的链接。

切片过去的长度

实际上支持切片过去的长度。您可以切片超出切片的长度,但不能切片超出容量:

  

早些时候我们将s切成比其容量短的长度。我们可以成长   通过再次切片来达到它的能力:

     

切片不能超出其容量。试图这样做会   导致运行时混乱,就像在a的边界之外索引时一样   切片或数组。同样,切片不能在零以下重新切片   访问数组中的早期元素。

https://blog.golang.org/go-slices-usage-and-internals

在你的情况下,底层数组的容量为2.你只附加一个元素,所以另一个元素等于它的零值。当你重新过去长度时,Go可以识别出切片已经具有所需的容量。所以它返回一个指向同一个数组的新切片,但长度值设置为2.这是一个如何工作的例子:

package main

import "fmt"

func main() {
    var s []int
    s = append(s, 1, 2)
    fmt.Println(s, cap(s))

    s = s[:1]
    fmt.Println(s, cap(s))

    s = s[:2]
    fmt.Println(s, cap(s))
}

playground

会打印

  

[1 2] 2
  [1] 2
  [1 2] 2

你可以看到,即使我重新缩小到较小的长度,第二个元素仍然保留。

答案 3 :(得分:0)

容量增长不受用户控制

append(s S, x ...T) S  // T is the element type of S 
     

如果s的容量不足以容纳附加值,   append分配一个适合的新的,足够大的底层数组   现有的切片元素和附加值。除此以外,   append重新使用底层数组。

参考:https://golang.org/ref/spec#Appending_and_copying_slices
并参见:https://golang.org/doc/effective_go.html#append

它不会增加2(性能优化):
测试样本代码,初始容量为5个字节,然后是16而不是10(参见注释输出):

package main

import "fmt"

func main() {
    s := []byte{1, 2, 3, 4, 5}
    fmt.Println(cap(s)) // 5
    s = append(s, s...)
    fmt.Println(cap(s)) // 16
}

测试示例代码(带注释输出):

package main

import (
    "fmt"
)

func main() {
    s := []int{0}
    fmt.Println(cap(s)) // 1
    s = append(s, s...)
    fmt.Println(cap(s)) // 2
}

测试示例代码(带注释输出):

package main

import (
    "fmt"
)

func main() {
    s := []int{}
    fmt.Println(cap(s)) // 0
    s = append(s, 1)
    fmt.Println(cap(s)) // 1
}

使用nil切片测试样本代码(带注释输出):

package main

import (
    "fmt"
)

func main() {
    var s []int
    fmt.Println(cap(s)) // 0
    s = append(s, 1)
    fmt.Println(cap(s)) // 1
}

您的示例代码(带注释输出):

package main

import "fmt"

func main() {
    var s1 []int
    s2 := append(s1, 1)
    fmt.Println(cap(s1)) // 0
    fmt.Println(cap(s2)) // 1
}

测试5个整数的示例代码(带注释输出):

package main

import "fmt"

func main() {
    s := []int{1, 2, 3, 4, 5}
    fmt.Println(cap(s)) // 5
    s = append(s, s...)
    fmt.Println(cap(s)) // 10
}

您无法访问s2[1]之类切片的未初始化索引:
panic:运行时错误:切片范围超出范围:
测试示例代码(带注释输出):

package main

import "fmt"

func main() {
    var s1 []int
    s2 := append(s1, 1)

    fmt.Println(cap(s1)) // 0
    fmt.Println(cap(s2)) // 1
    fmt.Println(s1)      // []
    fmt.Println(s2)      // [1]

    //fmt.Println(s2[0:2]) //panic: runtime error: slice bounds out of range
    //fmt.Println(s2[1])   //panic: runtime error: slice bounds out of range

}
  

Bounds Checking Elimination(或BCE)是删除的一般术语   冗余绑定检查。通常一个go程序会在a时出现恐慌   切片或字符串在其边界之外访问。那里有两个   绑定检查的类型:用于索引(a [i])和切片(a [i:j])。   go编译器在每次访问时插入这些边界检查,但是在   大多数情况下,它们不是必需的,并且根据上下文是多余的。

     

绑定检查很重要,因为它提供了防御   缓冲区溢出攻击并尽早发现常见的编程错误。   BCE很重要,因为:它加速了代码,制作了二进制文件   小。如果通过绑定检查减慢二进制文件然后开发人员   将有动机禁用绑定检查(使用-gcflags = -B)。

ref