Golang Go语言中不同写法的性能差异,不太理解底层实现

https://mp.weixin.qq.com/s/LIdlk0p32iW2KDjGHGflMQ

这最后怎么得出的 r+0 更快的?什么道理....面试时会被问到吗


Golang Go语言中不同写法的性能差异,不太理解底层实现
18 回复

谁问你这个给他两个大逼斗

更多关于Golang Go语言中不同写法的性能差异,不太理解底层实现的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


茴字有几种写法?

这种性能差异我觉得其实体现出的是底层实现的 bug ,毫无意义的代码反而效率更高

普通开发者了解 go 编译器里寄存器的使用就可以了, 学会文中这点对日常开发没什么太大帮助, 赞同文章的最后一句话, 这些细节差异应该是编译器处理没必要暴露给开发者

面试问这种问题就不是想招好好干活的
看上去是多了+0 之后,触发了编译器某个优化逻辑,让上面循环过程中使用的变量停留在寄存器,而不用写回内存再读出来,确实是编译器内部问题,开发者不用管

开头直接猜编译器优化问题,
看到开始汇编了,以为最后会解释,结果没有??

那这是狗屁文章。。。

无责任猜测:

在 g 里传给 chan 的是 (r+0) 是一个「临时」值,r 没有被传递,因此 r 被优化放到了寄存器上。
而在 f 里要传给 chan 的是变量 r ,所以它不能被优化到寄存器里,只能放到栈上。

所以是寄存器优先原则的作用。

补充一点:文章引用 2 里介绍了 Go 函数参数都是「栈传递」,所以上述解释应该正确

一方面我同意 #3 ,更像是编译器的 bug ,另一方面我本地似乎复现不了这个差距如此巨大的结果,估计可以看下 1.21rc 到现在( go 1.21.6 ) 的差异。

而对于一定程度上的差异( 100000ns per op ),单纯从生成代码上来看,f 生成的函数直接对 r 做了修改,所以需要一次对 r 的 load ,而 g 是对一个临时变量做修改,虽然二者都是一次 load 跟一次 store ,但是 r 毕竟不好说分配在哪儿(也许在 heap 上,也许在 register ,看编译器优化),那么 r 确实可能比起临时变量( go 倾向于分配在 register 上)的读写要更慢。至于为什么会有如此差异,实际上应该是因为编译器识别出来了这个累加 pattern ,而在 f 里因为没有额外操作,所以直接对 r 进行操作,把加数这些都当 immediate value 优化成单次 INCQ 了;而在 g 中,由于又读到了 r + 0 ,编译器首先优化成了将其写入中间变量的操作,又在后续 pass 中发现其实对 r 基本无操作,去掉了这里面所有 r 的主动 reference ,将其完全优化到了完全只读写中间变量,所以生成了这个样子的代码。

以上仅抛砖引玉,我不是 plan9 和 go compiler 专家,只能看个大概,这里面同样可能会有很多说不清的其他因素影响。但是我仍然同意,这种 case 应该 report 给官方去修改,而不是当新时代的语言律师模拟考题,同样,如果在乎这个粒度的性能差距,可能我们会选择更精细的语言和优化方式了,而不是在这继续抄写茴字剩下的写法。

同意,除了我觉得编译器优化逻辑只要不出错就不是 bug 。毕竟不优化的地方多了去了……

对 f 要优化的话需要分析整个函数里变量的引用关系(诶…怎么变成 rust 了??

同意楼上,这应该是编译器可以优化的问题,不应该作为一个 feature 存在
谁面试问这种问题,就给他两个大逼斗

语言层面上这两者完全是等价的吧,底层差异这么大那就是编译器的问题了

这样的编译几乎和 g 一样,应该是堆栈导致,go 的编译器确实垃圾。。对比 rust/C/C++

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

进一步信息,这个问题只出现在 1.21 和 1.20 中,其他版本编译没有问题。
根据 https://go.godbolt.org/ 提供的反编译信息

你们上来就争论,却不跑一遍作者的示例代码。
Linux Ubuntu i5-8500 测试结果是:

go1.21.6 下确实如那篇文章所说 g 明显比 f 快。
go1.22rc2 下 g 和 f 效率几乎无差异。

go compiler 的编译优化确实一言难尽,之前测试过,很基础的循环展开都不会做,官方美名其曰「保证编译速度」: https://github.com/golang/go/issues/51302

学到了,蟹蟹大佬们

在Golang(Go语言)中,不同写法的性能差异确实存在,这通常源于Go语言的底层实现机制。理解这些差异,首先需要掌握Go语言的几个核心特性,如编译优化、内存管理、协程调度等。

  1. 编译优化:Go编译器会对代码进行优化,但并非所有写法都能被优化到最佳。例如,循环展开、内联函数等优化技术,对代码结构有一定要求。因此,编写清晰、简洁的代码,有助于编译器进行更好的优化。

  2. 内存管理:Go使用垃圾回收机制管理内存,但频繁的内存分配和回收会影响性能。使用切片、映射等数据结构时,尽量复用内存,减少不必要的分配。此外,避免在循环中创建大量临时对象,以减少GC压力。

  3. 协程调度:Go的协程(goroutine)非常轻量,但大量协程的创建和调度也会带来性能开销。合理使用协程池,控制协程数量,可以避免过度调度带来的性能损耗。

  4. 具体写法:例如,使用切片时,预分配切片容量可以减少内存重新分配的次数;使用map时,选择适当的初始容量可以提高性能。此外,避免使用复杂的嵌套结构,保持代码简洁,也有助于提高性能。

总之,要理解Go语言中不同写法的性能差异,需要深入理解Go语言的底层实现机制,并在实践中不断摸索和优化。通过编写高效、简洁的代码,结合编译器的优化,可以充分发挥Go语言的性能优势。

回到顶部