+0和不+0的性能差异

前几日,有群友转发了某位技术大佬的weibo。并在群里询问如下两个函数哪个执行的速度比较快(weibo内容)。

go 复制代码
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

代码如下:

go 复制代码
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函数的主要汇编代码

text 复制代码
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函数的主要汇编代码

text 复制代码
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编译器、优化器、对源码编译导致的,也就是编译器的黑魔法使然。

相关推荐
程序员Query7 小时前
day01-认识Go语言 + golang环境的安装
开发语言·后端·golang
喵了几个咪1 天前
GoWind Flow 拆解:一个 Go 工作流引擎与它的双模式设计器
开发语言·人工智能·后端·golang
codeejun1 天前
每日一Go·MySQL-14、慢 SQL 产生原因全解
sql·mysql·golang
利威尔xu4 天前
Go map 底层崩溃:3 个 goroutine 并发写同一张 map,fatal error 崩掉整个服务
golang·源码·map·并发·后端开发
利威尔xu5 天前
2026-11-08-Go-defer-panic-recover面试
面试·golang
JWASX6 天前
【Go】map 源码解析 - group
后端·golang·map·源码分析
联犀7 天前
纯 Go 的 CAD 开源库 go-cad:直接解析 DWG/DXF,现在也能导出 PDF
开发语言·golang·pdf
FungLeo7 天前
成为全栈·Go 后端篇·三轮代码审阅之后:把可读性和证据做成工程规则
golang·go·ast·静态检查·成为全栈·代码审阅
联犀7 天前
联犀 Go SIMD 实战:视频转码从 16.5ms 到 5.46ms
开发语言·golang·音视频
紧跟先前的步伐8 天前
Go AI Agent 实战 28|Agent 的预算、超时与停止条件
开发语言·人工智能·后端·ai·golang