首页
/ Go SIMD 规格驱动迁移路线图:从 simdgen 的 categories.yaml 到可执行的 spec 包

Go SIMD 规格驱动迁移路线图:从 simdgen 的 categories.yaml 到可执行的 spec 包

2026-09-05 09:07:20作者:宣海椒Queenly

Go 标准库正在实验性地引入 simdsimd/archsimd 包,其 API 规模庞大、横跨 amd64/arm64/sve/wasm 多平台。src/simd/internal/spec/TASKS.md 记录了整个代码生成体系的迁移路线图:把原先基于 YAML 文件(simdgen 的 categories.yaml)描述 SIMD 操作的方式,逐步替换为一个用可执行 Go 代码写成的"规格包"(spec 包),使其成为所有导出 API 的唯一事实来源。读懂这篇任务清单,就能理解 Go SIMD 包"一套规格、多端生成"的架构演进方向、当前进度(如已完成的 refgen 参考实现)以及后续待办事项。

背景:两套 API 描述体系并存

Go 的 SIMD 支持目前分为两层(参见 src/simd/doc.go 的包文档,整个 simd 包带 //go:build goexperiment.simd 构建标签,需要实验开关才能使用):

  • simd:面向用户的可移植 API,向量长度无关(至少 128 位),提供 Int8sFloat64s 等类型,底层由硬件(如 ARM NEON、AVX2/AVX512)实现或纯 Go 模拟;
  • simd/archsimd:面向具体架构的 API,例如 Float32x4,直接对应汇编指令能力。

这两个包以及 wasm 目标,历史上都由 src/simd/archsimd/_gen/ 下的生成器统一产生,其中最核心的是 simdgen:它从一组 YAML 文件读取操作定义。例如 categories.yaml 目前的内容就只有一行聚合导入:

!import ops/*/categories.yaml

即把 simdgen/ops/ 目录下 45 个 YAML 文件中的操作定义合并起来。YAML 方案的问题是:操作的数学语义、跨类型推广规则只能靠文本约定表达,无法直接被执行和测试。

于是团队引入了新方案——src/simd/internal/spec/ 下的 spec 包,而 TASKS.md 正是这次迁移的工作清单。它在开头明确承认:"This spec is quite incomplete!"(本规格尚不完整),计划是把 categories.yaml 的内容全部迁入 spec 并最终删除 categories.yaml

spec 包:用可执行 Go 代码写成的"极大主义"规格

TASKS.md 提出迁移时必须仔细考虑三件事,这三点也定义了 spec 包的设计原则:

  1. 每个操作的文档注释:逐步转向更正式的、对数学操作给出规范化描述的风格;
  2. 操作如何跨类型、跨宽度推广,尤其推广到 scalable vectors:spec 被刻意设计为 maximalist(极大主义)——把每种架构"能做的上界"全部圈定下来;
  3. 哪些操作是架构相关的:明确架构依赖的边界。

泛型化的操作规格

spec/doc.go 的说明,spec 包"描述 SIMD API 中所有可能操作的函数与方法签名、文档注释以及行为(写成参考性的 Go 实现)",并强制"一个名字只意味着一件事"(one name means one thing)跨所有平台、跨两个包保持一致。规格本身不打算被直接调用specgen 工具解释这个 spec 包,产出完整的 API 描述,再喂给其他生成器;而 spec 的可执行部分则用于精确规定操作语义、支撑一致性测试。

spec 操作以"重度使用类型参数"的程式化风格编写,让单个函数就能描述一个跨多种向量类型推广的操作。以 Add 为例(doc.go 第 30–58 行):

// Add adds corresponding elements of two vectors.
func AddE Nums, W Width (z Vec[E, W]) {
    ...
}

约定:首参类型为 Vec 的规格函数,表示向量类型上的方法。类型参数 E Nums 约束元素类型(任意尺寸的数字类型,uint8float64 等),W Width 约束向量总位宽。该规格会被展开(expand)为所有满足约束的具体 API 方法,例如:

func (Int8x16)  Add(Int8x16)  Int8x16
func (Int8x32)  Add(Int8x32)  Int8x32
func (Float64x8) Add(Float64x8) Float64x8
func (Int8s)    Add(Int8s)    Int8s    // scalable
...
func (Float64s) Add(Float64s) Float64s

specgen:requires 类型约束指令

有些操作在类型参数之间存在 Go 类型系统难以表达的关系,此时用 //specgen:requires 指令约束。doc.go 以 DotProductPairs 为例:

// DotProductPairs computes the dot product of x and y.
//
//specgen:require z={xB}{xN*2}x{xL/2}
func DotProductPairsE Nums, W Width, zE Nums (z Vec[zE, W]) {
    ...
}

约束表达式通过参数名引用每个参数/结果的类型,Vec(及 Array)参数还会派生一组变量:

变量 含义 示例(Uint32x8
v 整个向量类型 Uint32x8
vE 元素类型 uint32
vB 基础类型名 uint
vN 基础类型位宽 32
vL 通道数 / 数组元素数 8
vW 向量总位宽 256

形状约束有三种形式:BaseNxL(L 个 BaseN 元素)、BaseNwW(总宽 W 位)、BaseNs(scalable 向量)。其中 BaseNLW 都可为字面量或 {} 包围的表达式,例如 Uint32x{vL}{zB}{zN}w128。对 DotProductPairs,约束 z={vB}{vN*2}x{vL/2} 等价于"结果 z 与 v 同基础类型、元素类型宽度翻倍、通道数减半",也等价于 z={vB}{vN*2}w{vW}zB=vB zN=vN*2 zL=vL/2zE={vB}{vN*2} zW=vW 等写法。完整语法见 specgen/specexpr 包

名字与文档模板

当 API 名或文档注释依赖类型参数时,spec 支持简单的模板系统,在 {} 中引用约束变量。例如(doc.go 第 116–124 行):

//specgen:name Load{z}
func LoadZE Elt, W Width (z Vec[E, W]) {

规格函数本身可任意命名(只要导出),API 名由指令模板生成:当 LoadZuint32 + Width128 上实例化时,生成名即为 LoadUint32x4。这类模板特别适合构造器、转换函数等必须把类型写进名字的场景。

spec 的类型系统

spec/types.go 可以看到规格层的核心类型定义:

  • 元素类型Floats(float32/float64)、Ints(int8–int64)、Uints(uint8–uint64)、Nums(三者之并);
  • 掩码类型Mask8/Mask16/Mask32/Mask64,在 spec 内部是"宽掩码"(wide mask)——元素逻辑上是布尔,合法取值只有 0^0;生成器把它们翻译成 API 中的 Mask 类型;
  • 宽度Width128/256/512WidthScalable。scalable 在规格层面完全符号化,而执行 spec 时被具体解释为 4096 位scalableWidth = 4096,types.go 第 92–107 行——刻意取一个大于任何支持平台的值,以便测试发现假设了固定宽度的错误);
  • Vec[E EltOrMask, W Width]:实现为一个 Go 切片,长度必须是 width[W]() / elemBits[E]()
  • 另有 Array[E, W](翻译成 API 中的 [L]E 数组类型)和 UintN(位宽由约束决定的 uint 类型);指针与切片类型直接映射到 API。

规格的语义由真实 Go 实现保证,例如 mathlib.go 提供了带饱和运算的 saturateaddSaturated 等辅助函数(配合 math/bits 处理溢出/进位),并配有 basic_test.go、mathlib_test.go 等测试;spec 目录还有 math.goconverts.goshuffles.goloadstore.gomasks.goreinterp.gocombinators.go 等按主题划分的规格文件。

TASKS.md 的九项迁移任务逐项解读

任务清单(TASKS.md 第 14–31 行)是本次迁移的权威进度表。逐项对照当前仓库状态:

# 任务 状态 仓库证据
1 把 specgen 接入 simdgen,过渡期内 union categories.yaml 输入与 specgen 输入 未开始 simdgen/main.go 仍只消费 YAML
2 categories.yaml 迁移为 spec 未开始 ops/*.yaml 45 个文件仍在
3 弄清如何表示编译器专用(非 API)的 simdgen 操作 未开始
4 删除 categories.yaml 未开始(依赖 1、2) categories.yaml 仍存在
5 让 specgen 进入其他生成器(tmplgenwasmgen),使 spec 成为所有导出 API 的唯一事实来源 未开始 tmplgenwasmgen 尚独立
6 构建工具校验手写 API(如模拟实现)与 spec 一致;一种设想是手写代码留在 archsimd 内但改成非导出函数,生成器补上薄薄一层导出的 API 胶水 未开始 对照 simd/simd_emulated.go 等手写文件可理解"emulations"所指
7 为测试生成完整参考实现:提供 SIMD API 但只是包装 spec 包 已完成(勾选) refgen → 生成 simdref.go
8 生成 archsimd API 对 spec 测试层的一致性测试(conformance tests) 未开始

值得注意的依赖关系:任务 4(删除 YAML)必须排在任务 1、2 之后,任务 5 的"唯一事实来源"目标又依赖于任务 3 解决编译器内部操作(如 simdgen 生成的汇编/SSA 规则类产物 面向编译器而非导出 API)如何表示的问题;任务 8 的一致性测试则直接构建在任务 7 的参考实现之上。

已完成项深潜:refgen 参考实现的生成原理

任务 7 是清单中唯一勾选完成的事项,其产物是理解整个 spec 体系如何"落地"的最佳入口。refgen/main.go 的文件头注释自述:"refgen produces a reference implementation of the SIMD API backed by the spec implementation."

从源码结构看,其工作流程为:

  1. 定位并加载 spec:无参数时调用 specgen.MustFindSpecDir() 自动找到 spec 目录,再用 specgen.Load(specDir, nil) 把 spec 包解析为一组带类型信息的规格函数;
  2. 收集向量类型:遍历所有规格函数,把出现过的接收者 Vec 类型去重并排序(同元素类型按宽度比较,fixed 宽度排在 scalable 之前,见 main.go 第 66–97 行);
  3. 生成类型:为每种向量类型在 simd/internal/simdref/simdref.go 中生成形如 type <Name> struct { v []<elem> } 的结构体,其中掩码元素类型映射为 spec.Mask8 等;
  4. 生成方法:为每个 spec 函数生成"薄壳",把 simdref 的结构体参数转成 spec 包类型、调用 spec 的实现函数(spec 内部以切片执行语义)、再把结果转回来。

生成的 simdref.go 因此提供了一套与正式 SIMD API 同形、但纯由 spec 语义驱动的参考实现——后续任务 8 的 archsimd 一致性测试即可拿它当"标准答案"对照真实硬件实现。doc.go 还提到另一个调试入口:cmd/speclssrc/simd/archsimd/_gen/cmd/specls/main.go),用于查看 spec 生成的 API 形态、排查规格问题。

此外,_gen 目录下还能看到这套工具链的周边:simdgen 本体(含 go_amd64.yamlgo_arm64.yamlgo_sve.yaml 等平台规则与 fetch-xed.sh/fetch-arm64.sh 等指令集数据获取脚本)、负责把多来源操作取交集的 midway 以及做约束求解的 unify。任务 1 所说的"union categories.yaml 与 specgen 输入",正是要让 simdgen 在过渡期内同时消费两套来源。

这套迁移对读者的实际意义

  • 理解 API 命名Int8x16Float64s 这类看似随意的名字,实际来自 spec 的 E(元素)× W(宽度)笛卡尔展开与 {} 命名模板,而非人工逐个定义;
  • 追溯语义来源:某个方法的数学行为(如饱和加法)以可执行 Go 代码和测试形式存在于 spec 包archsimdsimd 只是它的子集投影——这正是 doc.go 所说"archsimd 和 simd 包是该规格 API 的子集"的含义;
  • 评估成熟度simd 包仍挂在 goexperiment.simd 实验标签下,spec 包自己也承认"quite incomplete",上述九项任务中七项未勾选,说明当前仍处于生成体系重构期,API 与文档以仓库现状为准。

如何在仓库中继续深入

从"YAML 文本描述"到"可执行 Go 规格 + 单一事实来源",TASKS.md 勾勒出 Go SIMD 工具链的核心工程决策:把 API 的正确性从"生成器约定"升级为"可运行、可测试的代码",而 refgen 参考实现的完成,为后续的跨平台一致性验证打下了第一块基石。

登录后查看全文
热门项目推荐
相关项目推荐