Go 高性能之空结构体

概述 #

Go 的标准库没有内置的 Set 类型,在不引用第三方包的情况下,一般是结合内置的 map 类型实现 Set 相关功能。

map 实现 set #

这里假设 Set 元素类型为 int, 那么我们就以 int 作为 map 的键类型,以 bool 作为 map 的值类型 (之所以选择 bool 类型,是因为其大小为 1 个字节,相对其他数据类型可以节省内存,当然,也可以使用 byte 类型,其大小同样是 1 个字节)。

package main

import "fmt"

// Set 类型定义
type set map[int]bool

// 初始化一个新的 Set
func newSet() set {
	return make(set)
}

// 元素是否存在于与集合中
func (s set) contains(ele int) bool {
	_, ok := s[ele]
	return ok
}

// 添加元素到集合
func (s set) add(ele int) {
	s[ele] = true
}

// 从集合中删除某个元素
func (s set) remove(ele int) {
	delete(s, ele)
}

func main() {
	s := newSet()

	fmt.Println(s.contains(100))
	s.add(100)
	fmt.Println(s.contains(100))
	s.remove(100)
	fmt.Println(s.contains(100))
}
$ go run main.go

# 输出如下
false
true
false

上述示例代码通过内置类型 map 实现了 set 类型相关功能,美中不足的一点在于: 每个元素都要浪费一个 bool 类型作为标识占位符, 能否进一步的优化呢? 当然是可以的,这就是接下来要讲到的 空结构体 了。

空结构体 #

Go 中的空结构体 struct{}{} 是一个底层的内置变量,不占用任何内存:

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	fmt.Printf("size = %d\n", unsafe.Sizeof(struct{}{}))
}
$ go run main.go

# 输出如下
size = 0

结合刚才的例子,可以将 struct{}{} 作为 Set 的元素,这样不论 Set 有多少个元素,标志位 内存占用始终为 0 。

使用 bool 实现 Set #

测试代码 #

package performance

import (
	"testing"
)

// Set 类型定义, 使用 bool 类型作为占位符
type set map[int]bool

// 初始化一个新的 Set
func newSet() set {
	return make(set)
}

// 元素是否存在于与集合中
func (s set) contains(ele int) bool {
	_, ok := s[ele]
	return ok
}

// 添加元素到集合
func (s set) add(ele int) {
	s[ele] = true
}

// 从集合中删除某个元素
func (s set) remove(ele int) {
	delete(s, ele)
}

func Benchmark_Compare(b *testing.B) {
	s := newSet()

	for i := 0; i < b.N; i++ {
		s.add(i)
	}
	for i := 0; i < b.N; i++ {
		_ = s.contains(i)
		s.remove(i)
	}
}

运行测试,并将基准测试结果写入文件:

$ go test -run='^$' -bench=. -count=1 -benchtime=10000000x -benchmem > bool.txt

使用空结构体实现 Set #

测试代码 #

package performance

import (
	"testing"
)

// Set 类型定义, 使用 bool 类型作为占位符
type set map[int]struct{}

// 初始化一个新的 Set
func newSet() set {
	return make(set)
}

// 元素是否存在于与集合中
func (s set) contains(ele int) bool {
	_, ok := s[ele]
	return ok
}

// 添加元素到集合
func (s set) add(ele int) {
	s[ele] = struct{}{}
}

// 从集合中删除某个元素
func (s set) remove(ele int) {
	delete(s, ele)
}

func Benchmark_Compare(b *testing.B) {
	s := newSet()

	for i := 0; i < b.N; i++ {
		s.add(i)
	}
	for i := 0; i < b.N; i++ {
		_ = s.contains(i)
		s.remove(i)
	}
}

运行测试,并将基准测试结果写入文件:

$ go test -run='^$' -bench=. -count=1 -benchtime=10000000x -benchmem > struct.txt

使用 benchstat 比较差异 #

$ benchstat -alpha=100 bool.txt struct.txt

# 输出如下
name        old time/op    new time/op    delta
_Compare-8     371ns ± 0%     332ns ± 0%  -10.47%  (p=1.000 n=1+1)

name        old alloc/op   new alloc/op   delta
_Compare-8     44.0B ± 0%     40.0B ± 0%   -9.09%  (p=1.000 n=1+1)

name        old allocs/op  new allocs/op  delta
_Compare-8      0.00           0.00          ~     (all equal)

从输出的结果中可以看到,相比于使用 bool 作为 Set 元素占位符,使用 空结构体 在性能和内存占用方面,都有了小幅度的优化提升。 因为时间关系,这里的基准测试只运行了 10000000 次,运行次数越大,优化的效果越明显。感兴趣的读者可以将 -benchtime 调大后看看优化效果。

小结 #

Go 中的空结构体 struct{}{} 不占用任何内存,而且有很清晰的语义性质 (作为占位符使用)。除了刚才示例中实现 Set 功能外, 还可以使用空结构体作为 通道信号标识空对象 等,各种使用场景请读者自行探究。

彩蛋 #

除了空结构体 struct{}{} 之外,还有一个鲜为人知的大小为 0 的数据类型是: 空数组

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	fmt.Printf("size = %d\n", unsafe.Sizeof([0]int{}))
}
$ go run main.go

# 输出如下
size = 0

转载申请

本作品采用 知识共享署名 4.0 国际许可协议 进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,商业转载请联系作者获得授权。

© 蛮荆 | 陕公网安备 61011302001681 号 | 陕ICP备2023004378号-1 | Rendered by Hugo