从慢到 SIMD:一个 Go 优化故事

在 Go 中针对点积等操作进行手动 SIMD 优化,既展示了性能潜力,也暴露了该语言在高性能数值计算方面的当前局限。评论者将 Go 缺乏自动向量化和易用 SIMD 抽象,与 C++、Rust、C# 和 Java 生态进行对比,讨论显式 intrinsics、汇编或通过 FFI 调用 C/BLAS 风格库,哪种路径更实用。讨论还呈现了围绕通过底层优化实现纵向扩展、通过更多机器实现横向扩展,以及依赖汇编或 cgo 带来的维护和可移植性成本之间的更广泛权衡。

Go 编译器、SIMD 与优化模型

  • 多位评论者指出,Go 仍然缺乏自动向量化,且循环优化整体较弱(例如有限的循环展开),与 C/C++/Rust/C#/Java 相比,不太适合高端数值计算。
  • 也有人认为这可能是“隐藏的福气”,因为向量化器很难做到正确,而且容易脆弱或出错。
  • 多位参与者更倾向于显式的 SIMD 抽象(类似 ISPC、intrinsics、可移植 SIMD 类型),而不是“魔法般”的自动向量化。

与其他语言和库的比较

  • C/C++:现代编译器在使用 -O3-march 时,通常可以自动向量化点积,尤其是在使用 fast-math 标志时。手写 SIMD 仍然可能通过利用微架构细节胜过自动向量化。
  • Rust:迭代器可以优化得很好;整数循环可以向量化,但由于结合律规则,浮点归约会被阻止,除非使用特殊的 intrinsics 或 nightly 特性。可移植 SIMD 很有前景,但尚未完全稳定。
  • C#:拥有稳定的 intrinsics 和可移植 SIMD,以及辅助库。Java 的 Panama vectors 仍处于预览阶段,但因运算缓慢或缺失而受到批评。
  • BLAS/Gonum:Go 的 BLAS float32 点积性能超过了该博客中的浮点版本,但由于缺少 int8 点积,对量化向量来说无法使用。
  • 提到的其他工具:Halide(调度解耦)、Google Highway、适用于其他语言的 SimSIMD,以及用于生成 Go 汇编的 avo。

fast-math 与数值语义

  • 关于类似 -ffast-math 标志的热烈讨论:它们对向量化浮点归约是必要的,但可能会微妙地改变结果,并且历史上在翻译单元之间有“传染性”影响。
  • 建议的替代方案包括:更窄的标志(-fassociative-math-fno-signed-zeros)、按函数属性、专门的“fast float”类型,或具有显式语义的稳定 intrinsics。

Go 切片、容量与微观细节

  • 关于 Go 的完整切片语法 a[i:j:k] 的讨论:它会限制容量,可能避免一些额外的容量计算,但并不能可靠地消除边界检查。
  • 切片容量复用在追加时可能导致令人意外的别名行为,有些人觉得这不直观或不安全;也有人认为切片很好地封装了 C 的 指针+长度(+容量) 惯用法。

FFI、汇编与扩展性考虑

  • 有些人更倾向于通过 cgo 使用 C(或 BLAS/Halide),而不是手写汇编;另一些人则因为工具链、静态二进制、可移植性和调试复杂性而避免使用 cgo。
  • 建议把跨核心并行化作为 SIMD 之外另一条直接的加速路径。
  • 更广泛的一点是:垂直优化(单节点速度)可以显著降低基础设施成本,尤其对于单位经济性糟糕的 SaaS 而言。