https://blog.csdn.net/panda_8/article/details/106722195 Plan9汇编手册

https://blog.csdn.net/qq_45091883/article/details/123515862 汇编示例,可以仔细看看

因为编译器优化的问题,很多时候代码性能的优劣其实和我们想象中是不一样的。这里对常见代码性能做一些比较。

说明:我用go version go1.18.4 windows/amd64做的测试。

for循环判断条件

  1. 在for循环中,判断是否继续循环的条件,往往是一个表达式,执行时候会每次循环都计算一次表达式结果吗?看下面两种写法:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 第一种
func Sum(arr []int) int {
	total := 0
	for i := 0; i < len(arr); i++ {
		total += arr[i]
	}
	return total
}

// 第二种
func Sum(arr []int) int {
	total := 0
	length := len(arr)
	for i := 0; i < length; i++ {
		total += arr[i]
	}
	return total
}

编译成Plan9汇编,命令:go tool compile -S demo.go

看看汇编结果:

# 第一种
0x0000 00000 (demo.go:3)        TEXT    "".Sum(SB), NOSPLIT|ABIInternal, $0-24                   
0x0000 00000 (demo.go:3)        MOVQ    AX, "".arr+8(FP)                                         
0x0005 00005 (demo.go:3)        FUNCDATA        $0, gclocals·1a65e721a2ccc325b382662e7ffee780(SB)
0x0005 00005 (demo.go:3)        FUNCDATA        $1, gclocals·69c1753bd5f81501d95132d08af04464(SB)
0x0005 00005 (demo.go:3)        FUNCDATA        $5, "".Sum.arginfo1(SB)                          
0x0005 00005 (demo.go:3)        FUNCDATA        $6, "".Sum.argliveinfo(SB)                       
0x0005 00005 (demo.go:3)        PCDATA  $3, $1                                                   
0x0005 00005 (demo.go:6)        XORL    CX, CX                                                   
0x0007 00007 (demo.go:6)        XORL    DX, DX                                                   
0x0009 00009 (demo.go:5)        JMP     25                                                       
0x000b 00011 (demo.go:5)        LEAQ    1(CX), SI                                                
0x000f 00015 (demo.go:6)        MOVQ    (AX)(CX*8), DI                                           
0x0013 00019 (demo.go:6)        ADDQ    DI, DX                                                   
0x0016 00022 (demo.go:5)        MOVQ    SI, CX                                                   
0x0019 00025 (demo.go:5)        CMPQ    BX, CX                                                   
0x001c 00028 (demo.go:5)        JGT     11                                                       
0x001e 00030 (demo.go:8)        MOVQ    DX, AX                                                   
0x0021 00033 (demo.go:8)        RET    

# 第二种
0x0000 00000 (demo.go:3)        TEXT    "".Sum(SB), NOSPLIT|ABIInternal, $0-24                   
0x0000 00000 (demo.go:3)        MOVQ    AX, "".arr+8(FP)                                         
0x0005 00005 (demo.go:3)        FUNCDATA        $0, gclocals·1a65e721a2ccc325b382662e7ffee780(SB)
0x0005 00005 (demo.go:3)        FUNCDATA        $1, gclocals·69c1753bd5f81501d95132d08af04464(SB)
0x0005 00005 (demo.go:3)        FUNCDATA        $5, "".Sum.arginfo1(SB)                          
0x0005 00005 (demo.go:3)        FUNCDATA        $6, "".Sum.argliveinfo(SB)                       
0x0005 00005 (demo.go:3)        PCDATA  $3, $1                                                   
0x0005 00005 (demo.go:5)        XORL    CX, CX                                                   
0x0007 00007 (demo.go:5)        XORL    DX, DX                                                   
0x0009 00009 (demo.go:6)        JMP     25                                                       
0x000b 00011 (demo.go:6)        LEAQ    1(CX), SI                                                
0x000f 00015 (demo.go:7)        MOVQ    (AX)(CX*8), DI                                           
0x0013 00019 (demo.go:7)        ADDQ    DI, DX                                                   
0x0016 00022 (demo.go:6)        MOVQ    SI, CX                                                   
0x0019 00025 (demo.go:6)        CMPQ    BX, CX                                                   
0x001c 00028 (demo.go:6)        JGT     11                                                       
0x001e 00030 (demo.go:9)        MOVQ    DX, AX                                                   
0x0021 00033 (demo.go:9)        RET    

看看汇编指令一模一样;不用担心了吧,推荐第一种写法。

  1. 如果条件判断是一个函数呢?
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 第三种
func Sum(arr []int) int {
	total := 0
	for i := 0; i < ArrLen(arr); i++ {
		total += arr[i]
	}
	return total
}

func ArrLen(arr []int) int {
	return len(arr)
}

// 第四种
func Sum(arr []int) int {
	total := 0
	for i := 0; i < ArrLen(&arr); i++ {
		total += arr[i]
	}
	return total
}

func ArrLen(arr *[]int) int {
	return len(*arr)
}

结果如下:

# 第三种
0x0000 00000 (demo.go:3)        TEXT    "".Sum(SB), NOSPLIT|ABIInternal, $0-24
0x0000 00000 (demo.go:3)        MOVQ    AX, "".arr+8(FP)
0x0005 00005 (demo.go:3)        FUNCDATA        $0, gclocals·1a65e721a2ccc325b382662e7ffee780(SB)
0x0005 00005 (demo.go:3)        FUNCDATA        $1, gclocals·69c1753bd5f81501d95132d08af04464(SB)
0x0005 00005 (demo.go:3)        FUNCDATA        $5, "".Sum.arginfo1(SB)
0x0005 00005 (demo.go:3)        FUNCDATA        $6, "".Sum.argliveinfo(SB)
0x0005 00005 (demo.go:3)        PCDATA  $3, $1
0x0005 00005 (demo.go:12)       XORL    CX, CX
0x0007 00007 (demo.go:12)       XORL    DX, DX
0x0009 00009 (demo.go:5)        JMP     25
0x000b 00011 (demo.go:5)        LEAQ    1(CX), SI
0x000f 00015 (demo.go:6)        MOVQ    (AX)(CX*8), DI
0x0013 00019 (demo.go:6)        ADDQ    DI, DX
0x0016 00022 (demo.go:5)        MOVQ    SI, CX
0x0019 00025 (<unknown line number>)    NOP
0x0019 00025 (demo.go:5)        CMPQ    BX, CX
0x001c 00028 (demo.go:5)        JGT     11
0x001e 00030 (demo.go:8)        MOVQ    DX, AX
0x0021 00033 (demo.go:8)        RET

# 第四种
0x0000 00000 (demo.go:3)        TEXT    "".Sum(SB), NOSPLIT|ABIInternal, $0-24                   
0x0000 00000 (demo.go:3)        FUNCDATA        $0, gclocals·1a65e721a2ccc325b382662e7ffee780(SB)
0x0000 00000 (demo.go:3)        FUNCDATA        $1, gclocals·69c1753bd5f81501d95132d08af04464(SB)
0x0000 00000 (demo.go:3)        FUNCDATA        $2, "".Sum.stkobj(SB)                            
0x0000 00000 (demo.go:3)        FUNCDATA        $5, "".Sum.arginfo1(SB)                          
0x0000 00000 (demo.go:3)        MOVQ    AX, "".arr+8(SP)                                         
0x0005 00005 (demo.go:3)        MOVQ    BX, "".arr+16(SP)                                        
0x000a 00010 (demo.go:3)        MOVQ    CX, "".arr+24(SP)                                        
0x000f 00015 (demo.go:3)        XORL    CX, CX                                                   
0x0011 00017 (demo.go:3)        XORL    DX, DX                                                   
0x0013 00019 (demo.go:5)        JMP     35                                                       
0x0015 00021 (demo.go:5)        LEAQ    1(CX), SI                                                
0x0019 00025 (demo.go:6)        MOVQ    (AX)(CX*8), DI                                           
0x001d 00029 (demo.go:6)        ADDQ    DI, DX                                                   
0x0020 00032 (demo.go:5)        MOVQ    SI, CX                                                   
0x0023 00035 (demo.go:5)        CMPQ    BX, CX                                                   
0x0026 00038 (demo.go:5)        JGT     21                                                       
0x0028 00040 (demo.go:8)        MOVQ    DX, AX                                                   
0x002b 00043 (demo.go:8)        RET   

比较之后发现,虽然不同写法有些许差异,但是经过编译优化之后,这些写法的性能几乎是一样的。

  1. 再看下面这个局部变量是切片的例子
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func InnerSlice1(num []int) int {
	total := 0
	for i := range num {
		total += num[i]
	}
	return total
}

func InnerSlice2(num []int) int {
	total := 0
	arr := num
	for i := range arr {
		total += arr[i]
	}
	return total
}

func InnerSlice3(num []int) int {
	total := 0
	arr := &num
	for i := range *arr {
		total += (*arr)[i]
	}
	return total
}

汇编结果如下:

# InnerSlice1 和 InnerSlice2 结果一样
0x0000 00000 (.\value_ok.go:10) TEXT    "".InnerSlice1(SB), NOSPLIT|ABIInternal, $0-24            
0x0000 00000 (.\value_ok.go:10) MOVQ    AX, "".num+8(FP)                                         
0x0005 00005 (.\value_ok.go:10) FUNCDATA        $0, gclocals·1a65e721a2ccc325b382662e7ffee780(SB)
0x0005 00005 (.\value_ok.go:10) FUNCDATA        $1, gclocals·69c1753bd5f81501d95132d08af04464(SB)
0x0005 00005 (.\value_ok.go:10) FUNCDATA        $5, "".InnerSlice1.arginfo1(SB)                   
0x0005 00005 (.\value_ok.go:10) FUNCDATA        $6, "".InnerSlice1.argliveinfo(SB)                
0x0005 00005 (.\value_ok.go:10) PCDATA  $3, $1                                                   
0x0005 00005 (.\value_ok.go:13) XORL    CX, CX                                                   
0x0007 00007 (.\value_ok.go:13) XORL    DX, DX                                                   
0x0009 00009 (.\value_ok.go:12) JMP     25                                                       
0x000b 00011 (.\value_ok.go:12) LEAQ    1(CX), SI                                                
0x000f 00015 (.\value_ok.go:13) MOVQ    (AX)(CX*8), DI                                           
0x0013 00019 (.\value_ok.go:13) ADDQ    DI, DX                                                   
0x0016 00022 (.\value_ok.go:12) MOVQ    SI, CX                                                   
0x0019 00025 (.\value_ok.go:12) CMPQ    BX, CX                                                   
0x001c 00028 (.\value_ok.go:12) JGT     11                                                       
0x001e 00030 (.\value_ok.go:15) MOVQ    DX, AX                                                   
0x0021 00033 (.\value_ok.go:15) RET   

# InnerSlice3稍有不同
0x0000 00000 (.\value_ok.go:26) TEXT    "".InnerSlice3(SB), NOSPLIT|ABIInternal, $0-24
0x0000 00000 (.\value_ok.go:26) FUNCDATA        $0, gclocals·1a65e721a2ccc325b382662e7ffee780(SB)
0x0000 00000 (.\value_ok.go:26) FUNCDATA        $1, gclocals·69c1753bd5f81501d95132d08af04464(SB)
0x0000 00000 (.\value_ok.go:26) FUNCDATA        $2, "".InnerSlice3.stkobj(SB)
0x0000 00000 (.\value_ok.go:26) FUNCDATA        $5, "".InnerSlice3.arginfo1(SB)
0x0000 00000 (.\value_ok.go:26) MOVQ    AX, "".num+8(SP)
0x0005 00005 (.\value_ok.go:26) MOVQ    BX, "".num+16(SP)
0x000a 00010 (.\value_ok.go:26) MOVQ    CX, "".num+24(SP)
0x000f 00015 (.\value_ok.go:26) XORL    CX, CX
0x0011 00017 (.\value_ok.go:26) XORL    DX, DX
0x0013 00019 (.\value_ok.go:29) JMP     35
0x0015 00021 (.\value_ok.go:29) LEAQ    1(CX), SI
0x0019 00025 (.\value_ok.go:30) MOVQ    (AX)(CX*8), DI
0x001d 00029 (.\value_ok.go:30) ADDQ    DI, DX
0x0020 00032 (.\value_ok.go:29) MOVQ    SI, CX
0x0023 00035 (.\value_ok.go:29) CMPQ    BX, CX
0x0026 00038 (.\value_ok.go:29) JGT     21
0x0028 00040 (.\value_ok.go:32) MOVQ    DX, AX
0x002b 00043 (.\value_ok.go:32) RET

分析:这里再次印证了(语言基础类型,包括slice,string)申请局部变量不会占用堆栈空间;编译时候都被优化掉了。局部变量用指针反而会增加寄存器、栈指针的指令,不推荐使用。

  1. 思考,如果循环比较的是map集合又如何呢?
1
2
3
4
5
6
7
func Sum(set map[int]int) int {
	total := 0
	for i := 0; i < len(set); i++ {
		total += set[i]
	}
	return total
}

自己查看汇编结果,会复杂很多。看的出来slice和map是完全不同的实现逻辑,for循环判断条件如果是个函数,而且编译器无法推断优化的话;将每次都调用函数,再用函数返回结果做判断比较。

同时这里只能感叹,编译器的优化做了很多事情,可做的也还有很多。很多语言性能不好不是语言的问题,而是没有人给优化编译器。

结论:

  1. for循环判断表达式是否提出作为局部变量,用栈内存,对性能几无影响。怎么写简单、好阅读就怎么来。
  2. 但判断表达式结果可能变化的话,提出变量将改变循环的逻辑,请谨慎甄别。
  3. for循环判断表达式如果是函数(包括len(Array)这种),而且结果可能变化,将每次循环都调用函数返回值来判断。

for循环和range迭代

先看下面两种写法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func Range(numbers []int32) int32 {
	total := int32(0)
	for i, num := range numbers {
		total += int32(i)
		total += num
	}
	return total
}

func For(numbers []int32) int32 {
	total := int32(0)
	for i := 0; i < len(numbers); i++ {
		total += int32(i)
		total += numbers[i]
	}
	return total
}

汇编指令结果是一模一样,看下面的结果。

0x0000 00000 (.\demo_for_range.go:25)   TEXT    "".RangeStruct(SB), NOSPLIT|ABIInternal, $0-24
0x0000 00000 (.\demo_for_range.go:25)   MOVQ    AX, "".users+8(FP)
0x0005 00005 (.\demo_for_range.go:25)   FUNCDATA        $0, gclocals·1a65e721a2ccc325b382662e7ffee780(SB)
0x0005 00005 (.\demo_for_range.go:25)   FUNCDATA        $1, gclocals·69c1753bd5f81501d95132d08af04464(SB)
0x0005 00005 (.\demo_for_range.go:25)   FUNCDATA        $5, "".RangeStruct.arginfo1(SB)
0x0005 00005 (.\demo_for_range.go:25)   FUNCDATA        $6, "".RangeStruct.argliveinfo(SB)
0x0005 00005 (.\demo_for_range.go:25)   PCDATA  $3, $1
0x0005 00005 (.\demo_for_range.go:27)   XORL    CX, CX
0x0007 00007 (.\demo_for_range.go:27)   XORL    DX, DX
0x0009 00009 (.\demo_for_range.go:27)   JMP     23
0x000b 00011 (.\demo_for_range.go:27)   LEAQ    1(CX), SI
0x000f 00015 (.\demo_for_range.go:28)   ADDL    CX, DX
0x0011 00017 (.\demo_for_range.go:29)   ADDL    (AX)(CX*4), DX
0x0014 00020 (.\demo_for_range.go:27)   MOVQ    SI, CX
0x0017 00023 (.\demo_for_range.go:27)   CMPQ    BX, CX
0x001a 00026 (.\demo_for_range.go:27)   JGT     11
0x001c 00028 (.\demo_for_range.go:31)   MOVL    DX, AX
0x001e 00030 (.\demo_for_range.go:31)   RET

如果改变数组的数据类型又如何呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
type User struct {
	id int32
}

func RangeStruct(users []User) int32 {
	total := int32(0)
	for i, user := range users {
		total += int32(i)
		total += user.id
	}
	return total
}

func ForStruct(users []User) int32 {
	total := int32(0)
	for i := 0; i < len(users); i++ {
		total += int32(i)
		total += users[i].id
	}
	return total
}

编译结果还是和上面一模一样,很显然编译器做了推断优化。那么我们继续变,增加结构体的内存占用大小:

1
2
3
4
type User struct {
	id   int32
	name [4096]byte
}

此时汇编的结果发现Range方式的指令比For更复杂,在无法准确判断两种汇编结果性能差异的情况下,我们用基准测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var users []User
const arrLength = 1024

func init() {
	users = make([]User, arrLength)
	for n := 0; n < arrLength; n++ {
		users[n].id = int32(n)
	}
}

func BenchmarkForStruct(b *testing.B) {
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		ForStruct(users)
	}
}

func BenchmarkRangeStruct(b *testing.B) {
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		RangeStruct(users)
	}
}

// 多次测试之后,发现结果几乎一样
// cpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
// BenchmarkForStruct-8             5047129               232.8 ns/op
// BenchmarkRangeStruct-8           5237208               229.7 ns/op

我们如果改写迭代项的值试一试呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func ForStruct(users []User) int32 {
	total := int32(0)
	for i := 0; i < len(users); i++ {
		total += int32(i)
		total += users[i].id
		users[i].name[i] = 123
	}
	return total
}

func RangeStruct(users []User) int32 {
	total := int32(0)
	for i, user := range users {
		total += int32(i)
		total += user.id
		user.name[i] = 123
	}
	return total
}

// 居然是下面的结果,惊喜意外吧?
// cpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
// BenchmarkForStruct-8              589506              1855 ns/op
// BenchmarkRangeStruct-8           5218659               233.2 ns/op

我猜for循环,通过索引找到了每个user对象,将其中name[0]的值修改了,因为数据量大,会有频繁换页影响性能了。而range方式,编译器猜测到user项的迭代是对象拷贝,每个user对象都是栈上的内存,而且修改这个值也是无意义的,那么编译器很可能就将这个逻辑优化掉了。

如果加大实体占用内存的大小,会出现啥情况呢?

1
2
3
4
5
6
7
8
9
type User struct {
	id   int32
	name [8960000]byte
}

// 我们改变name字段的内存大小,会发现大概在8MB-10MB左右的时候,Range模式把内存占满了。
// cpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
// BenchmarkForStruct-8             1000000              1033 ns/op
// BenchmarkRangeStruct-8                 1        5135601400 ns/op

协程栈空间是可以动态变化的,即使可变栈,局部变量占用内存过大可能就会逃逸分配到堆上,目前看极值可能是10MB。

协程栈在新版实现中默认是2KB,其可变范围是多大呢?下面这个错误可供参考(1GB):

1
2
3
4
5
6
runtime: goroutine stack exceeds 1000000000-byte limit
runtime: sp=0xc020271380 stack=[0xc020270000, 0xc040270000]
fatal error: stack overflow

// 代码在 runtime/stack.go
var maxstacksize uintptr = 1 << 20 // enough until runtime.main sets it for real

如果迭代项都改成对象引用会如何呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func ForStruct(users []*User) int32 {
	total := int32(0)
	for i := 0; i < len(users); i++ {
		total += int32(i)
		total += users[i].id
		users[i].name[i] = 123
	}
	return total
}

func RangeStruct(users []*User) int32 {
	total := int32(0)
	for i, user := range users {
		total += int32(i)
		total += user.id
		user.name[i] = 123
	}
	return total
}

// 都是对象引用,这时性能几乎一样了
// cpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
// BenchmarkForStruct-8              564808              2035 ns/op
// BenchmarkRangeStruct-8            572229              2031 ns/op

再研究下for;;下面的两种写法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
func ForStruct(pms *List) int32 {
	total := int32(0)
	for i := 0; i < len((*pms)); i++ {
		total += (*pms)[i].id
	}
	return total
}

func ForStruct(pms *List) int32 {
	total := int32(0)
	arr := (*pms)
	for i := 0; i < len(arr); i++ {
		total += arr[i].id
	}
	return total
}

// 下面的代码只比上面的代码多用两个寄存器,不会占用栈内存空间,仅此而已
// 0x0000 00000 (.\demo_for_range.go:57)   MOVQ    (AX), CX
// 0x0003 00003 (.\demo_for_range.go:57)   MOVQ    8(AX), DX

// PS: 上面提出局部变量的写法,和下面两种for...range是一样的汇编指令
func RangeStruct(pms *List) int32 {
	total := int32(0)
	arr := *pms
	for _, it := range arr {
		total += it.id
	}
	return total
}

func RangeStruct(pms *List) int32 {
	total := int32(0)
	for i := range *pms {
		total += (*pms)[i].id
	}
	return total
}

上面试验看出下面两点:

  1. 大部分情况下for;;for...range的写法性能是一样的。
  2. 当迭代的结构体内存占用过大时,range方式每次迭代因为是对象拷贝,会占用协程栈空间。

结论:

  1. 无论何时推荐用for;;循环,用for...range只迭代索引也可
  2. 为提高代码可读性,很多时候定义局部变量是好办法。

switch和if…else

看一个简单的例子吧:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func IfCondition(a int) int {
	if a == 1 {
		return a + 1
	} else if a == 2 {
		return a + 2
	}
	return a
}

func SwiftCondition(a int) int {
	switch a {
	case 1:
		return a + 1
	case 2:
		return a + 2
	default:
		return a
	}
}

两种写法汇编结果一模一样:

0x0000 00000 (.\switch_if.go:3)    TEXT    "".IfCondition(SB), NOSPLIT|ABIInternal, $0-8            
0x0000 00000 (.\switch_if.go:3)    FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (.\switch_if.go:3)    FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (.\switch_if.go:3)    FUNCDATA        $5, "".IfCondition.arginfo1(SB)                  
0x0000 00000 (.\switch_if.go:3)    FUNCDATA        $6, "".IfCondition.argliveinfo(SB)               
0x0000 00000 (.\switch_if.go:3)    PCDATA  $3, $1                                                   
0x0000 00000 (.\switch_if.go:4)    CMPQ    AX, $1                                                   
0x0004 00004 (.\switch_if.go:4)    JEQ     18                                                       
0x0006 00006 (.\switch_if.go:6)    CMPQ    AX, $2                                                   
0x000a 00010 (.\switch_if.go:6)    JNE     17                                                       
0x000c 00012 (.\switch_if.go:7)    ADDQ    $2, AX                                                   
0x0010 00016 (.\switch_if.go:7)    RET                                                              
0x0011 00017 (.\switch_if.go:9)    RET                                                              
0x0012 00018 (.\switch_if.go:5)    INCQ    AX                                                       
0x0015 00021 (.\switch_if.go:5)    RET    

结论:

if...else 与 switch性能是一样的,喜欢啥就用啥吧。

再看下面常见的一个例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var arr []string

func CompareStr1(str1 string) int {
	for i := range arr {
		if arr[i][0] == str1[0] && arr[i][len(arr[i])-1] == str1[len(str1)-1] && arr[i] == str1 {
			return 5
		}
	}
	return 7
}

func CompareStr2(str1 string) int {
	for i := range arr {
		if arr[i][0] == str1[0] {
			if arr[i][len(arr[i])-1] == str1[len(str1)-1] {
				if arr[i] == str1 {
					return 5
				}
			}
		}
	}
	return 7
}

经过基准测试,发现两个函数性能并不一样,CompareStr2总是比CompareStr1略好,当然差异很小。汇编结果发现两者的输出逻辑其实是有差异的。

结论:

if判断语句在用 && 或 || 做级联时,如果表达式本身就存在运算,用分级if判断些许能提高一点点性能。

type断言类型转换

先看整形互转

1
2
3
4
5
6
7
func ToInt(v int32) int {
	return int(v)
}

func ToInt32(v int) int32 {
	return int32(v)
}

结果如何呢,看下面的汇编语句,差异挺小。总之数值(整形、浮点型)之间随意转换的性能损耗都是极小的。

"".ToInt STEXT nosplit size=4 args=0x8 locals=0x0 funcid=0x0 align=0x0                      
     0x0000 00000 (.\t.go:3)      TEXT    "".ToInt(SB), NOSPLIT|ABIInternal, $0-8                  
     0x0000 00000 (.\t.go:3)      FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
     0x0000 00000 (.\t.go:3)      FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
     0x0000 00000 (.\t.go:3)      FUNCDATA        $5, "".ToInt.arginfo1(SB)                        
     0x0000 00000 (.\t.go:3)      FUNCDATA        $6, "".ToInt.argliveinfo(SB)                     
     0x0000 00000 (.\t.go:3)      PCDATA  $3, $1                                                   
     0x0000 00000 (.\t.go:4)      MOVLQSX AX, AX                                                   
     0x0003 00003 (.\t.go:4)      RET                                                              
     0x0000 48 63 c0 c3                                      Hc..                                             
"".ToInt32 STEXT nosplit size=1 args=0x8 locals=0x0 funcid=0x0 align=0x0                         
     0x0000 00000 (.\t.go:7)      TEXT    "".ToInt32(SB), NOSPLIT|ABIInternal, $0-8             
     0x0000 00000 (.\t.go:7)      FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
     0x0000 00000 (.\t.go:7)      FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
     0x0000 00000 (.\t.go:7)      FUNCDATA        $5, "".ToInt32.arginfo1(SB)                      
     0x0000 00000 (.\t.go:7)      FUNCDATA        $6, "".ToInt32.argliveinfo(SB)                   
     0x0000 00000 (.\t.go:7)      PCDATA  $3, $1                                                   
     0x0000 00000 (.\t.go:8)      RET                                                              
     0x0000 c3     

下面看看断言和反射

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
func ToInt(v int64) int {
	switch v {

	case 1:
		return int(v)
	case 12:
		return int(v)
	case 123:
		return int(v)
	case 1234:
		return int(v)
	default:
		return 0
	}
}

func ToTypeInt(v any) int {
	switch v.(type) {
	case int:
		return v.(int)
	case int32:
		return int(v.(int32))
	case int64:
		return int(v.(int64))
	case string:
		ret, _ := strconv.Atoi(v.(string))
		return ret
	default:
		return 0
	}
}

func ToReflectInt(v any) int {
	vVal := reflect.ValueOf(v)
	switch vVal.Kind() {
	case reflect.Int:
		return int(vVal.Int())
	case reflect.Int32:
		return int(vVal.Int())
	case reflect.Int64:
		return int(vVal.Int())
	case reflect.String:
		ret, _ := strconv.Atoi(vVal.String())
		return ret
	default:
		return 0
	}
}

写个基准测试看看

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var source int64 = 123

func init() {
}

func BenchmarkToInt(b *testing.B) {
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		ToInt(source)
	}
}

func BenchmarkToTypeInt(b *testing.B) {
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		ToTypeInt(source)
	}
}

func BenchmarkReflectInt(b *testing.B) {
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		ToReflectInt(source)
	}
}

结果如下

plan9> go test -bench=Int$ -benchmem
goos: windows
goarch: amd64
cpu: Intel(R) Core(TM) i7-10700 CPU @ 2.90GHz
BenchmarkToInt-16               1000000000               0.4469 ns/op          0 B/op          0 allocs/op
BenchmarkToTypeInt-16           669050343                1.778 ns/op           0 B/op          0 allocs/op
BenchmarkReflectInt-16          339428430                3.534 ns/op           0 B/op          0 allocs/op
PASS
ok      yufa/plan9      3.108s

如果将数据源改成字符串:var source string = "123",再次测试结果如下:

BenchmarkToTypeInt-16           231602151                5.175 ns/op           0 B/op          0 allocs/op
BenchmarkReflectInt-16          41504682                28.38 ns/op           16 B/op          1 allocs/op

用反射的时候发生了一次堆分配,当然整体性能差的太多了。有意思的是下面这个测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func ToInt(v int64) int {
	switch v {
	case 1:
		return int(v)
	case 12:
		return int(v)
	case 123:
		return int(v)
	case 1234:
		return int(v)
	default:
		return 0
	}
}

func ToInt2(v int64) int {
	switch v {
	case 1:
		return int(v)
	case 12:
		return int(v)
	case 123:
		return int(v)
	case 1234:
		ret, _ := strconv.Atoi("123")
		return ret
	default:
		return 0
	}
}

v=123传参测试,结果居然有明显差异;看来即使逻辑分支不执行,多用了函数堆栈空间对性能也是有影响的。

BenchmarkToInt-16       1000000000               0.7514 ns/op          0 B/op          0 allocs/op
Benchmark2ToInt-16      682067368                1.761 ns/op           0 B/op          0 allocs/op

结论:

  1. 整形浮点型之间类型转换开销很小,可以忽略不计。
  2. .(type)断言类型判断并转换开销也不大,至少要比用反射操作要快。
  3. 一旦涉及堆内存分配和资源回收,性能会下降非常明显。即使栈针移动也会消耗CPU。
  4. 反射性能比较差,要谨慎使用。因为每个反射函数调用,涉及大量逻辑判断,甚至堆资源分配。

条件判断

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package array

import "reflect"

const (
	kindsCount     = 27
    numberKindMask = 131068   // 0000 0000 0001 1111 1111 1111 1100
)

var asString = [27]bool{
	reflect.String:    true,
	reflect.Interface: true,
	reflect.Uint32:    true,
	reflect.Uint64:    true,
}

func ByIndex(kd reflect.Kind) bool {
	return asString[kd]
}

func ByEqual(kd reflect.Kind) bool {
	return kd == reflect.String || kd == reflect.Interface || kd == reflect.Int ||
    	kd == reflect.Uint8 || kd == reflect.Uint32 || kd == reflect.Int8 ||
    	kd == reflect.Int32 || kd == reflect.Int64 || kd == reflect.Uint16 ||
    	kd == reflect.Uint64
}

func ByBit(kd reflect.Kind) bool {
	return (1<<kd)&numberKindMask != 0
	// return kd < 17 && kd > 1
}

func BySwitch(kd reflect.Kind) bool {
	switch kd {
	case reflect.String:
		return true
	case reflect.Interface:
		return true
	case reflect.Int:
		return false
	case reflect.Uint8:
		return true
	case reflect.Uint32:
		return true
	case reflect.Int8:
		return false
	case reflect.Int32:
		return true
	case reflect.Int64:
		return true
	case reflect.Uint16:
		return false
	case reflect.Uint64:
		return true
	}
	return false
}

测试结果如下:

BenchmarkAsIndex-16             1000000000               0.2966 ns/op          0 B/op          0 allocs/op
BenchmarkEqualIndex-16          1000000000               0.2215 ns/op          0 B/op          0 allocs/op
BenchmarkBitIndex-16            1000000000               0.2195 ns/op          0 B/op          0 allocs/op
BenchmarkSwitchIndex-16         1000000000               0.7697 ns/op          0 B/op          0 allocs/op

结论:

  1. 这几种方式其实都很快,至少整形数是这样,太多的if…else…分支稍微影响性能。
  2. 相对来说位运算又简洁又快。

再看一个例子,JSON编解码中常见的字符比对,猜猜谁的性能更好:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
const (
	isSpaceMask = (1 << ' ') | (1 << '\t') | (1 << '\r') | (1 << '\n')
)

//go:nosplit
//go:inline
func isBlank(c byte) bool {
	return isSpaceMask&(1<<c) != 0
}

var (
	isBlankChar = [256]bool{
		' ':  true,
		'\n': true,
		'\r': true,
		'\t': true,
	}
}

func CharBit(str string) int {
	for i := 0; i < len(str); i++ {
		if isBlank(str[i]) {
			continue
		}
		return i
	}
	return -1
}

func CharIndex(str string) int {
	for i := 0; i < len(str); i++ {
		if isBlankChar[str[i]] {
			continue
		}
		return i
	}
	return -1
}

func CharCompare(str string) int {
	for i := 0; i < len(str); i++ {
		c := str[i]
		if c == ' ' || c == '\n' || c == '\r' || c == '\t' {
			continue
		}
		return i
	}
	return -1
}

一组可能的结果如下:

BenchmarkBitChar-16              9404683               127.2 ns/op             0 B/op          0 allocs/op
BenchmarkIndexChar-16           17384122                69.71 ns/op            0 B/op          0 allocs/op
BenchmarkCompareChar-16         24662990                48.94 ns/op            0 B/op          0 allocs/op

结论:

  1. 有时候位操作性能并没有你所想象中的好。
  2. 数组索引性能不错,而且不同输入组合,性能一致,只不过需要占用一段全局内存空间。
  3. 直接比值加||运算,在首个条件就满足时,反而性能最好。平均比对到第二和第三个条件时,性能和数组索引接近。
  4. 当判断条件需要比较两三组值的时候,直接比较往往性能最好。

很多司空见惯的问题,往往不是你想的那样。需要测试。

(完)

PS:下面是Go发明者之一Rob Pike给出的编程建议,值得用心感悟。

  1. 你没有办法预测每个程序的运行时间,瓶颈会出现在出乎意料的地方,所以在分析瓶颈原因之前,先不要盲目猜测。
  2. 测试(measure)。在测试之前不要优化程序,即使在测试之后也要慎重,除非一部分代码占据绝对比重的运行时间。
  3. 花哨的算法在 n 比较小时效率通常比较糟糕,而 n 通常是比较小的,并且这些算法有一个很大的常数。除非你确定 n 在变大,否则不要用花哨的算法。即便 n 不变大,也要先遵循第 2 个原则。
  4. 相对于朴素的算法来说,花哨的算法更容易出现Bug,更难调试。尽量使用朴素的算法和数据结构。
  5. 数据占主导地位(Data dominates)。如果你选择了正确的数据结构,并且已经把事情组织好,那么算法的效率显而易见。编程的核心是数据结构,不是算法。

参考阅读:

https://www.zhihu.com/tardis/bd/art/551945410

https://view.inews.qq.com/k/20230914A08H0P00?no-redirect=1&web_channel=wap&openApp=false

(完)