程序员社区

Go切片及其内存管理

【译文】原文地址
Go切片的使用很方便,内部实现也很有趣。有很多关于slice的文档和博客,都对其概念进行了讲解包括内部结构。本文更多地关注切片内存管理。让我们从元素的复制和删除操作开始。

复制

由于内置函数copy,Go允许开发人员复制切片。当然append函数用于向切片中追加元素,也可以实现切片的复制。如下两个例子所示:

func main() {
   a := []int{4, 2, 1}

   b := make([]int, len(a))
   copy(b, a)

   c := append([]int{}, a...)
}

新创建的切片b和c底层实质上是包含相同值的数组。
这两个函数可以在其他场景中使用,例如删除切片中的一个元素或部分元素。如下是一个使用copy函数删除切片中第二个元素的例子:

package main

func main() {
    a := []int{4, 2, 1}

    copy(a[1:], a[2:])
    a = a[:len(a)-1]
}

使用append函数实现:

func main() {
    a := []int{4, 2, 1}

    a = append(a[:1], a[2:]...)
}

这种实现是可以的是因为Go规范保证了不管函数参数是否有内存重叠:

内置函数append和copy支持常规的切片操作。对于两个函数,其结果与参数引用的内存是否重叠是无关的。

第一个例子生成的汇编代码显示了底层函数管理内存拷贝:

[...]
0x00a3 00163 (main.go:8)    CALL   runtime.memmove(SB)
[...]
0x00f4 00244 (main.go:10)   CALL   runtime.memmove(SB)

这个实现依赖于memmove函数能处理内存重叠。如下是一个带有字节切片的例子能说明重叠问题:

func main() {
   a := []byte("hello")
   copy(a[2:], a)
}

复制内存从a的第一个字节由前向后到最后一个字节重叠。

Go切片及其内存管理插图
正向复制

memmove通过反向复制的能力解决了这个问题:

Go切片及其内存管理插图1
反向复制

反向复制解决了重叠问题,并满足了Go规范中提供的保证。

memmove在汇编中的实现实际上比这个复杂的多。它处理前向和反向复制,并不总是需要循环,并且可以用很少的指令就能处理复制

重置切片

重用一个切片意味着首先要清除其内容。Go提供了编译器优化,以便更快清除一个切片。如下是一个清除整数切片的例子:

func main() {
   a := []int{4, 2, 1}

   for i := range a {
      a[i] = 0
   }
}

在切片上循环逐一清除元素可能会很麻烦。为了解决这个问题,Go 1.5提供了一个优化,能够识别这种循环,并通过调用内存清理函数来替换它。这可以从汇编代码中确认:

0x0047 00071 (main.go:6)    CALL   runtime.memclrNoHeapPointers(SB)

后缀noheappointer指的是不包含任何指针的片。同样的函数也适用于包含指针的切片;在这种情况下,编译器将调用memclrHasPointer。
这种优化大大加快了清理速度。下面是一个包含6、16、64和256个元素的基准测试:

Go切片及其内存管理插图2

分配与复制

当分配一个切片时,Go首选分配内存,然后通过之前看到的函数memclr*将其置零。

Go切片及其内存管理插图3

然而,如果下一条指令如果是一个现有切片的复制,清除操作只会浪费时间:

Go切片及其内存管理插图4

因此,对这种情况的一种优化,切片在立即复制的情况下删除内存清理部分:

Go切片及其内存管理插图5

这将加快分配速度。以下是一个基准:

func BenchmarkAllocAndCopy(b *testing.B) {
   a := make([]int, 250)
   for k, _ := range a  {
      a[k] = k*2
   }
   b.ResetTimer()

   for i := 0;i < b.N; i++  {
      b := make([]int, len(a))
      copy(b, a)
   }
}

以下是基准测试结果:

name               old time/op  new time/op  delta
AllocAndCopy500-8   531ns ± 1%   502ns ± 4%   -5.44%
AllocAndCopy250-8   284ns ± 1%   272ns ± 5%   -4.10%
AllocAndCopy50-8   78.5ns ± 3%  72.1ns ± 1%   -8.16%
AllocAndCopy5-8    30.6ns ± 1%  26.1ns ± 1%  -14.80%

使用copy触发这种特殊分配的唯一条件是确保make和copy两条指令相互跟随。

赞(0) 打赏
未经允许不得转载:IDEA激活码 » Go切片及其内存管理

一个分享Java & Python知识的社区