Golang中使用[3]float32实现的Vec3性能不如结构体
Golang中使用[3]float32实现的Vec3性能不如结构体 我正在处理一些图形相关的项目,并且注意到我的Go代码运行速度不如预期。
经过一番调查,我推测问题可能出在通过值传递我的Vec3上。因此,我将所有Vec3(定义为 [3] float32)分配在堆上,并使用 *Vec3 指针来操作,这使得我的代码快了很多(速度提升了两倍)。
第二天,我决定检查生成的汇编代码,这时我注意到我的代码是通过栈来传递Vec3 [3] float32 的。偶然间,我将Vec3转换为了 struct { X, Y, Z float32 },结果发现Go编译器不再通过栈传递它了(可能改为通过寄存器传递),并且这个结构体版本的速度与指向数组的指针版本一样快,甚至可能更快。
我的问题是,为什么Go编译器对数组版本的处理方式不同?编译器难道不能像处理结构体版本那样,通过寄存器传递它吗?
以下是我的基准测试代码:
package main
import (
"testing"
)
type ArrVec3 [3]float32
func (v ArrVec3) Sub(u ArrVec3) (res ArrVec3) {
res[0] = v[0] - u[0]
res[1] = v[1] - u[1]
res[2] = v[2] - u[2]
return
}
func (v ArrVec3) Mul(t float32) (res ArrVec3) {
res[0] = v[0] - t
res[1] = v[1] - t
res[2] = v[2] - t
return
}
func (v ArrVec3) Dot(u ArrVec3) float32 {
return u[0]*v[0] + u[1]*v[1] + u[2]*v[2]
}
type Vec3 struct {
X, Y, Z float32
}
func (v Vec3) Sub(u Vec3) (res Vec3) {
res.X = v.X - u.X
res.Y = v.Y - u.Y
res.Z = v.Z - u.Z
return
}
func (v Vec3) Mul(t float32) (res Vec3) {
res.X = v.X * t
res.Y = v.Y * t
res.Z = v.Z * t
return
}
func (v Vec3) Dot(u Vec3) float32 {
return u.X*v.X + u.Y*v.Y + u.Z*v.Z
}
func BenchmarkArrVec3(b *testing.B) {
v := ArrVec3{1, 2, 3}
n := ArrVec3{0, 1, 0}
for i := 0; i < b.N; i++ {
v.Sub(n.Mul(v.Dot(n) * 2))
}
}
func BenchmarkVec3(b *testing.B) {
v := Vec3{1, 2, 3}
n := Vec3{0, 1, 0}
for i := 0; i < b.N; i++ {
v.Sub(n.Mul(v.Dot(n) * 2))
}
}
这是godbolt链接:Compiler Explorer 如果我在阅读汇编代码时犯了任何错误,请见谅,这是我第一次阅读Go的汇编代码。
更多关于Golang中使用[3]float32实现的Vec3性能不如结构体的实战教程也可以访问 https://www.itying.com/category-94-b0.html
MoustaphaSaad:
我的问题是,为什么 Go 编译器不区别对待数组版本?编译器难道不能像处理结构体版本那样,将其通过寄存器传递吗?
它很可能可以,但根据我对 Go 内部 ABI 规范 的理解,Go 团队可能没有费心去处理这个问题,具体来说:
非平凡的数组总是通过栈传递,因为对数组进行索引通常需要一个计算出的偏移量,而这通常无法用寄存器实现。总的来说,数组在函数签名中很少见(在 Go 1.15 标准库中仅占 0.7%,在 kubelet 中占 0.2%)。我们考虑过允许数组字段通过栈传递,而参数的其他字段通过寄存器传递,但如果被调用函数获取了参数的地址,这会产生与其他大型结构体相同的问题,并且只会让 kubelet 中不到 0.1% 的函数受益(即使对这些函数来说,收益也微乎其微)。
我不确定他们所说的“非平凡”具体指什么,因为 float32 数组在我看来似乎相当“平凡”,但在这里使用结构体似乎确实是最佳解决方案。
更多关于Golang中使用[3]float32实现的Vec3性能不如结构体的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
在 Go 编译器中,数组和结构体的参数传递优化确实存在差异。根据 Go 的调用约定,当结构体字段较少且大小适中时,编译器会尝试通过寄存器传递,而数组通常通过栈传递。以下是具体分析:
1. 参数传递差异
Go 1.17+ 引入了基于寄存器的调用约定(ABIInternal),对于小型结构体(如包含 2-3 个 float32 字段),编译器会优先使用寄存器传递。但数组目前仍被视为单一值,即使元素较少,也可能通过栈传递。
2. 汇编代码对比
以 Sub 方法为例,结构体版本可能生成如下优化代码(AMD64):
// Vec3.Sub 可能通过寄存器传递参数
MOVSS X0, res.X // 使用 SSE 寄存器
MOVSS X1, res.Y
MOVSS X2, res.Z
而数组版本可能通过栈复制:
// ArrVec3.Sub 可能通过栈复制
MOVQ v+0(FP), AX // 从栈加载地址
MOVQ u+24(FP), BX
MOVSS (AX), X0 // 额外内存访问
3. 性能影响
栈传递会导致:
- 额外的内存读写
- 限制寄存器优化
- 更高的调用开销
4. 验证示例
可通过以下代码观察内存分配差异:
//go:noinline
func testStruct(v Vec3) Vec3 {
return v
}
//go:noinline
func testArray(v ArrVec3) ArrVec3 {
return v
}
func main() {
s := Vec3{1, 2, 3}
a := ArrVec3{1, 2, 3}
println(testStruct(s))
println(testArray(a))
}
使用 go build -gcflags="-m -m" 查看编译器优化决策。
5. 建议方案
对于性能敏感的场景:
- 使用结构体而非数组定义向量
- 考虑使用
-m编译标志分析优化决策 - 对于需要数组语义的情况,可尝试以下包装方式:
type Vec3 struct {
arr [3]float32
}
func (v Vec3) X() float32 { return v.arr[0] }
// 提供类似数组的访问接口,同时保留结构体优化特性
这种差异源于 Go 编译器对类型布局的不同处理,目前结构体在寄存器传递优化方面更具优势。

