前几日,有群友转发了某位技术大佬的weibo。并在群里询问如下两个函数哪个执行的速度比较快(weibo内容)。
func g(n int, ch chan<- int) {
r := 0
for i := 0; i < n; i++ {
r += i
}
ch <- r + 0
}
func f(n int, ch chan<- int) {
r := 0
for i := 0; i < n; i++ {
r += i
}
ch <- r
}
很显然,g函数中ch <- r + 0 比 f函数中 ch <- r 多了一个+0
g、f的for循环都执行了n次,对r进行更新
那到底哪个快呢?我们搞了一组Benchmark测试
环境如下:
go version: 1.20
go os: windows
go arch: arm64
代码如下:
package main
import "testing"
func BenchmarkG(b *testing.B) {
ch := make(chan int)
N := 100000
for i := 0; i < b.N; i++ {
go g(N, ch)
}
}
func BenchmarkF(b *testing.B) {
ch := make(chan int)
N := 100000
for i := 0; i < b.N; i++ {
go f(N, ch)
}
}
为了显现出性能差异,我们直接将g、f两个函数中for循环次数 N 设定为100000(十万次)。
执行结果如下
从结果可以看出:
g函数在单位时间,总共执行了167175次,每次耗时7148ns
f函数在单位时间,总共执行了71856次,每次耗时23909ns
很显然,g函数的执行效率更胜一筹
那为什么会产生这样的结果呢?
话不多说,直接上大招
使用:go tool compile -S ./main.go > dump.txt
将目标go文件的汇编写入dump.txt
下面截取了g函数的主要汇编代码
main.g STEXT size=112 args=0x10 locals=0x28 funcid=0x0 align=0x0
0x0000 00000 (main.go:11) TEXT main.g(SB), ABIInternal, $48-16
...
0x0018 00024 (main.go:11) MOVD ZR, R2
0x001c 00028 (main.go:11) MOVD ZR, R3
0x0020 00032 (main.go:13) JMP 48
0x0024 00036 (main.go:13) ADD $1, R2, R4
0x0028 00040 (main.go:14) ADD R2, R3, R3
0x002c 00044 (main.go:13) MOVD R4, R2
0x0030 00048 (main.go:13) CMP R2, R0
0x0034 00052 (main.go:13) BGT 36
0x0038 00056 (main.go:16) MOVD R3, main..autotmp_4-8(SP)
0x003c 00060 (main.go:16) MOVD R1, R0
0x0040 00064 (main.go:16) MOVD $main..autotmp_4-8(SP), R1
0x0044 00068 (main.go:16) PCDATA $1, $1
0x0044 00068 (main.go:16) CALL runtime.chansend1(SB)
0x0048 00072 (main.go:17) LDP -8(RSP), (R29, R30)
0x004c 00076 (main.go:17) ADD $48, RSP
0x0050 00080 (main.go:17) RET (R30)
下面截取了f函数的主要汇编代码
main.f STEXT size=112 args=0x10 locals=0x28 funcid=0x0 align=0x0
0x0000 00000 (main.go:3) TEXT main.f(SB), ABIInternal, $48-16
...
0x0018 00024 (main.go:4) MOVD ZR, main.r-8(SP)
0x001c 00028 (main.go:4) MOVD ZR, R2
0x0020 00032 (main.go:5) JMP 52
0x0024 00036 (main.go:6) MOVD main.r-8(SP), R3
0x0028 00040 (main.go:6) ADD R2, R3, R3
0x002c 00044 (main.go:6) MOVD R3, main.r-8(SP)
0x0030 00048 (main.go:5) ADD $1, R2, R2
0x0034 00052 (main.go:5) CMP R2, R0
0x0038 00056 (main.go:5) BGT 36
0x003c 00060 (main.go:8) MOVD R1, R0
0x0040 00064 (main.go:8) MOVD $main.r-8(SP), R1
0x0044 00068 (main.go:8) PCDATA $1, $1
0x0044 00068 (main.go:8) CALL runtime.chansend1(SB)
0x0048 00072 (main.go:9) LDP -8(RSP), (R29, R30)
0x004c 00076 (main.go:9) ADD $48, RSP
0x0050 00080 (main.go:9) RET (R30)
对比一下
不难看出,g函数在循环结构中,只使用了R0、R2、R3、R4寄存器。
f函数在循环结构中,使用了R0、R2、R3寄存器,并在单次循环内,操作了两次栈内存
0x0024 00036 (main.go:6) MOVD main.r-8(SP), R3
将main.r-8(SP)栈内存对应的内容,加载进R3寄存器
0x002c 00044 (main.go:6) MOVD R3, main.r-8(SP)
将R3寄存器的内容写入,main.r-8(SP)栈内存
因为CPU读写内存的速度远低于读写寄存器的速度,所以在大样本量的数据驱动下,g函数的执行速度要远快于f函数的执行速度。
至于为什么出现该性能差异,究其根本,是Go编译器、优化器、对源码编译导致的,也就是编译器的黑魔法使然。