bigywater.tech
Go · 15

类型断言与类型开关:一次断言要花多少纳秒

“类型断言很慢”是一个流传很广的说法。这篇先给一条基线,再分别量具体类型断言、接口类型断言和类型开关,用汇编看它们各自做了什么,最后看 case 数量与位置对耗时有没有影响。

≈ 基线
具体类型断言 1.97–2.02 ns/op,空函数调用基线 1.95 ns/op
+0.15~0.45 ns
接口类型断言(CALL runtime.typeAssert)比基线多出的量
位置无关
8 个具体类型的 switch,命中第 1 个与第 8 个都是 1.96–1.97 ns/op

先量基线,再量断言

实测 · 本机执行输出
  基线:调用 //go:noinline 空函数(不断言)         中位 1.95 ns/op(最小 1.94 / 最大 1.97)
  x.(A) 命中                             中位 2.02 ns/op(最小 1.94 / 最大 3.39)
  v, ok := x.(A) 命中                    中位 1.97 ns/op(最小 1.95 / 最大 2.01)
  v, ok := x.(A) 未命中(装的是 H)            中位 1.98 ns/op(最小 1.97 / 最大 2.05)
  x.(Mer) 命中(接口类型断言)                   中位 2.33 ns/op(最小 2.30 / 最大 2.39)
  m, ok := x.(Mer) 命中                  中位 2.40 ns/op(最小 2.39 / 最大 2.42)
  m, ok := x.(Other) 未命中               中位 2.10 ns/op(最小 2.09 / 最大 2.11)
  switch 2 个具体类型:命中第 1 个               中位 1.98 ns/op(最小 1.97 / 最大 2.00)
  switch 8 个具体类型:命中第 1 个(A)            中位 1.98 ns/op(最小 1.97 / 最大 1.99)
  switch 8 个具体类型:命中第 8 个(H)            中位 1.99 ns/op(最小 1.97 / 最大 2.06)
  switch 2 个接口类型:命中第 2 个               中位 2.36 ns/op(最小 2.35 / 最大 2.37)
  AllocsPerRun:断言命中 0 次,未命中 0 次

单次断言的耗时已经小到和调用本身的开销混在一起,所以没有基线的“xx 纳秒”没有意义。这里每个函数都是 //go:noinline,基线就是调用一个空函数。需要诚实地说明的是:这些数字是紧循环里装在 any 里的固定值,分支预测与缓存都很理想,真实程序里的值会更分散(上一篇看到类型随机时接口调用从约 2 ns 变成 7 ns)。

汇编里的差别

实测 · 本机执行输出
  main.assertConcrete:
    (<gtmp>/ta2/main.go:36)	CMP	R16, RSP
    (<gtmp>/ta2/main.go:36)	MOVD	$type:main.A(SB), R3
    (<gtmp>/ta2/main.go:36)	CMP	R3, R0
    (<gtmp>/ta2/main.go:36)	MOVD	$type:interface {}(SB), R2
    (<gtmp>/ta2/main.go:36)	CALL	runtime.panicdottypeE(SB)
  main.assertConcreteOK:
    (<gtmp>/ta2/main.go:40)	MOVD	$type:main.A(SB), R2
    (<gtmp>/ta2/main.go:40)	CMP	R2, R0
  main.assertIfaceOK:
    (<gtmp>/ta2/main.go:50)	CMP	R16, RSP
    (<gtmp>/ta2/main.go:51)	CBZ	R0, 60
    (<gtmp>/ta2/main.go:51)	CBZ	R0, 88
    (<gtmp>/ta2/main.go:52)	CALL	(R2)
    (<gtmp>/ta2/main.go:51)	CMP	R7, R0
    (<gtmp>/ta2/main.go:51)	CBNZ	R7, 104
    (<gtmp>/ta2/main.go:51)	CALL	runtime.typeAssert(SB)
  main.switch2:
    (<gtmp>/ta2/main.go:67)	CBZ	R0, 100
    (<gtmp>/ta2/main.go:67)	CMPW	R3, R2
    (<gtmp>/ta2/main.go:68)	MOVD	$type:main.A(SB), R2
    (<gtmp>/ta2/main.go:68)	CMP	R2, R0
    (<gtmp>/ta2/main.go:67)	CMPW	R3, R2
    (<gtmp>/ta2/main.go:70)	MOVD	$type:main.B(SB), R2
    (<gtmp>/ta2/main.go:70)	CMP	R2, R0
  main.switchIface:
    (<gtmp>/ta2/main.go:100)	CMP	R16, RSP
    (<gtmp>/ta2/main.go:101)	CBZ	R0, 60
    (<gtmp>/ta2/main.go:101)	CMP	R8, R0
    (<gtmp>/ta2/main.go:101)	CBNZ	R8, 76
    (<gtmp>/ta2/main.go:101)	CALL	runtime.interfaceSwitch(SB)
    (<gtmp>/ta2/main.go:101)	CBZ	R0, 184
    (<gtmp>/ta2/main.go:101)	CMP	$1, R0
    (<gtmp>/ta2/main.go:105)	CALL	(R2)
    (<gtmp>/ta2/main.go:103)	CALL	(R2)

具体类型断言在汇编里就是一次比较:把类型字与 type:main.A 的地址比较,v, ok := 形式甚至没有任何调用。接口类型断言则要判断一个动态类型是否实现了某个接口,这件事交给 runtime.typeAssert(调用之前还有几条比较和跳转,我没有逐条核对它们在做什么)。类型开关在汇编里先比较类型的 hash(CMPW),命中再比较类型地址确认;含接口类型的 case 走 runtime.interfaceSwitch。

case 的数量与顺序

实测 · 本机执行输出
  switch8 命中第 1 个                      中位 1.96 ns/op(最小 1.96 / 最大 1.97)
  switch8 命中第 2 个                      中位 1.96 ns/op(最小 1.96 / 最大 1.97)
  switch8 命中第 3 个                      中位 1.96 ns/op(最小 1.96 / 最大 1.97)
  switch8 命中第 4 个                      中位 1.97 ns/op(最小 1.96 / 最大 1.97)
  switch8 命中第 5 个                      中位 1.96 ns/op(最小 1.96 / 最大 1.97)
  switch8 命中第 6 个                      中位 1.96 ns/op(最小 1.96 / 最大 1.97)
  switch8 命中第 7 个                      中位 1.97 ns/op(最小 1.96 / 最大 2.05)
  switch8 命中第 8 个                      中位 1.97 ns/op(最小 1.95 / 最大 1.97)
  switch8 全部未命中(装 int)                 中位 2.01 ns/op(最小 2.01 / 最大 2.02)

8 个具体类型的 switch 命中每一个都是同样的耗时,说明编译器没有把它编译成从上到下逐个比较;汇编里能看到的是 hash 比较(CMPW),至于具体用二分还是跳转表,我没有逐条核对。结论只限于这个实验:在 8 个 case 内,case 的位置不影响耗时。

分步演示

类型断言与类型开关
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。每个函数 //go:noinline,装在 any 里的值固定;每个变体 7 轮 testing.Benchmark 取中位数;汇编取自 go build -gcflags=-S(arm64)
    • 没有验证:amd64 上的汇编与耗时、case 数远多于 8 个时的行为、值在多种类型间随机变化时的断言成本、runtime.typeAssert 的内部实现、泛型代码中的类型断言
    京ICP备2026067502号-1京公网安备11010502063487号