拓冰建站拓冰建站
首页 / 资讯中心 / 正文

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

V 语言原子操作基准测试实战x.atomics 库 amd64 与 i386 双平台性能对比与源码解析【免费下载链接】vSimple, fast, safe, compiled language for developing maintainable software. Compiles itself in 1s with zero library dependencies. Supports automatic C V translation. https://vlang.io项目地址: https://gitcode.com/GitHub_Trending/v/v导读本文以 V 语言官方仓库中vlib/x/atomics实验性库的基准测试文档vlib/x/atomics/benchmarks/README.md为骨架完整讲解该库在 AMD64 与 i386 两个平台下的原子操作性能实测方法、1 亿次迭代的完整测试数据并结合 atomics.amd64.v 与 atomics.i386.v 的汇编实现剖析lock xadd、lock cmpxchg、cmpxchg8b、MMXmovq等底层指令的差异。读完本文你将掌握如何在任意机器上复现这套基准测试、如何读懂 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 表格操作i32i64u32u64load_*支持支持支持支持store_*支持支持支持支持add_*支持支持支持支持swap_*支持支持支持支持cas_*支持支持支持支持and_*支持支持支持支持or_*支持支持支持支持完整可运行的 API 示例可参阅 vlib/x/atomics/examples/basic.v以及两个多线程实战示例counter.v4 线程 × 10000 次add_i64计数校验与 spinlock.v基于cas_u32自旋锁。二、基准测试环境被测硬件与编译配置基准测试文档明确记录了测试环境的完整规格这些数据来自 benchmarks/README.md是该项目实测环境的描述不代表所有平台的普遍结论项目配置CPUAMD Ryzen 9 9950X3D16 核 / 32 线程内存64 GiB操作系统EndeavourOSLinuxkernel 6.18.6-arch1-1amd64 编译命令v -prod -cc gcc -gc nonei386 编译命令v -keepc -cc i686-linux-gnu-gcc -prod -m32 -arch i386 -cflags -mmmx -w -gc none两个平台编译命令的差异值得注意-prod启用生产模式优化是获得真实性能数据的必要前提-gc none关闭 GC垃圾回收排除回收器对计时的影响-cc gccamd64指定后端 C 编译器为 GCCi386 交叉编译参数使用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 基准程序如何工作从源码看该基准采用标准库stdC FFI与自定义库customx.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_u64custom_add_u64调用atomics.add_u64保证两个被测方走完全相同的调用路径预热 计时先用 10 万次调用预热warm up再启动time.new_stopwatch()计时循环执行iterations 100_000_0001 亿次防止优化吞噬计时结束后调用keepalive_u64/u32/i64/i32——一个含asm volatilenop的空汇编函数将最终结果以寄存器约束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 总耗时store3.788378.783ms3.773377.301msload1.078107.848ms1.084108.381msadd3.601360.067ms3.782378.213msswap (exchange)3.805380.520ms3.835383.493mscas3.824382.391ms3.783378.264msu32 系列操作std (ns/op)std 总耗时custom (ns/op)custom 总耗时store3.783378.346ms3.822382.245msload1.084108.427ms1.085108.536msadd3.663366.308ms3.857385.722msswap (exchange)3.855385.503ms3.859385.892mscas3.870387.025ms3.837383.680msi64 系列std 经 u64操作std (ns/op)std 总耗时custom (ns/op)custom 总耗时store (via u64)3.784378.377ms3.790378.993msload (via u64)0.93593.519ms0.86486.350msadd (via u64)3.608360.752ms3.843384.319msswap (exchange u64)3.826382.621ms3.835383.513mscas (via u64)3.840383.988ms3.847384.733msi32 系列std 经 u32操作std (ns/op)std 总耗时custom (ns/op)custom 总耗时store (via u32)3.840383.983ms3.841384.068msload (via u32)1.100109.956ms1.100109.978msadd (via u32)3.659365.907ms3.846384.574msswap (exchange u32)3.848384.830ms3.836383.562mscas (via u32)3.837383.690ms3.815381.453msAMD64 数据小结仅对上述实测数据的客观归纳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 总耗时store9.575957.485ms7.703770.251msload1.769176.860ms1.892189.238msadd5.544554.431ms5.310530.964msswap5.320531.988ms5.242524.175mscas4.948494.824ms5.268526.833msu32 系列操作std (ns/op)std 总耗时custom (ns/op)custom 总耗时store3.896389.574ms4.067406.712msload1.132113.242ms1.135113.523msadd3.951395.090ms4.141414.139msswap4.136413.586ms4.138413.812mscas4.136413.644ms4.705470.505msi64 系列操作std (ns/op)std 总耗时custom (ns/op)custom 总耗时store9.643964.327ms7.373737.327msload1.700169.983ms1.824182.396msadd5.270526.950ms5.268526.833msswap4.917491.690ms5.095509.546mscas4.931493.122ms5.268526.774msi32 系列操作std (ns/op)std 总耗时custom (ns/op)custom 总耗时store4.018401.776ms4.138413.820msload1.132113.185ms1.134113.359msadd3.949394.853ms4.139413.947msswap4.135413.485ms4.136413.623mscas4.137413.702ms4.704470.402msI386 数据小结仅对上述实测数据的客观归纳i386 上 64 位操作普遍比 32 位操作更贵——最明显的是 64 位storestd 约 9.6 ns/opcustom 约 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], eaxxchg 与内存操作数配合时自动带总线锁无需显式lock前缀一次指令同时完成取旧值/存新值cas_*lock cmpxchg [rdx], ecx64 位用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,--undefinedpanicUnaligned确保这个被汇编直接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 访存两类实现策略的性能边界提供了直接证据。【免费下载链接】vSimple, fast, safe, compiled language for developing maintainable software. Compiles itself in 1s with zero library dependencies. Supports automatic C V translation. https://vlang.io项目地址: https://gitcode.com/GitHub_Trending/v/v创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门