ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Google Benchmark 汇编测试实战:用 LLVM FileCheck 守护基准测试库的代码生成质量

Google Benchmark 汇编测试实战:用 LLVM FileCheck 守护基准测试库的代码生成质量 性能测试【免费下载链接】benchmarkA microbenchmark support library项目地址https://gitcode.com/gh_mirrors/benchmark5/benchmark点击查看免费下载Google Benchmark 这类基准测试库有一个特殊问题它的一些核心函数如DoNotOptimize、ClobberMemory存在的唯一意义就是影响编译器生成什么样的机器码。普通的单元测试无法验证这一点——必须检查编译器实际生成的汇编输出。本篇基于 docs/AssemblyTests.md 系统讲解这套汇编测试机制的工作原理如何为测试编写CHECK指令、tools/strip_asm.py如何清洗汇编输出、CMake 如何自动驱动 FileCheck 校验以及如何编写能容忍不同编译器差异的可移植测试。读完本文你将掌握为 C 项目编写汇编级回归测试的完整方法论。为什么需要汇编级测试基准测试的正确性高度依赖编译器没有把被测代码优化掉。库中承担这一职责的函数有DoNotOptimize阻止某个值或表达式被优化器消除且要求引入尽可能小的开销ClobberMemory强制冲刷对全局内存的待写操作充当读/写屏障KeepRunning基准循环控制其生成代码的质量直接决定循环开销。从 include/benchmark/benchmark.h 的源码可以看到DoNotOptimize在 GCC/Clang 上是通过内联汇编实现的空指令template class Tp inline BENCHMARK_ALWAYS_INLINE void DoNotOptimize(Tp value) { #if defined(__clang__) asm volatile( : r,m(value) : : memory); #else asm volatile( : m,r(value) : : memory); #endif }这种实现的效果完全由编译器决定同一个调用GCC 可能生成movl $101, %eaxClang 可能生成movl $101, -8(%rsp)。因此库必须用汇编测试来锁定每个编译器版本的输出形态任何上游版本升级导致的代码生成变化都会被捕获。仓库中目前有三个汇编测试文件test/donotoptimize_assembly_test.cctest/clobber_memory_assembly_test.cctest/state_assembly_test.cc测试的解剖结构两步写法编写一个汇编测试只需两步写出你希望生成汇编的目标代码在代码中插入// CHECK注释行描述要匹配的汇编内容。文档给出的最小示例// CHECK-LABEL: test_add: extern C int test_add() { extern int ExternInt; return ExternInt 1; // CHECK: movl ExternInt(%rip), %eax // CHECK: addl %eax // CHECK: ret }CHECK-LABEL锚定函数入口以函数名后的冒号标识该函数后续的CHECK行按顺序在汇编输出中做前向搜索匹配。匹配工作由 LLVM 的FileCheck工具完成它读取测试源文件中的CHECK指令与编译器产出的.s文件逐条比对。真实测试中的典型用例来自 test/donotoptimize_assembly_test.cc// CHECK-LABEL: test_with_lvalue: extern C void test_with_lvalue() { int x 101; benchmark::DoNotOptimize(x); // CHECK-GNU: movl $101, %eax // CHECK-CLANG: movl $101, -{{[0-9]}}(%[[REG:[a-z]]]) // CHECK: ret }这里正是编译器差异的直观体现GCC 把立即数直接放进寄存器Clang 则先压栈再使用。编写技巧Tips and Tricks文档总结了五条实用规则最小化匹配原则CHECK不要求紧跟上一条匹配之后的下一行因此应省略对无关汇编的检查只匹配足以确立正确性的最小输出。需要精确相邻匹配时使用CHECK-NEXT只测试优化后输出测试以-O3 -g0编译见下文 CMake 配置不关心未优化代码汇编先经清洗生成物会由 tools/strip_asm.py 进一步处理剔除注释、汇编指令directive和未使用的标签清洗后的汇编文件位置build-directory/test/test-name.s失败时可直接查看该文件比对差异利用 CHECK 前缀FileCheck 支持--check-prefixesBenchmark 使用CHECK-CLANG与CHECK-GNU分别匹配 Clang 或 GCC 的输出普通CHECK则对所有编译器生效。注意CHECK-NOT和CHECK-LABEL不是前缀它们本身就是不带前缀的指令变体。此外测试函数统一使用extern C关闭名称修饰name mangling让函数名在CHECK行中可直接书写不必写形如_Z12test_addv的修饰名。编写可移植测试的三种武器不同编译器、不同版本可能生成完全不同的代码测试必须容忍这种差异。FileCheck 提供了三种机制1. 捕获变量Named Variables当 GCC 把变量存在寄存器而 Clang 存内存时可以先捕获操作数的实际形式再用捕获结果匹配后续指令// CHECK-LABEL: test_div_no_op_into_shr: extern C void test_div_no_op_into_shr(int value) { int divisor 2; benchmark::DoNotOptimize(divisor); // hide the value from the optimizer return value / divisor; // CHECK: movl $2, [[DEST:.*]] // CHECK: idivl [[DEST]] // CHECK: ret }[[DEST:.*]]在movl $2行捕获了目的操作数无论是%ecx还是-4(%rsp)后续idivl [[DEST]]复用捕获值。仓库中的真实用例test_div_by_twotest/donotoptimize_assembly_test.cc写法完全一致。2. 正则表达式匹配编译器间常常只有细微差异比如栈帧偏移量。此时在CHECK行中嵌入正则int ExternInt; struct Point { int x, y, z; }; // CHECK-LABEL: test_store_point: extern C void test_store_point() { Point p{ExternInt, ExternInt, ExternInt}; benchmark::DoNotOptimize(p); // CHECK: movl ExternInt(%rip), %eax // CHECK: movl %eax, -{{[0-9]}}(%rsp) // CHECK: movl %eax, -{{[0-9]}}(%rsp) // CHECK: movl %eax, -{{[0-9]}}(%rsp) // CHECK: ret }{{[0-9]}}匹配任意偏移数字从而同时容忍两种编译器给出的不同栈布局。test_with_large_lvalue中捕获寄存器本身[[REG:[a-z]]]也是同一思路的延伸。3. CHECK-DAG 检查乱序匹配当两条指令的相对顺序在不同编译器间可能互换时用CHECK-DAG组做非顺序匹配。test/clobber_memory_assembly_test.cc 中的test_redundant_store正是此模式// CHECK-DAG: ExternInt // CHECK-DAG: movl $3 // CHECK: movl $51它验证ClobberMemory()阻断了冗余存储消除优化两次写入都保留而test_redundant_read则用CHECK-NOT: ExternInt2反向验证屏障未引入多余的重复读。CMake 驱动的测试流水线整套机制由 CMake 自动编排核心是 test/AssemblyTests.cmake 中的add_filecheck_test宏。流程如下每个测试被编译为对象库附加编译标志-S -O3 -g0 -fno-stack-protector每个标志先经check_cxx_compiler_flag探测编译器是否支持——这与文档所述以-O3 -g0编译完全对应add_custom_target调用tools/strip_asm.py把对象文件-S生成的汇编清洗为CMAKE_CURRENT_BINARY_DIR/name.s通过add_test注册 FileCheck 检查命令形如FileCheck donotoptimize_assembly_test.cc \ --input-filebuild/test/donotoptimize_assembly_test.s \ --check-prefixesCHECK,CHECK-CLANG # 或 CHECK-GNU--check-prefixes中的第二个前缀由编译器 ID 动态决定TOUPPER后的CLANG或GNU从而同一个测试文件在两种编译器下只激活各自的分支行。test/CMakeLists.txt 中注册了三个测试并做了硬性前置检查if (BENCHMARK_ENABLE_ASSEMBLY_TESTS) if (NOT LLVM_FILECHECK_EXE) message(FATAL_ERROR LLVM FileCheck is required when including this file) endif() include(AssemblyTests.cmake) add_filecheck_test(donotoptimize_assembly_test) add_filecheck_test(state_assembly_test) add_filecheck_test(clobber_memory_assembly_test) endif()启用条件何时自动打开根目录 CMakeLists.txt 的should_enable_assembly_tests函数按顺序过滤只有全部满足才会把BENCHMARK_ENABLE_ASSEMBLY_TESTS默认置为ON构建类型不匹配coverage源码注释指出--coverage会改变代码生成测试会失败非 MSVCCMAKE_SYSTEM_PROCESSOR匹配x86_64指针宽度为 8 字节64 位构建32 位目前 FIXME 不支持PATH上能find_program(LLVM_FILECHECK_EXE FileCheck)。任一条件不满足测试整体禁用而非报错因此文档中Filecheck 不在 PATH 上时测试被禁用正是这一逻辑的结果。用户也可显式传入-DBENCHMARK_ENABLE_ASSEMBLY_TESTSON/OFF覆盖默认值——显式开启但缺少 FileCheck 会触发FATAL_ERROR。strip_asm.py让汇编可比对清洗脚本 tools/strip_asm.py 是保证跨平台可比对的关键。从源码看它做三类事标签处理transform_labelsELF 生成.L1这样的局部标签MachO 生成不带点的L1。脚本先把未使用的.L标签行整行删除只有被跳转指令引用的标签才保留见find_used_labels并把 MachO 风格标签统一补点归一化标识符归一化process_identifiersMachO 会给所有符号加一个前导下划线如_test_addELF 则可能带 Itanium 修饰的_Z前缀。函数会剥掉这两个差异使同一份CHECK行在 Linux 和 macOS 上都能匹配剔除噪声行process_asm中的discard_regexes删除汇编指令行.text、.globl等、注释行、#APP/#NO_APP内联汇编标记、数据定义指令.string、.quad等并额外处理 MachO 的GOTPCREL属性。这一步解释了为什么文档要求匹配最小输出——经过清洗后的.s文件已经去掉了大量环境相关噪声测试只需关注指令本身。状态、循环与版本约束test/state_assembly_test.cc 验证的是KeepRunning()循环生成的代码它更复杂地组合了前述所有武器用[[CALL:call(q)*]]捕获call/callq差异GNU 用callClang 用callq用CHECK-NEXT锁定StartKeepRunning调用后紧跟的testq %rbx, %rbx条件判断用CHECK-GNU-NEXT/CHECK-CLANG-NEXT区分循环计数递减的写法GCC 生成subq $1, %rbxClang 生成incq %rax或addq $-1, %rbx并用CHECK-DAG组容忍while循环出口跳转指令的顺序差异。最后版本敏感性需要明确test/AssemblyTests.cmake 中写明了基准版本——Clang5.0.0、GCC5.5.0编译器版本与基准不符时仅打印 Assembly tests may be broken 警告而不中断构建。这正是文档当前要求与限制一节的实现依据测试要求FileCheck 在构建机 PATH 上否则整体禁用目标仅限x86_64编译器仅限GCC 或 Clang指定了--coverage或-fsanitize等改变代码生成的额外标志的构建会失败根 CMake 在 coverage 构建类型下直接跳过自动启用从 test/BUILD 可以看到 Bazel 构建目前通过 glob 排除*_assembly_test.cc并留有 FIXME 注释即汇编测试尚未在 Bazel 下启用。扩展到其他架构/编译器的路径在文档中已指明方向借助 FileCheck 的CHECK前缀机制为新的目标平台添加CHECK-NAME行即可无需改动现有测试结构。小结Google Benchmark 的汇编测试体系是一条完整流水线-O3 -g0 -S编译出汇编 →strip_asm.py清洗标签、标识符与指令噪声 → FileCheck 按CHECK/CHECK-CLANG/CHECK-GNU前缀做顺序匹配。它的工程价值在于用捕获变量、正则和CHECK-DAG吸收编译器间的合理差异把代码生成质量变成了可持续回归验证的对象。对于任何核心逻辑依赖编译器行为的 C 库这套模式都可以直接复用。赞分享性能测试【免费下载链接】benchmarkA microbenchmark support library项目地址https://gitcode.com/gh_mirrors/benchmark5/benchmark点击查看免费下载相关推荐Google Benchmark 汇编级测试Assembly Tests完全指南用 LLVM FileCheck 校验 DoNotOptimize 等函数的机器码输出Google Benchmark 汇编级测试Assembly Tests完全指南用 LLVM FileCheck 校验 DoNotOptimize 等函数性能测试开发工具用 LLVM FileCheck 守护 JAX 原语到 MLIR 的降级JAX 轻量级降级回归测试指南用 LLVM FileCheck 守护 JAX 原语到 MLIR 的降级JAX 轻量级降级回归测试指南 JAX 的核心工作方式之一是将 PythonNum机器学习深度学习OpenCodeInterpreter HumanEval基准测试全面评估代码生成质量OpenCodeInterpreter HumanEval基准测试全面评估代码生成质量 OpenCodeInterpreter项目中的HumanEval基上一篇最完整ExoPlayer DRM配置指南Widevine/L1-L3切换与离线许可证管理下一篇CANN/pypto矩阵乘法操作创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表