前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >毫巅之微---不同写法的性能差异 番外篇

毫巅之微---不同写法的性能差异 番外篇

作者头像
fliter
发布2024-01-25 09:17:14
960
发布2024-01-25 09:17:14
举报
文章被收录于专栏:旅途散记

有位知名技术博主贴了一张图片,问两段Go代码的性能优劣:

区别仅在 c<-rc<-r+0,直观感觉是不应该有差异。

做一个性能测试,看看结果

main.go:

代码语言:javascript
复制
package main

func f(n int, c chan<- int) {
 r := 0
 for i := 0; i < n; i++ {
  r += 1
 }
 c <- r
}

func g(n int, c chan<- int) {
 r := 0
 for i := 0; i < n; i++ {
  r += 1
 }
 c <- r + 0
}

demo_test.go:

代码语言:javascript
复制
package main

import (
 "testing"
)

func BenchmarkF(b *testing.B) {
 c := make(chan int)
 n := 1000000

 for i := 0; i < b.N; i++ {
  go f(n, c)
  <-c
 }
}

func BenchmarkG(b *testing.B) {
 c := make(chan int)
 n := 1000000

 for i := 0; i < b.N; i++ {
  go g(n, c)
  <-c
 }
}

执行 go test -test.bench=".*" -benchmem

第4行显示了BenchmarkF 执行了495次,每次的执行平均时间是2097269纳秒, 每次操作有1次内存分配,每次分配了24Byte大小的内存空间

第5行显示了BenchmarkG 执行了3765次,每次的平均执行时间是317891纳秒, 每次操作有1次内存分配,每次分配了24Byte大小的内存空间

即 使用c <- r + 0比使用c <- r 执行时间快了很多...

有点出乎意料,在 go.godbolt.org[1]查看两段代码的汇编 (要加上func main,不然编译不过)

f:

代码语言:javascript
复制
        TEXT    main.main(SB), LEAF|NOFRAME|ABIInternal, $-4-0
        FUNCDATA        $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
        FUNCDATA        $1, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
        JMP     (R14)
main_f_pc0:
        TEXT    main.f(SB), ABIInternal, $12-8
        MOVW    8(g), R1
        PCDATA  $0, $-2
        CMP     R1, R13
        BLS     main_f_pc80
        PCDATA  $0, $-1
        MOVW.W  R14, -16(R13)
        FUNCDATA        $0, gclocals·IuErl7MOXaHVn7EZYWzfFA==(SB)
        FUNCDATA        $1, gclocals·J5F+7Qw7O7ve2QcWC7DpeQ==(SB)
        FUNCDATA        $5, main.f.arginfo1(SB)
        MOVW    $0, R0
        MOVW    R0, main.r-4(SP)
        MOVW    main.n(FP), R1
        JMP     main_f_pc48
main_f_pc32:
        MOVW    main.r-4(SP), R2
        ADD     $1, R2, R2
        MOVW    R2, main.r-4(SP)
        ADD     $1, R0, R0
main_f_pc48:
        CMP     R0, R1
        BGT     main_f_pc32
        MOVW    main.c+4(FP), R0
        MOVW    R0, 4(R13)
        MOVW    $main.r-4(SP), R0
        MOVW    R0, 8(R13)
        PCDATA  $1, $1
        CALL    runtime.chansend1(SB)
        MOVW.P  16(R13), R15
main_f_pc80:
        NOP
        PCDATA  $1, $-1
        PCDATA  $0, $-2
        MOVW    R14, R3
        CALL    runtime.morestack_noctxt(SB)
        PCDATA  $0, $-1
        JMP     main_f_pc0

g:

代码语言:javascript
复制
        TEXT    main.main(SB), LEAF|NOFRAME|ABIInternal, $-4-0
        FUNCDATA        $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
        FUNCDATA        $1, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
        JMP     (R14)
main_g_pc0:
        TEXT    main.g(SB), ABIInternal, $12-8
        MOVW    8(g), R1
        PCDATA  $0, $-2
        CMP     R1, R13
        BLS     main_g_pc68
        PCDATA  $0, $-1
        MOVW.W  R14, -16(R13)
        FUNCDATA        $0, gclocals·IuErl7MOXaHVn7EZYWzfFA==(SB)
        FUNCDATA        $1, gclocals·J5F+7Qw7O7ve2QcWC7DpeQ==(SB)
        FUNCDATA        $5, main.g.arginfo1(SB)
        MOVW    main.n(FP), R0
        MOVW    $0, R1
        JMP     main_g_pc32
main_g_pc28:
        ADD     $1, R1, R1
main_g_pc32:
        CMP     R1, R0
        BGT     main_g_pc28
        MOVW    R1, main..autotmp_4-4(SP)
        MOVW    main.c+4(FP), R0
        MOVW    R0, 4(R13)
        MOVW    $main..autotmp_4-4(SP), R0
        MOVW    R0, 8(R13)
        PCDATA  $1, $1
        CALL    runtime.chansend1(SB)
        MOVW.P  16(R13), R15
main_g_pc68:
        NOP
        PCDATA  $1, $-1
        PCDATA  $0, $-2
        MOVW    R14, R3
        CALL    runtime.morestack_noctxt(SB)
        PCDATA  $0, $-1
        JMP     main_g_pc0

差异如下:

主要区别在于: (来自ChatGPT)

  1. f函数在循环内部定义了一个main.r变量来累加,g函数直接使用i作为累加变量。
  2. f函数传给chansend1调用的是main.r变量地址,g函数直接传i计数器地址。

从性能上看:

  • g函数优于f函数,因为不需要额外定义变量来累加,直接使用临时变量i更高效。
  • g函数通过直接传递i变量地址,也比f函数少了一次内存操作(把r变量地址取出来)。(作者注:但从压测结果看,两个方法内存操作次数一样)

所以总的来说,g函数的实现更加简洁高效,性能上也比f函数好。

通过清晰定义临时变量和避免多余内存操作,g函数的汇编实现利用了机器堆栈和寄存器更好,效率提升明显。这也符合go语言寄存器优先的设计理念。

再细致了解下关键的两段汇编代码的作用:

代码语言:javascript
复制
FUNCDATA        $5, main.f.arginfo1(SB)
        MOVW    $0, R0
        MOVW    R0, main.r-4(SP)
        MOVW    main.n(FP), R1
        JMP     main_f_pc48
main_f_pc32:
        MOVW    main.r-4(SP), R2
        ADD     $1, R2, R2
        MOVW    R2, main.r-4(SP)
        ADD     $1, R0, R0
main_f_pc48:
        CMP     R0, R1

这段汇编代码段是f函数的主体循环部分:

FUNCDATA $5, main.f.arginfo1(SB)

这行声明f函数的参数信息,主要用于支持运行时类型检查。

代码语言:javascript
复制
MOVW $0, R0
MOVW R0, main.r-4(SP) 

这两行将循环计数器R0初始化为0,并写入栈帧中定义的r变量地址。

代码语言:javascript
复制
MOVW main.n(FP), R1
JMP main_f_pc48

读取n参数的值赋给R1,并跳转到主循环入口。

代码语言:javascript
复制
main_f_pc32:
        MOVW    main.r-4(SP), R2
        ADD     $1, R2, R2
        MOVW    R2, main.r-4(SP)
        ADD     $1, R0, R0

循环体内:

  • 将r变量值读取到R2里
  • R2加1模拟累加
  • 写回r变量地址
  • R0计数器加1
代码语言:javascript
复制
main_f_pc48:
        CMP     R0, R1

循环判断:比对计数器和n参数,是否执行完整轮循环。

所以这个代码段实现了f函数中的主循环累加逻辑:

  • 每轮循环都读取更新r变量的值
  • 将结果通过channel发送出去

主要是通过堆栈和几个寄存器交互来实现循环内部的计算。

对于

代码语言:javascript
复制
FUNCDATA        $5, main.g.arginfo1(SB)
        MOVW    main.n(FP), R0
        MOVW    $0, R1
        JMP     main_g_pc32
main_g_pc28:
        ADD     $1, R1, R1
main_g_pc32:
        CMP     R1, R0
        BGT     main_g_pc28

这段汇编代码实现的是g函数的主体循环逻辑:

FUNCDATA $5, main.g.arginfo1(SB)

声明g函数的参数信息

代码语言:javascript
复制
MOVW main.n(FP), R0
MOVW $0, R1 

读取n参数值赋给R0,计数器R1初始化为0

JMP main_g_pc32

跳转到主循环入口

代码语言:javascript
复制
main_g_pc28:
        ADD     $1, R1, R1

循环体内:R1计数器加1

代码语言:javascript
复制
main_g_pc32:
        CMP     R1, R0
        BGT     main_g_pc28

循环判断:比较R1和R0,是否完成n次循环

与f函数不同的是,g函数直接使用循环计数器R1作为累加变量,不需要额外定义变量。

所以整体逻辑是:

  • R1作为循环计数器和累加器
  • 每轮循环内R1自增1
  • 判断是否完成n轮循环

通过寄存器R1实现简单高效的计数和累加,避免了定义额外变量的开销。

这就是g函数循环实现的核心差异。

但u1s1,编译器不该屏蔽这样的细节差异吗...要靠这样犄角旮旯的tricks达到最佳性能,一定程度并不符合Go的理念

推荐阅读:Go 函数调用 ━ 栈和寄存器视角[2]

参考资料

[1]

go.godbolt.org: https://go.godbolt.org/

[2]

Go 函数调用 ━ 栈和寄存器视角: https://studygolang.com/articles/21875

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-01-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 旅途散记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档