Go随笔 | 常见语句性能分析
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循环判断条件
- 在for循环中,判断是否继续循环的条件,往往是一个表达式,执行时候会每次循环都计算一次表达式结果吗?看下面两种写法:
|
|
编译成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
看看汇编指令一模一样;不用担心了吧,推荐第一种写法。
- 如果条件判断是一个函数呢?
|
|
结果如下:
# 第三种
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
比较之后发现,虽然不同写法有些许差异,但是经过编译优化之后,这些写法的性能几乎是一样的。
- 再看下面这个局部变量是切片的例子
|
|
汇编结果如下:
# 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
)申请局部变量不会占用堆栈空间;编译时候都被优化掉了。局部变量用指针反而会增加寄存器、栈指针的指令,不推荐使用。
- 思考,如果循环比较的是map集合又如何呢?
|
|
自己查看汇编结果,会复杂很多。看的出来slice和map是完全不同的实现逻辑,for循环判断条件如果是个函数,而且编译器无法推断优化的话;将每次都调用函数,再用函数返回结果做判断比较。
同时这里只能感叹,编译器的优化做了很多事情,可做的也还有很多。很多语言性能不好不是语言的问题,而是没有人给优化编译器。
结论:
- for循环判断表达式是否提出作为局部变量,用栈内存,对性能几无影响。怎么写简单、好阅读就怎么来。
- 但判断表达式结果可能变化的话,提出变量将改变循环的逻辑,请谨慎甄别。
- for循环判断表达式如果是函数(包括len(Array)这种),而且结果可能变化,将每次循环都调用函数返回值来判断。
for循环和range迭代
先看下面两种写法:
|
|
汇编指令结果是一模一样,看下面的结果。
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
如果改变数组的数据类型又如何呢?
|
|
编译结果还是和上面一模一样,很显然编译器做了推断优化。那么我们继续变,增加结构体的内存占用大小:
|
|
此时汇编的结果发现Range方式的指令比For更复杂,在无法准确判断两种汇编结果性能差异的情况下,我们用基准测试:
|
|
我们如果改写迭代项的值试一试呢?
|
|
我猜for循环,通过索引找到了每个user对象,将其中name[0]的值修改了,因为数据量大,会有频繁换页影响性能了。而range方式,编译器猜测到user项的迭代是对象拷贝,每个user对象都是栈上的内存,而且修改这个值也是无意义的,那么编译器很可能就将这个逻辑优化掉了。
如果加大实体占用内存的大小,会出现啥情况呢?
|
|
协程栈空间是可以动态变化的,即使可变栈,局部变量占用内存过大可能就会逃逸分配到堆上,目前看极值可能是10MB。
协程栈在新版实现中默认是2KB,其可变范围是多大呢?下面这个错误可供参考(1GB):
|
|
如果迭代项都改成对象引用会如何呢?
|
|
再研究下for;;
下面的两种写法:
|
|
上面试验看出下面两点:
- 大部分情况下
for;;
和for...range
的写法性能是一样的。 - 当迭代的结构体内存占用过大时,range方式每次迭代因为是对象拷贝,会占用协程栈空间。
结论:
- 无论何时推荐用
for;;
循环,用for...range
只迭代索引也可 - 为提高代码可读性,很多时候定义局部变量是好办法。
switch和if…else
看一个简单的例子吧:
|
|
两种写法汇编结果一模一样:
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
性能是一样的,喜欢啥就用啥吧。
再看下面常见的一个例子
|
|
经过基准测试,发现两个函数性能并不一样,CompareStr2总是比CompareStr1略好,当然差异很小。汇编结果发现两者的输出逻辑其实是有差异的。
结论:
if判断语句在用 && 或 || 做级联时,如果表达式本身就存在运算,用分级if判断些许能提高一点点性能。
type断言类型转换
先看整形互转
|
|
结果如何呢,看下面的汇编语句,差异挺小。总之数值(整形、浮点型)之间随意转换的性能损耗都是极小的。
"".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
下面看看断言和反射
|
|
写个基准测试看看
|
|
结果如下
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
用反射的时候发生了一次堆分配,当然整体性能差的太多了。有意思的是下面这个测试:
|
|
当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
结论:
- 整形浮点型之间类型转换开销很小,可以忽略不计。
- .(type)断言类型判断并转换开销也不大,至少要比用反射操作要快。
- 一旦涉及堆内存分配和资源回收,性能会下降非常明显。即使栈针移动也会消耗CPU。
- 反射性能比较差,要谨慎使用。因为每个反射函数调用,涉及大量逻辑判断,甚至堆资源分配。
条件判断
|
|
测试结果如下:
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
结论:
- 这几种方式其实都很快,至少整形数是这样,太多的if…else…分支稍微影响性能。
- 相对来说位运算又简洁又快。
再看一个例子,JSON编解码中常见的字符比对,猜猜谁的性能更好:
|
|
一组可能的结果如下:
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
结论:
- 有时候位操作性能并没有你所想象中的好。
- 数组索引性能不错,而且不同输入组合,性能一致,只不过需要占用一段全局内存空间。
- 直接比值加||运算,在首个条件就满足时,反而性能最好。平均比对到第二和第三个条件时,性能和数组索引接近。
- 当判断条件需要比较两三组值的时候,直接比较往往性能最好。
很多司空见惯的问题,往往不是你想的那样。需要测试。
(完)
PS:下面是Go发明者之一Rob Pike给出的编程建议,值得用心感悟。
- 你没有办法预测每个程序的运行时间,瓶颈会出现在出乎意料的地方,所以在分析瓶颈原因之前,先不要盲目猜测。
- 测试(measure)。在测试之前不要优化程序,即使在测试之后也要慎重,除非一部分代码占据绝对比重的运行时间。
- 花哨的算法在 n 比较小时效率通常比较糟糕,而 n 通常是比较小的,并且这些算法有一个很大的常数。除非你确定 n 在变大,否则不要用花哨的算法。即便 n 不变大,也要先遵循第 2 个原则。
- 相对于朴素的算法来说,花哨的算法更容易出现Bug,更难调试。尽量使用朴素的算法和数据结构。
- 数据占主导地位(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
(完)
- 原文作者: 闪电侠
- 原文链接:https://chende.ren/2023/03/06164620-009-asm-compare.html
- 版权声明:本作品采用 开放的「署名 4.0 国际 (CC BY 4.0)」创作共享协议 进行许可