Skip to content

Commit

Permalink
docs:精简性能篇
Browse files Browse the repository at this point in the history
  • Loading branch information
dablelv committed Jun 7, 2024
1 parent 218357d commit 59c1ec3
Show file tree
Hide file tree
Showing 4 changed files with 26 additions and 15 deletions.
4 changes: 3 additions & 1 deletion 第四篇:最佳性能/0.导语.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
代码的稳健、可读和高效是我们每一个位 coder 的共同追求。本篇将结合 Go 语言特性,为书写高效的代码,力争从常用数据结构、内存管理和并发编程三个方面给出相关建议。话不多说,让我们一起学习 Go 高性能编程技法吧。
代码的鲁棒、高效和可读是我们每一位 coder 的共同追求。

本篇将结合 Go 语言特性,为书写高效的代码,力争从常用数据结构、内存管理和并发编程三个方面给出相关建议。话不多说,让我们一起学习 Go 高性能编程技法吧。
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,34 @@ Go 语言标准库以及很多开源软件中都使用了 Go 语言的反射能

# 1.使用 strconv 而不是 fmt

基本数据类型与字符串之间的转换,优先使用 strconv 而不是 fmt,因为前者性能更佳。
基本数据类型与字符串之间的转换,优先使用 strconv 而非 fmt,因为前者性能更佳。

```go
// Bad
for i := 0; i < b.N; i++ {
s := fmt.Sprint(rand.Int())
func BenchmarkFmtSprint(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = fmt.Sprint(rand.Int())
}
}

BenchmarkFmtSprint-4 143 ns/op 2 allocs/op

// Good
for i := 0; i < b.N; i++ {
s := strconv.Itoa(rand.Int())
func BenchmarkStrconv(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = strconv.Itoa(rand.Int())
}
}

BenchmarkStrconv-4 64.2 ns/op 1 allocs/op
```
运行如下基准测试命令,将看到上面的基准测试结果。
```shell
# -bench=. 表示执行所有基准测试函数
# -benchmem 打印基准测试的内存分配统计信息
# main/perf 表示执行基准测试的包
go test -bench=. -benchmem main/perf
```
为什么性能上会有两倍多的差距,因为 fmt 实现上利用反射来达到范型的效果,在运行时进行类型的动态判断,所以带来了一定的性能损耗。

# 2.少量的重复不比反射差
Expand All @@ -47,8 +58,7 @@ func DeleteSliceElms(i interface{}, elms ...interface{}) interface{} {
}
```

很多时候,我们可能只需要操作一个类型的切片,利用反射实现的类型泛化扩展的能力压根没用上。退一步说,如果我们真地需要对 uint64 以外类型的切片进行过滤,拷贝一次代码又何妨呢?可以肯定的是,绝大部份场景,根本不会对所有类型的切片进行过滤,那么反射带来好处我们并没有充分享受,但却要为其带来的性能成本买单。

很多时候,我们可能只需要操作一个类型的切片,利用反射实现的类型泛化扩展的能力压根没用上。退一步说,如果我们真地需要对 uint64 以外类型的切片进行过滤,拷贝一次代码又何妨呢?可以肯定的是,绝大部分场景,根本不会对所有类型的切片进行过滤,那么反射带来好处我们并没有充分享受,但却要为其带来的性能成本买单。
```go
// DeleteU64liceElms 从 []uint64 过滤指定元素。注意:不修改原切片。
func DeleteU64liceElms(i []uint64, elms ...uint64) []uint64 {
Expand Down Expand Up @@ -141,7 +151,6 @@ fmt.Println(NtohlNotUseBinary([]byte{0x7f, 0, 0, 0x1})) // 2130706433
该函数也是参考了 encoding/binary 包针对大端字节序将字节序列转为 uint32 类型时的实现。

下面看下剥去反射前后二者的性能差异。

```go
func BenchmarkNtohl(b *testing.B) {
for i := 0; i < b.N; i++ {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ CPU 始终以字长访问内存,如果不进行内存对齐,可能会增加

从这个例子可以看到,内存对齐对实现变量的原子性操作也是有好处的。每次内存访问是原子的,如果变量的大小不超过字长,那么内存对齐后,对该变量的访问就是原子的,这个特性在并发场景下至关重要。

简言之:合理的内存对齐可以提高内存读写的性能,并且便于实现变量操作的原子性。
简言之:合理的内存对齐可以提高内存读写性能,并且便于实现变量操作的原子性。

# 2.Go 内存对齐规则
编译器一般为了减少 CPU 访存指令周期,提高内存的访问效率,会对变量进行内存对齐。Go 作为一门追求高性能的后台编程语言,当然也不例外。
编译器一般为了减少 CPU 访存指令周期,提高内存访问效率,会对变量进行内存对齐。Go 作为一门追求高性能的后台编程语言,当然也不例外。

Go Language Specification 中 [Size and alignment guarantees](https://go.dev/ref/spec#Size_and_alignment_guarantees) 描述了内存对齐的规则。
>1.For a variable x of any type: unsafe.Alignof(x) is at least 1.
Expand All @@ -25,7 +25,7 @@ Go Language Specification 中 [Size and alignment guarantees](https://go.dev/ref
- 对于结构体类型的变量 x,计算 x 每一个字段 f 的 unsafe.Alignof(x.f),unsafe.Alignof(x) 等于其中的最大值。
- 对于数组类型的变量 x,unsafe.Alignof(x) 等于构成数组的元素类型的对齐系数。

其中函数 `unsafe.Alignof` 用于获取变量的对齐系数。 对齐系数决定了字段的偏移和变量的大小,两者必须是对齐系数的整数倍。
其中函数 `unsafe.Alignof` 用于获取变量的对齐系数。对齐系数决定了字段的偏移和变量的大小,两者必须是对齐系数的整数倍。

# 3.合理的 struct 布局
因为内存对齐的存在,合理的 struct 布局可以减少内存占用,提高程序性能。
Expand Down Expand Up @@ -72,7 +72,7 @@ demo2 的对齐系数由 c 的对齐系数决定,也是 4,因此,demo2 的
# 4.空结构与空数组对内存对齐的影响
空结构与空数组在 Go 中比较特殊。没有任何字段的空 struct{} 和没有任何元素的 array 占据的内存空间大小为 0。

因为这一点,空 struct{} 或空 array 作为其他 struct 的字段时,一般不需要内存对齐。但是有一种情况除外:即当 struct{} 或空 array 作为结构体最后一个字段时,需要内存对齐。因为如果有指针指向该字段,返回的地址将在结构体之外,如果此指针一直存活不释放对应的内存,就会有内存泄露的问题(该内存不因结构体释放而释放)。
因为这一点,空 struct{} 或空 array 作为其他 struct 的字段时,一般不需要内存对齐。但是有一种情况除外:即当 struct{} 或空 array 作为结构体最后一个字段时,需要内存对齐。因为如果有指针指向该字段,返回的地址将在结构体之外,如果此指针一直存活不释放对应的内存,就会有内存泄露(该内存不因结构体释放而释放)。
```go
type demo3 struct {
a struct{}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

# 1.局部切片尽可能确定长度或容量

如果使用局部切片时,已知切片的长度或容量,请使用常量或数值字面量来定义。
如果使用局部切片,已知切片的长度或容量,请使用常量或数值字面量来定义。

```go
package main
Expand Down Expand Up @@ -41,7 +41,7 @@ func main() {
./main.go:5:12: make([]int, 0, number) escapes to heap
./main.go:9:12: make([]int, 0, 10) does not escape
```
从输出结果可以看到,使用变量(非常量)来指定切片的容量,会导致切片发生逃逸,影响性能。指定切片的长度时也是一样的,尽可能使用常量或数值字面量。
从输出结果可以看到,使用变量(非常量)来指定切片容量,会导致切片发生逃逸,影响性能。指定切片长度时也是一样的,尽可能使用常量或数值字面量。

下面看下二者的性能差异。

Expand Down

0 comments on commit 59c1ec3

Please sign in to comment.