V 语言原子操作基准测试实战:x.atomics 库 amd64 与 i386 双平台性能对比与源码解析
导读
本文以 V 语言官方仓库中 vlib/x/atomics 实验性库的基准测试文档(vlib/x/atomics/benchmarks/README.md)为骨架,完整讲解该库在 AMD64 与 i386 两个平台下的原子操作性能实测方法、1 亿次迭代的完整测试数据,并结合 atomics.amd64.v 与 atomics.i386.v 的汇编实现,剖析 lock xadd、lock cmpxchg、cmpxchg8b、MMX movq 等底层指令的差异。读完本文,你将掌握如何在任意机器上复现这套基准测试、如何读懂 ns/op 数据,以及理解"内联汇编原子操作"相对于"C FFI 原子操作"的取舍与代价。
一、背景:x.atomics 是一个怎样的库
在深入基准测试之前,需要先明确被测对象的性质。根据 vlib/x/atomics/README.md,x.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)逐操作对比的方法论,核心机制如下:
- 对比基线:程序通过
#include引入 V 仓库内置的thirdparty/stdatomic头文件(Windows 用 thirdparty/stdatomic/win/atomic.h,其他平台用 thirdparty/stdatomic/nix/atomic.h),并声明C.atomic_store_u32、C.atomic_load_u32、C.atomic_fetch_add_u32、C.atomic_exchange_u32、C.atomic_compare_exchange_strong_u32及对应的 64 位版本作为"std"基线; - 同构函数签名:每个操作都包装成
fn (&u32, u32)或fn (&u64, u64)形式的函数(例如std_add_u64调用C.atomic_fetch_add_u64,custom_add_u64调用atomics.add_u64),保证两个被测方走完全相同的调用路径; - 预热 + 计时:先用 10 万次调用预热(warm up),再启动
time.new_stopwatch()计时,循环执行iterations = 100_000_000(1 亿)次; - 防止优化吞噬:计时结束后调用
keepalive_u64/u32/i64/i32——一个含asm volatile+nop的空汇编函数,将最终结果以寄存器约束r (x)传给汇编,避免编译器因结果未被使用而删除整个循环(dead-code elimination); - 结果输出:
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.v。std 表示经 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.v 与 atomics.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/or→lock cmpxchg尝试写回 → 失败则jnz跳回重试(标签'3b'循环)。这正是文档 README 中提到的"predictable and inspectable code generation"的体现:每条操作对应何种指令序列,从源码一眼可查。
6.2 I386 实现:三种特殊手段
i386 是 32 位架构,64 位原子操作没有 native 单指令可用,于是 atomics.i386.v 采用了三种手法:
- 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_i64在emms后还附加了一条xor eax, eax; lock xaddl [esp], eax——从源码结构看,这是用一次锁定的栈内存原子操作充当内存屏障,以补足 MMX store 缺失的序语义,实现文档承诺的"顺序一致性"; cmpxchg8b实现 64 位 CAS/交换/加法:cas_u64/cas_i64、swap_u64/swap_i64、add_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 上 customadd与swap成本较高的直接原因;- 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.v、i64_test.v、i32_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.v、examples/spinlock.v 中的"期望值 vs 实际值"校验模式互相印证,共同说明:性能基准展示的是该库在顺序一致性语义下的实测开销,而正确性测试展示的是同一套实现的无锁并发可靠性,两者缺一不可。
八、复现、解读与适用前提
复现步骤(需先安装 V 编译器,并确保 amd64 场景有 gcc、i386 场景有 i686-linux-gnu-gcc 交叉工具链):
- 进入
vlib/x/atomics目录; - 执行上文第三节的 amd64 或 i386 命令(注意 i386 需确认 CPU 支持 MMX);
- 对比输出中的
ns/op列,或直接观察每组 std/custom 的差异。
解读数据时的三条原则:
- 平台敏感:本数据基于 AMD Ryzen 9 9950X3D + Linux 6.18.6 内核测得,换硬件、换内核、开/关
-prod都会改变绝对值;本文的表格应视为"某台特定机器上的快照",而非通用性能承诺; - 读相对差:更有价值的是同平台上 std 与 custom 的相对差异——amd64 上两者几乎持平,i386 上 custom 在 64 位 store/load 占优、在 cas 略逊,这恰好对应两种实现策略在不同指令集下的真实成本;
- 区分实验性质:
x.atomics是实验性库(README 自述 "This repository is an experiment"),目前仅 amd64/i386 两架构、仅顺序一致性一种内存序,生产项目中选用前应结合自身架构需求评估。
总体而言,这份基准测试文档给出了一个可复现、可检验的评测框架:atomic_benchmark.v 的"同签名对比 + 预热 + 1 亿次迭代 + keepalive 防优化"方法论,可直接迁移到任何原子库的评测中;而 amd64 与 i386 两组数据,则为理解"单指令原子原语"与"CAS 循环 / MMX 访存"两类实现策略的性能边界提供了直接证据。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00