Golang中固定容量的切片与数组性能对比

Golang中固定容量的切片与数组性能对比 我很好奇,当我使用一个分配了固定容量的切片与一个固定数组相比,性能会受到什么影响。

我喜欢使用分配了固定容量的切片,然后利用 append 的便利性来填充这个切片……

slc := make([]uint, 0, 8) // 切片将始终需要 8 个元素
slc = append(slc, 6) // 追加数据 8 次

(在我的情况下)性能损失是否微不足道?

注意:我知道切片的容量不是固定的,但在我的代码中不会超过这个限制。

2 回复

你好 G4143,最近怎么样? 我创建了一个名为 “sliceBenchmark.go” 的文件,内容如下:

package main

const SLICE_CAPACITY = 8

func main() {
	fixedArray()
	withCapacity()
	withoutCapacity()
}

func fixedArray() {
	var slc [SLICE_CAPACITY]uint
	for i := 0; i < SLICE_CAPACITY; i++ {
		slc[i] = 6
	}
}
func withCapacity() {
	slc := make([]uint, 0, SLICE_CAPACITY)
	for i := 0; i < SLICE_CAPACITY; i++ {
		slc = append(slc, 6)
	}
}
func withoutCapacity() {
	slc := []uint{}
	for i := 0; i < SLICE_CAPACITY; i++ {
		slc = append(slc, 6)
	}
}

以及另一个包含基准测试的文件 “sliceBenchmark_test.go”:

package main

import (
	"testing"
)

func BenchmarkWithCapacity(b *testing.B) {
        for n := 0; n < b.N; n++ {
                withCapacity()
        }
}

func BenchmarkWithoutCapacity(b *testing.B) {
        for n := 0; n < b.N; n++ {
                withoutCapacity()
        }
}

func BenchmarkFixedArray(b *testing.B) {
        for n := 0; n < b.N; n++ {
                fixedArray()
        }
}

在我的笔记本电脑上,我得到了以下结果: | BenchmarkWithCapacity-4 | 210583917 | 5.995 ns/op | | BenchmarkWithoutCapacity-4 | 10043202 | 113.6 ns/op | | BenchmarkFixedArray-4 | 381437634 | 3.104 ns/op |

预先指定容量的函数比不指定容量的快了20倍。 而固定数组比预先指定容量的切片还要快 :slight_smile:

更多关于Golang中固定容量的切片与数组性能对比的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在性能方面,固定容量的切片与数组相比确实存在一些差异,但通常这些差异在大多数应用场景中可以忽略不计。以下是具体的对比分析:

1. 内存分配与访问

数组在栈上分配,而切片在堆上分配(即使使用 make 预分配容量)。这意味着数组的访问可能更快,因为栈内存的局部性更好,且无需额外的堆管理开销。但 Go 编译器会尝试进行逃逸分析,如果切片未逃逸出函数作用域,也可能被分配在栈上,从而减少性能差距。

示例代码:

// 数组版本
func arrayVersion() [8]uint {
    var arr [8]uint
    for i := 0; i < 8; i++ {
        arr[i] = uint(i)
    }
    return arr
}

// 切片版本(预分配容量)
func sliceVersion() []uint {
    slc := make([]uint, 0, 8)
    for i := 0; i < 8; i++ {
        slc = append(slc, uint(i))
    }
    return slc
}

2. 追加操作的性能

使用 append 填充预分配容量的切片时,由于容量已固定,不会触发重新分配和复制,因此追加操作是高效的。但 append 仍需要检查切片长度和容量,并更新长度字段,这比直接通过索引赋值数组略慢。

基准测试示例:

func BenchmarkArray(b *testing.B) {
    for n := 0; n < b.N; n++ {
        var arr [8]uint
        for i := 0; i < 8; i++ {
            arr[i] = uint(i)
        }
    }
}

func BenchmarkSlice(b *testing.B) {
    for n := 0; n < b.N; n++ {
        slc := make([]uint, 0, 8)
        for i := 0; i < 8; i++ {
            slc = append(slc, uint(i))
        }
    }
}

运行结果通常显示数组版本稍快(约 5-15%),但具体差异取决于硬件和 Go 版本。

3. 函数传递与复制开销

数组作为值类型传递时会完整复制,而切片传递的是引用(底层数组指针、长度和容量)。对于大型固定集合,切片传递的开销更小。但你的场景中容量为 8,数组复制开销很小。

示例:

// 数组传递(复制整个数组)
func processArray(arr [8]uint) {
    // 操作数组
}

// 切片传递(仅复制切片头,24字节 on 64位系统)
func processSlice(slc []uint) {
    // 操作切片
}

4. 实际建议

如果你的集合容量确实固定且较小(如 8 个元素),使用数组可能略微提升性能。但切片的 append 便利性和灵活性往往更重要,除非你在高性能关键路径上。

性能损失通常微不足道,但如果你需要极致性能,可以:

  • 使用数组并通过索引直接赋值。
  • 如果必须用切片,考虑预分配长度而非容量,直接通过索引赋值:
slc := make([]uint, 8) // 预分配长度
for i := range slc {
    slc[i] = uint(i) // 直接赋值,避免 append 开销
}

总之,对于容量为 8 的场景,性能差异通常可以忽略。选择更符合代码清晰性和维护性的方式即可。

回到顶部