首页
/ V 语言原子操作基准测试实战:x.atomics 库 amd64 与 i386 双平台性能对比与源码解析

V 语言原子操作基准测试实战:x.atomics 库 amd64 与 i386 双平台性能对比与源码解析

2026-09-09 10:53:43作者:魏侃纯Zoe

导读

本文以 V 语言官方仓库中 vlib/x/atomics 实验性库的基准测试文档(vlib/x/atomics/benchmarks/README.md)为骨架,完整讲解该库在 AMD64 与 i386 两个平台下的原子操作性能实测方法、1 亿次迭代的完整测试数据,并结合 atomics.amd64.vatomics.i386.v 的汇编实现,剖析 lock xaddlock cmpxchgcmpxchg8b、MMX movq 等底层指令的差异。读完本文,你将掌握如何在任意机器上复现这套基准测试、如何读懂 ns/op 数据,以及理解"内联汇编原子操作"相对于"C FFI 原子操作"的取舍与代价。


一、背景:x.atomics 是一个怎样的库

在深入基准测试之前,需要先明确被测对象的性质。根据 vlib/x/atomics/README.mdx.atomics 是 V 生态中一个实验性的低层原子操作库,其核心定位是:

  • 纯 V 内联汇编实现:不依赖 C FFI,直接使用 V 的 asm volatile 特性生成机器指令;
  • 显式 i386 支持:在 i386 上需要 MMX 指令集(README 原文明确说明 "explicit i386 support (MMX required on i386)");
  • 当前仅支持 amd64 与 i386,其他架构尚未实现;
  • 全部操作目前提供顺序一致性(sequentially consistent)语义,尚未实现 relaxed / acquire / release 等弱内存序。

其动机在于:V 现有生态中原子操作通过调用 C 实现(如本基准测试中对比的 thirdparty/stdatomic),这种方式虽然可用,但引入了对 C 工具链与头文件的依赖,并且无法精确控制生成的机器指令。x.atomics 探索的是另一条路——用 V 自己的内联汇编直接生成架构相关的原子指令,从而让代码生成"可预测、可检查"。

该库目前提供的操作集合与覆盖面(来自 README 的 "Available Operations" 表格):

操作 i32 i64 u32 u64
load_* 支持 支持 支持 支持
store_* 支持 支持 支持 支持
add_* 支持 支持 支持 支持
swap_* 支持 支持 支持 支持
cas_* 支持 支持 支持 支持
and_* 支持 支持 支持 支持
or_* 支持 支持 支持 支持

完整可运行的 API 示例可参阅 vlib/x/atomics/examples/basic.v,以及两个多线程实战示例:counter.v(4 线程 × 10000 次 add_i64 计数校验)与 spinlock.v(基于 cas_u32 自旋锁)。


二、基准测试环境:被测硬件与编译配置

基准测试文档明确记录了测试环境的完整规格(这些数据来自 benchmarks/README.md,是该项目实测环境的描述,不代表所有平台的普遍结论):

项目 配置
CPU AMD Ryzen 9 9950X3D(16 核 / 32 线程)
内存 64 GiB
操作系统 EndeavourOS(Linux,kernel 6.18.6-arch1-1)
amd64 编译命令 v -prod -cc gcc -gc none
i386 编译命令 v -keepc -cc i686-linux-gnu-gcc -prod -m32 -arch i386 -cflags -mmmx -w -gc none

两个平台编译命令的差异值得注意:

  • -prod:启用生产模式优化,是获得真实性能数据的必要前提;
  • -gc none:关闭 GC(垃圾回收),排除回收器对计时的影响;
  • -cc gcc(amd64):指定后端 C 编译器为 GCC;
  • i386 交叉编译参数:使用 i686-linux-gnu-gcc 交叉工具链,-m32 -arch i386 生成 32 位代码,-cflags -mmmx 显式启用 MMX 指令集(i386 上 64 位原子访问依赖 MMX,见下文源码分析),-keepc 保留中间 C 代码便于检查。

如果你要在其他机器上复现,应把 CPU/内存/内核视为变量——数据会因硬件微架构与内核版本不同而变化,但方法论与命令可直接沿用。


三、如何运行基准测试

文档给出的运行命令分为 amd64 与 i386 两套,位于仓库根目录执行:

# amd64
v -prod -cc gcc -gc none run benchmarks/atomic_benchmark.v

# i386
v -keepc -cc i686-linux-gnu-gcc -prod -m32 -arch i386 -cflags -mmmx -w -gc none run benchmarks/atomic_benchmark.v

其中基准程序本体位于 vlib/x/atomics/benchmarks/atomic_benchmark.v(实际运行时路径从 vlib/x/atomics/ 目录下执行,即 benchmarks/atomic_benchmark.v)。

3.1 基准程序如何工作

从源码看,该基准采用标准库(std,C FFI)与自定义库(custom,x.atomics)逐操作对比的方法论,核心机制如下:

  1. 对比基线:程序通过 #include 引入 V 仓库内置的 thirdparty/stdatomic 头文件(Windows 用 thirdparty/stdatomic/win/atomic.h,其他平台用 thirdparty/stdatomic/nix/atomic.h),并声明 C.atomic_store_u32C.atomic_load_u32C.atomic_fetch_add_u32C.atomic_exchange_u32C.atomic_compare_exchange_strong_u32 及对应的 64 位版本作为"std"基线;
  2. 同构函数签名:每个操作都包装成 fn (&u32, u32)fn (&u64, u64) 形式的函数(例如 std_add_u64 调用 C.atomic_fetch_add_u64custom_add_u64 调用 atomics.add_u64),保证两个被测方走完全相同的调用路径;
  3. 预热 + 计时:先用 10 万次调用预热(warm up),再启动 time.new_stopwatch() 计时,循环执行 iterations = 100_000_000(1 亿)次;
  4. 防止优化吞噬:计时结束后调用 keepalive_u64/u32/i64/i32——一个含 asm volatile + nop 的空汇编函数,将最终结果以寄存器约束 r (x) 传给汇编,避免编译器因结果未被使用而删除整个循环(dead-code elimination);
  5. 结果输出ns_per_op := f64(elapsed.nanoseconds()) / f64(iters),即用总耗时纳秒数除以迭代次数得到每次操作的平均纳秒数。

值得留意的是 std 侧的 CAS 包装:std_cas_u64 内部维护一个 mut expected := u64(0) 变量并取其地址传给 atomic_compare_exchange_strong_u64,每次调用 expected 都被重置为 0,因此与 custom 侧的 atomics.cas_u64(addr, 0, val)(固定期望值 0)在语义上对齐,保证对比公平。

3.2 i64/i32 与 u64/u32 的对应关系

main() 的调用顺序可见,测试按 u64 → u32 → i64 → i32 四组依次执行,每组各测 store / load / add / swap / cas 五个操作。i64 与 i32 的 std 侧实现均通过 unsafe 块把有符号类型按位重解释为无符号类型后复用 u64/u32 的 C 原子函数(如 C.atomic_store_u64(voidptr(addr), u64(val))),这解释了输出行中 "via u64" / "via u32" 的标注。


四、AMD64 实测结果(100M 次迭代,ns/op)

以下数据完整引自 benchmarks/README.md 的 AMD64 部分,命令为 v -prod -cc gcc -gc none run atomic_benchmark.vstd 表示经 C FFI 调用的 stdatomic 基线,custom 表示 x.atomics 内联汇编实现。

u64 系列:

操作 std (ns/op) std 总耗时 custom (ns/op) custom 总耗时
store 3.788 378.783ms 3.773 377.301ms
load 1.078 107.848ms 1.084 108.381ms
add 3.601 360.067ms 3.782 378.213ms
swap (exchange) 3.805 380.520ms 3.835 383.493ms
cas 3.824 382.391ms 3.783 378.264ms

u32 系列:

操作 std (ns/op) std 总耗时 custom (ns/op) custom 总耗时
store 3.783 378.346ms 3.822 382.245ms
load 1.084 108.427ms 1.085 108.536ms
add 3.663 366.308ms 3.857 385.722ms
swap (exchange) 3.855 385.503ms 3.859 385.892ms
cas 3.870 387.025ms 3.837 383.680ms

i64 系列(std 经 u64):

操作 std (ns/op) std 总耗时 custom (ns/op) custom 总耗时
store (via u64) 3.784 378.377ms 3.790 378.993ms
load (via u64) 0.935 93.519ms 0.864 86.350ms
add (via u64) 3.608 360.752ms 3.843 384.319ms
swap (exchange u64) 3.826 382.621ms 3.835 383.513ms
cas (via u64) 3.840 383.988ms 3.847 384.733ms

i32 系列(std 经 u32):

操作 std (ns/op) std 总耗时 custom (ns/op) custom 总耗时
store (via u32) 3.840 383.983ms 3.841 384.068ms
load (via u32) 1.100 109.956ms 1.100 109.978ms
add (via u32) 3.659 365.907ms 3.846 384.574ms
swap (exchange u32) 3.848 384.830ms 3.836 383.562ms
cas (via u32) 3.837 383.690ms 3.815 381.453ms

AMD64 数据小结(仅对上述实测数据的客观归纳)load 类操作约 0.86–1.10 ns/op,明显快于写类操作;store / add / swap / cas 均落在约 3.6–3.9 ns/op 区间;std 与 custom 两组数据在大多数操作上差距在 ±5% 以内,说明在 amd64 上"内联汇编原子操作"与"C FFI 原子操作"的实际执行开销基本持平,差异更多来自调用包装与指令序列的细微差别,而非质的差距。


五、I386 实测结果(100M 次迭代,ns/op)

以下数据完整引自 benchmarks/README.md 的 I386 部分,命令为 v -keepc -cc i686-linux-gnu-gcc -prod -m32 -arch i386 -cflags -mmmx -w -gc none run benchmarks/atomic_benchmark.v

u64 系列:

操作 std (ns/op) std 总耗时 custom (ns/op) custom 总耗时
store 9.575 957.485ms 7.703 770.251ms
load 1.769 176.860ms 1.892 189.238ms
add 5.544 554.431ms 5.310 530.964ms
swap 5.320 531.988ms 5.242 524.175ms
cas 4.948 494.824ms 5.268 526.833ms

u32 系列:

操作 std (ns/op) std 总耗时 custom (ns/op) custom 总耗时
store 3.896 389.574ms 4.067 406.712ms
load 1.132 113.242ms 1.135 113.523ms
add 3.951 395.090ms 4.141 414.139ms
swap 4.136 413.586ms 4.138 413.812ms
cas 4.136 413.644ms 4.705 470.505ms

i64 系列:

操作 std (ns/op) std 总耗时 custom (ns/op) custom 总耗时
store 9.643 964.327ms 7.373 737.327ms
load 1.700 169.983ms 1.824 182.396ms
add 5.270 526.950ms 5.268 526.833ms
swap 4.917 491.690ms 5.095 509.546ms
cas 4.931 493.122ms 5.268 526.774ms

i32 系列:

操作 std (ns/op) std 总耗时 custom (ns/op) custom 总耗时
store 4.018 401.776ms 4.138 413.820ms
load 1.132 113.185ms 1.134 113.359ms
add 3.949 394.853ms 4.139 413.947ms
swap 4.135 413.485ms 4.136 413.623ms
cas 4.137 413.702ms 4.704 470.402ms

I386 数据小结(仅对上述实测数据的客观归纳):i386 上 64 位操作普遍比 32 位操作更贵——最明显的是 64 位 store(std 约 9.6 ns/op,custom 约 7.4–7.7 ns/op),这是因为 32 位模式下 64 位访存没有单指令支撑,必须依赖 MMX 或 cmpxchg8b 等特殊手段(详见下文源码解析);custom store_u64/store_i64 相对 std 有约 20%–24% 的降幅,而 cas 则相反,std 略优于 custom。这组数据恰好说明了 i386 是"两种实现策略差异最显著"的平台。


六、源码级原理解析:这些性能差异从何而来

基准测试数字背后,是 atomics.amd64.vatomics.i386.v 两套内联汇编实现的直接体现。从源码结构看,两个平台采用了几类不同的指令策略:

6.1 AMD64 实现:单指令原子原语

  • add_*lock xadd [rdx], eax,再 add eax, delta 得到新值——x86 的 lock 前缀保证读-改-写不被打断;
  • swap_* / store_*xchg [rdx], eax(xchg 与内存操作数配合时自动带总线锁,无需显式 lock 前缀),一次指令同时完成"取旧值/存新值";
  • cas_*lock cmpxchg [rdx], ecx(64 位用 lock cmpxchgq [rdx], rcx),随后 sete al 根据零标志位生成布尔结果;
  • and_* / or_*:没有对应的单指令读-改-写形式,因此采用CAS 自旋循环——mov 读出旧值 → 计算 and/orlock cmpxchg 尝试写回 → 失败则 jnz 跳回重试(标签 '3b' 循环)。这正是文档 README 中提到的"predictable and inspectable code generation"的体现:每条操作对应何种指令序列,从源码一眼可查。

6.2 I386 实现:三种特殊手段

i386 是 32 位架构,64 位原子操作没有 native 单指令可用,于是 atomics.i386.v 采用了三种手法:

  1. MMX 指令访存 64 位store_u64/load_u64/store_i64/load_i64 使用 movq mm0, [esi] / movq [esi], mm0 一次搬运 8 字节,并在其后执行 emms 清空 MMX 状态。这解释了为什么 i386 64 位 store/load 的 custom 实现能显著快于 std;同时 README 中"MMX required on i386"的硬性要求也由此而来,编译时必须加 -cflags -mmmx。值得注意的细节是 store_i64emms 后还附加了一条 xor eax, eax; lock xaddl [esp], eax——从源码结构看,这是用一次锁定的栈内存原子操作充当内存屏障,以补足 MMX store 缺失的序语义,实现文档承诺的"顺序一致性";
  2. cmpxchg8b 实现 64 位 CAS/交换/加法cas_u64/cas_i64swap_u64/swap_i64add_u64/add_i64 均把 64 位值拆成高低两个 32 位(value_lo/value_hi,注意 V 中高低位组合为 u64(res_lo) | (u64(res_hi) << 32)),通过 lock cmpxchg8b [esi] 原子比较交换 8 字节;add_i64 额外用 add ebx, delta_lo; adc ecx, delta_hi 处理进位,并以 jnz 组成 CAS 重试循环。这类循环型实现正是 i386 上 custom addswap 成本较高的直接原因;
  3. 32 位操作与 amd64 同构add_u32/swap_u32/store_u32/load_u32/cas_u32 使用 lock xadd/xchg/lock cmpxchg 单指令,与 amd64 行为一致。

6.3 对齐检查:panicUnaligned

两个平台的每个操作入口都有一段相同的对齐守卫:

mov rdx, dest
test rdx, 3     ; 32 位操作检查低 2 位;64 位操作为 test rdx, 7
jz '1f'         ; 对齐则跳转执行
call panicUnaligned
jmp '2f'
1: ...

未对齐时调用 vlib/x/atomics/panic_unaligned.v 中导出的 panicUnaligned 函数,触发 panic('unaligned atomic operation')。该文件还包含一段条件编译逻辑:在 prod && (gcc || clang) 下添加链接标志 -Wl,--undefined=panicUnaligned,确保这个被汇编直接 call 的符号在最终二进制中一定被链接。这也是"内联汇编原子操作"相较 C FFI 更精细控制生成的典型体现——从指令到符号链接都显式管理。

6.4 内存模型

vlib/x/atomics/README.md 明确指出:当前所有操作都是**顺序一致(sequentially consistent)**的,即所有操作呈现全局一致的全序;未实现 relaxed / acquire / release 等弱语义;未来若引入弱变体,会显式命名并在文档中说明。因此当前版本的 API 使用上不需要(也不支持)像 C11/C++ 那样指定 memory order。


七、测试验证:并发正确性有据可依

性能数字之外,正确性由 vlib/x/atomics/u64_test.v(以及同目录的 u32_test.vi64_test.vi32_test.v)保障,测试文件头部标注 // vtest build: !macos && !windows && (amd64 || i386),说明其运行范围为 amd64/i386 的 Linux 类平台。典型用例包括:

  • 单线程基本语义test_add_u64_return 验证 add_u64 返回的是加后新值且内存同步更新;test_cas_u64_fail 验证期望值不匹配时返回 false 且不改写内存;test_add_u64_wraparound 验证 64 位回绕(0xffffffffffffffff + 1 == 0);
  • 并发正确性test_add_u64_concurrent 用 8 个线程各执行 10 万次 add_u64(px, 1),断言最终值恰为 800,000——若任何一次加法丢失,该断言必然失败;test_cas_u64_concurrent_inc 演示经典"load + CAS 重试"的无锁自增范式;test_and_u64_concurrent / test_or_u64_concurrent 验证位操作的幂等收敛性质。

这些测试与 examples/counter.vexamples/spinlock.v 中的"期望值 vs 实际值"校验模式互相印证,共同说明:性能基准展示的是该库在顺序一致性语义下的实测开销,而正确性测试展示的是同一套实现的无锁并发可靠性,两者缺一不可。


八、复现、解读与适用前提

复现步骤(需先安装 V 编译器,并确保 amd64 场景有 gcc、i386 场景有 i686-linux-gnu-gcc 交叉工具链):

  1. 进入 vlib/x/atomics 目录;
  2. 执行上文第三节的 amd64 或 i386 命令(注意 i386 需确认 CPU 支持 MMX);
  3. 对比输出中的 ns/op 列,或直接观察每组 std/custom 的差异。

解读数据时的三条原则

  1. 平台敏感:本数据基于 AMD Ryzen 9 9950X3D + Linux 6.18.6 内核测得,换硬件、换内核、开/关 -prod 都会改变绝对值;本文的表格应视为"某台特定机器上的快照",而非通用性能承诺;
  2. 读相对差:更有价值的是同平台上 std 与 custom 的相对差异——amd64 上两者几乎持平,i386 上 custom 在 64 位 store/load 占优、在 cas 略逊,这恰好对应两种实现策略在不同指令集下的真实成本;
  3. 区分实验性质x.atomics 是实验性库(README 自述 "This repository is an experiment"),目前仅 amd64/i386 两架构、仅顺序一致性一种内存序,生产项目中选用前应结合自身架构需求评估。

总体而言,这份基准测试文档给出了一个可复现、可检验的评测框架:atomic_benchmark.v 的"同签名对比 + 预热 + 1 亿次迭代 + keepalive 防优化"方法论,可直接迁移到任何原子库的评测中;而 amd64 与 i386 两组数据,则为理解"单指令原子原语"与"CAS 循环 / MMX 访存"两类实现策略的性能边界提供了直接证据。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
docsdocs
暂无描述
Markdown
899
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
525
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395