ARTICLE DETAIL

资讯详情

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

VLC 仓库 checkasm 测试编写完全指南:从 API 命名到汇编级验证与基准测试

VLC 仓库 checkasm 测试编写完全指南:从 API 命名到汇编级验证与基准测试 音视频【免费下载链接】vlcVLC media player - plays everything, runs anywhere. Code here: https://code.videolan.org/videolan/vlc项目地址https://gitcode.com/gh_mirrors/vl/vlc点击查看免费下载本篇技术指南以 VLC 仓库test/checkasm/ext/docs/writing-tests.md为核心系统讲解如何为 checkasmVideoLAN 自研的汇编 / SIMD 优化实现测试框架编写高质量测试。checkasm 是 VLC 项目中验证 DSP、编解码等优化函数正确性与性能的关键基础设施其完整源码、头文件与自测用例均位于 test/checkasm/ext。读完本文你将掌握 checkasm 的基本测试工作流、现代/传统两套 API 命名、缓冲区分配与初始化、随机参数生成、多配置循环测试、浮点容差比较、越界写检测、MMX 特例处理以及如何用固定种子、函数过滤和多种导出格式组织可复现的基准测试。基本测试结构八步工作流checkasm 的核心思想是在同一个测试进程里同时调用参考实现通常是纯 C 版本和被测实现通常是汇编 / SIMD 优化版本对比输出是否一致并可选地对新实现做基准计时。在深入高级模式之前先熟悉基本测试结构完整可运行示例见 getting-started.md 的 Quick Start 章节。一个典型测试的工作流包含 8 个步骤分配对齐的缓冲区存放测试数据用checkasm_declare()声明被测函数的函数签名用checkasm_check_func()检查该函数是否应当被测试决定要不要跳过初始化测试输入、清空输出缓冲区分别调用参考实现checkasm_call_ref()与新实现checkasm_call_new()用checkasm_check2d()或类似函数比较两者输出用checkasm_bench_new()对新实现做基准测试可选用checkasm_report()汇报结果可选。其中第 3 步至关重要checkasm_check_func()会依据命令行传入的过滤模式--function/--test、当前 CPU 特性集合等条件决定当前这个函数变体是否值得测试。从 test.h 的宏定义可以看出它会同时把传入的函数指针记录到全局的checkasm_key_new并把查找到的参考实现键值写入checkasm_key_ref供后续的checkasm_call_ref()/checkasm_call_new()使用#define checkasm_check_func(func, ...) \ (checkasm_key_ref \ checkasm_check_key((checkasm_key_new (CheckasmKey) (func)), __VA_ARGS__))checkasm_call_ref()与checkasm_call_new()都是宏前者展开为checkasm_call(checkasm_func_ref, ...)只做信号保护后者展开为checkasm_call_checked(checkasm_func_new, ...)除信号保护外还会检查栈破坏、寄存器被 clobber、寄存器宽度不匹配、ABI 违规等常见汇编错误详见 test.h。API 命名约定现代 API 与传统短别名checkasm 同时支持两套命名风格可以在同一个测试文件中混用。现代 API推荐所有函数使用checkasm_前缀checkasm_declare(void, uint8_t *dst, const uint8_t *src, int len); checkasm_check_func(dsp-func, func_name) checkasm_call_ref(dst_c, src, len); checkasm_call_new(dst_a, src, len); checkasm_check1d(uint8_t, dst_c, dst_a, len, dst); checkasm_bench_new(dst_a, src, len); checkasm_report(func_name); checkasm_fail() checkasm_alternate(buf0, buf1) // ...传统/短别名 API为方便与向后兼容checkasm 提供了更短的别名declare_func(void, uint8_t *dst, const uint8_t *src, int len); check_func(dsp-func, func_name) call_ref(dst_c, src, len); call_new(dst_a, src, len); // checkasm_check1d() 没有短别名 bench_new(dst_a, src, len); report(func_name); fail() alternate(buf0, buf1)这些别名在 test.h 的 Short-hand Aliases 分组中集中定义例如#define call_ref checkasm_call_ref、#define declare_func checkasm_declare、#define bench_new checkasm_bench_new等。值得注意的是缓冲比较函数checkasm_check1d()/checkasm_check2d()系列没有对应的短别名只有checkasm_check、checkasm_check_padded这类以checkasm_开头的别名这是刻意的设计。两种风格完全等价且可混用。但为了文档与新增代码的一致性和可读性推荐统一使用带checkasm_前缀的现代命名。最佳实践缓冲区分配对齐是硬要求测试优化实现时缓冲区必须按平台要求对齐很多 SIMD 指令要求 16/32/64 字节对齐。CHECKASM_ALIGN()负责声明带平台相关对齐属性的变量BUF_RECT()更进一步声明一块四周带 padding、自动计算 stride 的矩形缓冲。// 简单数组 CHECKASM_ALIGN(uint8_t buf[1024]); // 2D 缓冲自动加 padding 并计算 stride // - 定义 dst 为指向 64x32 可用区域的指针以及 dst_stride字节为单位 BUF_RECT(uint8_t, dst, 64, 32);重要CHECKASM_ALIGN()必须对每个缓冲区单独使用// 正确 CHECKASM_ALIGN(uint8_t buf1[32]); CHECKASM_ALIGN(uint8_t buf2[32]); // 错误 - 只有 buf1 会被对齐 CHECKASM_ALIGN(uint8_t buf1[32], buf2[32]);从 utils.h 的实现看CHECKASM_ALIGN(x)在 MSVC 下展开为__declspec(align(CHECKASM_ALIGNMENT)) x在 GCC/Clang 下展开为x __attribute__((aligned(CHECKASM_ALIGNMENT)))——它修饰的是紧跟其后的单个声明这正是逐缓冲对齐要求的根源。BUF_RECT()宏会一次性生成一系列变量name_buf含 padding 的完整数组、name_stride以字节计的 stride、name_buf_h含 padding 的总行数以及指向可用区域起点的指针name。可用区域四周预留了足够 padding为后面的越界写检测checkasm_check_rect_padded()打基础#define BUF_RECT(type, name, w, h) \ DECL_CHECK_FUNC(*checkasm_check_impl_##name##_type, type) \ checkasm_check_impl_##type; \ CHECKASM_ALIGN(type name##_buf[((h) 32) * (CHECKASM_ROUND(w, 64) 64) 64]); \ const int name##_buf_w CHECKASM_ROUND(w, 64) 64; \ const int name##_buf_h (h) 32; \ ptrdiff_t name##_stride sizeof(type) * name##_buf_w; \ ... type *name name##_buf name##_buf_w * 16 64缓冲区初始化随机、病态与常量三种策略checkasm 提供三类缓冲区填充函数完整清单见 utils.h 的 memory 分组// 随机数据均匀分布 RANDOMIZE_BUF(buf); checkasm_randomize(buf, sizeof(buf)); // 等价于上面的宏 checkasm_randomize_mask8 (buf8, width, 0x0F); // 把取值范围约束到掩码内 checkasm_randomize_mask16(buf16, width, 1023); // 随机 10-bit 值 // 病态测试模式边界值、交替位、随机字节的混合 INITIALIZE_BUF(buf); checkasm_init(buf, sizeof(buf)); // 等价于上面的宏 checkasm_init_mask16(buf16, width, (1 12) - 1); // 约束到 12-bit 范围 // 清空为常量值 CLEAR_BUF(buf); checkasm_clear(buf, sizeof(buf)); // 等价于上面的宏 checkasm_clear16(buf16, width, 0x1234);checkasm_init()以及宏INITIALIZE_BUF()填充的是病态测试模式混合了极小值、极大值、交替位模式与随机字节专门用于触发潜在 bugcheckasm_clear()目前填充常量0xAA让测试失败时能从 hexdump 中一眼看出差异区域。对于BUF_RECT()创建的 2D 缓冲有对应的矩形版本宏它们作用于含 padding 的整块数组name_buf从而保证 padding 区域也有确定内容可供比对BUF_RECT(uint8_t, src, 64, 32); INITIALIZE_BUF_RECT(src); // 用病态测试字节初始化 RANDOMIZE_BUF_RECT(tmp); // 填充随机数据 CLEAR_BUF_RECT(dst); // 清空为常量数据当前为 0xAA多配置循环测试很多 DSP 函数按块尺寸block size分派不同实现。应当在多个配置例如各种 2 的幂块尺寸下循环测试保证覆盖全面BUF_RECT(uint8_t, src, 128, 128); BUF_RECT(uint8_t, dst_c, 128, 128); BUF_RECT(uint8_t, dst_a, 128, 128); checkasm_declare(void, uint8_t *dst, ptrdiff_t dst_stride, const uint8_t *src, ptrdiff_t src_stride, int w, int h); // 测试各种 2 的幂块尺寸从 4x4 到 128x128 for (int h 4; h 128; h 1) { for (int w 4; w 128; w 1) { if (checkasm_check_func(get_func(w), func_%dx%d, w, h)) { // 初始化测试数据 - 放在循环内部为每个测试选择不同的数据模式 INITIALIZE_BUF(src); CLEAR_BUF(dst_c); CLEAR_BUF(dst_a); // 测试该配置 checkasm_call_ref(dst_c, dst_c_stride, src, src_stride, w, h); checkasm_call_new(dst_a, dst_a_stride, src, src_stride, w, h); checkasm_check_rect_padded(dst_c, dst_c_stride, dst_a, dst_a_stride, w, h, dst); // 基准测试该配置 checkasm_bench_new(checkasm_alternate(dst_a, dst_c), dst_a_stride, src, src_stride, w, h); } } } checkasm_report(func);这里的checkasm_check_func()名称支持 printf 风格格式化如func_%dx%d会在报告中形成func_4x4、func_8x8这样清晰的命名。使用随机参数用checkasm_rand()及相关函数生成多样化的测试输入实现见 utils.h 的 rng 分组// 随机整数 int byte checkasm_rand() 0xFF; uint32_t flags checkasm_rand_uint32(); // 随机浮点 double d checkasm_randf(); // [0.0, 1.0) float f (float) checkasm_randf() * 100.0f; // [0.0, 100.0) // 正态分布 double normal checkasm_rand_norm(); // mean0, stddev1 checkasm_randomize_normf(buf, len); // 用 N(0,1) 填充缓冲区rng 分组还提供checkasm_rand_int32()、checkasm_rand_dist()、checkasm_randomize_dist/distf/range/rangef等变体它们都基于CheckasmConfig中指定的种子未指定时按当前时间取种因此可复现——这正是后面确定性测试一节的基础。组织报告用checkasm_report()把相关的函数分组汇报。推荐的模式是一个函数组一个 check_ 函数 一次 reportstatic void check_add_functions(const DSPContext *dsp) { // 测试多个相关函数 for (int bpc 8; bpc 12; bpc 2) { if (checkasm_check_func(get_add8(bpc), add8_%dbpc, bpc)) { // Test add8 } if (checkasm_check_func(get_add16(bpc), add16_%dbpc, bpc)) { // Test add16 } } // 对整个组汇报一次 checkasm_report(add); }对于只有少数几个函数、没有逻辑分组、或属于杂项函数的非常简单的测试checkasm_report()可以省略。任何其后没有checkasm_report()调用的函数将隐式地以测试本身的名字被汇报。注意这只有在这些函数是某个测试中最后被测试的函数时才有意义因为之后任何checkasm_report()调用都会把之前所有函数纳入其中该行为自 v1.0.1 起见 test.h。常见测试模式模式一2D 缓冲区处理处理带 stride 的 2D 缓冲区如滤波、运动补偿的函数是 checkasm 最典型的应用场景static void check_filter(const DSPContext *dsp) { // 定义带 padding 的 64x64 缓冲区 BUF_RECT(uint8_t, src, 64, 64); BUF_RECT(uint8_t, dst_c, 64, 64); BUF_RECT(uint8_t, dst_a, 64, 64); checkasm_declare(void, uint8_t *dst, ptrdiff_t dst_stride, const uint8_t *src, ptrdiff_t src_stride, int w, int h); for (int w 4; w 64; w 1) { if (checkasm_check_func(dsp-filter, filter_w%d, w)) { // 测试多个高度 for (int h 4; h 64; h 1) { INITIALIZE_BUF_RECT(src); CLEAR_BUF_RECT(dst_c); CLEAR_BUF_RECT(dst_a); checkasm_call_ref(dst_c, dst_c_stride, src, src_stride, w, h); checkasm_call_new(dst_a, dst_a_stride, src, src_stride, w, h); // 连 padding 一起检查 checkasm_check_rect_padded(dst_c, dst_c_stride, dst_a, dst_a_stride, w, h, dst); } // 只对全高度配置做基准 checkasm_bench_new(checkasm_alternate(dst_a, dst_c), dst_a_stride, src, src_stride, w, 64); } } checkasm_report(filter); }模式二有内部状态的函数对会修改内部状态或有副作用的函数关键在于让参考实现与被测实现从完全相同的初始状态出发static void check_decoder(const DecoderContext *dec) { DecoderState state_c, state_a; uint8_t bitstream[128]; // 用随机数据初始化比特流 RANDOMIZE_BUF(bitstream); checkasm_declare(int, DecoderState *state, const uint8_t *data, int len); if (checkasm_check_func(dec-decode, decode)) { // 把两个状态初始化为相同 init_decoder_state(state_c, bitstream, 128); init_decoder_state(state_a, bitstream, 128); // 解码并比较 int result_c checkasm_call_ref(state_c, bitstream, 128); int result_a checkasm_call_new(state_a, bitstream, 128); // 比较返回值 if (result_c ! result_a) { if (checkasm_fail()) { fprintf(stderr, return value mismatch: %d vs %d\n, result_c, result_a); } } // 比较最终状态可选 // // 该检查是否成立取决于 DecoderState 是否含 padding 字节 // 或不确定的内部状态 checkasm_check1d(uint8_t, state_c, state_a, sizeof(DecoderState), decoder state); checkasm_bench_new(state_a, bitstream, 128); } checkasm_report(decode); }注意checkasm_fail()的用法它记录失败并返回 1 仅当用户请求了详细诊断输出如--verbose时因此把fprintf包在if (checkasm_fail())分支里即可实现静默失败、按需诊断。模式三多输出函数对产生多个输出值的函数例如同时算方差与和需要逐一比较每个输出static void check_stats(const DSPContext *dsp) { CHECKASM_ALIGN(uint8_t buf[64 * 64]); checkasm_declare(int, const uint8_t *buf, int len, unsigned *variance, unsigned *sum); if (checkasm_check_func(dsp-compute_stats, compute_stats)) { unsigned var_c, var_a, sum_c, sum_a; INITIALIZE_BUF(buf); int result_c checkasm_call_ref(buf, 64*64, var_c, sum_c); int result_a checkasm_call_new(buf, 64*64, var_a, sum_a); // 比较所有输出 if (result_c ! result_a || var_c ! var_a || sum_c ! sum_a) { if (checkasm_fail()) { fprintf(stderr, result: %d vs %d, var: %u vs %u, sum: %u vs %u\n, result_c, result_a, var_c, var_a, sum_c, sum_a); } } checkasm_bench_new(buf, 64*64, var_a, sum_a); } checkasm_report(compute_stats); }模式四自定义输入生成对于需要特定测试模式的函数例如变换类函数可以自行构造最坏情况输入// 生成最坏情况输入来压测实现 static void generate_worst_case(uint16_t *buf, int len, int bitdepth_max) { // 创建倒序排列的输入值序列 for (int i 0; i len; i) buf[i] (len - 1 - i) bitdepth_max; } static void check_transform(const DSPContext *dsp) { #define WIDTH 64 CHECKASM_ALIGN(int16_t src [WIDTH]); CHECKASM_ALIGN(int16_t dst_c[WIDTH]); CHECKASM_ALIGN(int16_t dst_a[WIDTH]); checkasm_declare(void, int16_t *dst, const int16_t *src, int width); if (checkasm_check_func(dsp-transform, transform)) { // 同时测试随机输入与最坏情况输入 for (int pattern 0; pattern 2; pattern) { if (pattern 0) { INITIALIZE_BUF(src); // 随机输入 } else { generate_worst_case(src, WIDTH, 32767); // 最坏情况模式 } CLEAR_BUF(dst_c); CLEAR_BUF(dst_a); checkasm_call_ref(dst_c, src, WIDTH); checkasm_call_new(dst_a, src, WIDTH); checkasm_check1d(int16_t, dst_c, dst_a, WIDTH, dst); } checkasm_bench_new(checkasm_alternate(dst_a, dst_c), src, WIDTH); } checkasm_report(transform); }高级主题浮点结果比较ULP 与绝对容差汇编实现与 C 参考实现因计算顺序不同浮点结果不可能逐位一致必须用容差比较。checkasm 提供两套方案ULPUnits in Last Place末位单位容差与绝对 epsilon 容差完整 API 见 utils.h 的 floatcmp 分组static void check_float_func(const DSPContext *dsp) { #define WIDTH 128 CHECKASM_ALIGN(float src [WIDTH]); CHECKASM_ALIGN(float dst_c[WIDTH]); CHECKASM_ALIGN(float dst_a[WIDTH]); checkasm_declare(void, float *dst, const float *src, int len); if (checkasm_check_func(dsp-process_float, process_float)) { checkasm_randomize_normf(src, WIDTH); checkasm_call_ref(dst_c, src, WIDTH); checkasm_call_new(dst_a, src, WIDTH); // 用 ULP 容差比较 // 注意这里不能用 checkasm_check1d() const int max_ulp 1; checkasm_check2d(float_ulp, dst_c, 0, dst_a, 0, WIDTH, 1, dst, max_ulp); // 或用绝对 epsilon 容差 // if (!checkasm_float_near_abs_eps_array(dst_c, dst_a, 1e-6f, WIDTH)) { // checkasm_fail(); // } checkasm_bench_new(checkasm_alternate(dst_a, dst_c), src, WIDTH); } checkasm_report(process_float); }float_ulp是checkasm_check2d()支持的特殊类型名会路由到checkasm_check_impl_float_ulp()声明见 utils.h其最后一个可变参数即为max_ulp。浮点工具集还包含checkasm_float_near_ulp()、checkasm_float_near_abs_eps_ulp()、checkasm_double_near_abs_eps()及对应的数组版本以及无前缀的短别名float_near_ulp等。padding 与越界写检测很多汇编内核会顺手多写几个元素例如按 SIMD 宽度对齐。checkasm_check_rect_padded()系列能把 padding 区域也纳入比对从而检测函数是否写越界static void check_bounds(const DSPContext *dsp) { BUF_RECT(uint8_t, dst_c, 64, 64); BUF_RECT(uint8_t, dst_a, 64, 64); checkasm_declare(void, uint8_t *dst, ptrdiff_t stride, int w, int h); if (checkasm_check_func(dsp-fill, fill)) { const int w 64, h 64; CLEAR_BUF_RECT(dst_c); CLEAR_BUF_RECT(dst_a); checkasm_call_ref(dst_c, dst_c_stride, w, h); checkasm_call_new(dst_a, dst_a_stride, w, h); // 标准检查不含 padding checkasm_check2d(uint8_t, dst_c, dst_c_stride, dst_a, dst_a_stride, w, h, dst); // 带 padding 检测检测 w×h 之外的写入 checkasm_check_rect_padded(dst_c, dst_c_stride, dst_a, dst_a_stride, w, h, dst); // 允许右边缘最多越写 16 个元素对齐导致的越写 checkasm_check_rect_padded_align(dst_c, dst_c_stride, dst_a, dst_a_stride, w, h, dst, 16, 1); checkasm_bench_new(checkasm_alternate(dst_a, dst_c), dst_a_stride, w, h); } checkasm_report(fill); }从 utils.h 的宏定义看checkasm_check_rect_padded()展开为checkasm_check2d_padded(type, ..., align_w1, align_h1, padding8)——即默认允许 1 个元素的对齐越写、检查 8 个元素的 padding 带checkasm_check_rect_padded_align()则允许你自定义align_w/align_h文档中的16, 1表示只允许右边缘最多越写 16 个元素。更底层的checkasm_check2d_padded()接受align_w、align_h、padding三个额外参数align_h0可关闭上下边缘的越写检查。与之配套的一维版本是checkasm_check1d_padded()但它因实现原因不接受变参如需float_ulp检查请改用align_h0的checkasm_check2d_padded()。多配置基准测试与几何平均checkasm_bench_new()可以在同一个checkasm_check_func()块内多次调用框架会把所有计时结果累积并以几何平均汇报这非常适合多个尺寸合并成一个综合数字的场景if (checkasm_check_func(dsp-filter, filter_w%d, w)) { for (int h 4; h 64; h 1) { // 对所有高度做正确性测试 checkasm_call_ref(dst_c, dst_c_stride, src, src_stride, w, h); checkasm_call_new(dst_a, dst_a_stride, src, src_stride, w, h); checkasm_check2d(uint8_t, dst_c, dst_c_stride, dst_a, dst_a_stride, w, h, dst); // 对每个配置做基准 checkasm_bench_new(checkasm_alternate(dst_a, dst_c), dst_a_stride, src, src_stride, w, h); } // 框架对同一 checkasm_check_func() 的所有基准运行汇报几何平均 } checkasm_report(filter_w%d, w);除此之外还有两种策略对每个配置单独调用checkasm_check_func()以获得每个尺寸独立的基准报告或者只对最大的输入尺寸调用checkasm_bench_new()以考察极限行为具体取舍讨论见 benchmarking.md 的 Choosing Configurations 一节。多 Bitdepth 测试对支持多种位深bitdepth的编解码函数一种做法是在循环内按位深重建 DSP 上下文并约束随机数据范围static void check_pixfunc(void) { DSPContext dsp; #define WIDTH 64 CHECKASM_ALIGN(uint16_t src [WIDTH]); CHECKASM_ALIGN(uint16_t dst_c[WIDTH]); CHECKASM_ALIGN(uint16_t dst_a[WIDTH]); checkasm_declare(void, uint16_t *dst, const uint16_t *src, int len); for (int bpc 10; bpc 12; bpc 2) { const int bitdepth_max (1 bpc) - 1; dsp_context_init(dsp, checkasm_get_cpu_flags(), bpc); if (checkasm_check_func(dsp-process, process_%dbpc, bpc)) { // 在合法位深范围内随机化 checkasm_randomize_mask16(src, WIDTH, bitdepth_max); checkasm_call_ref(dst_c, src, WIDTH); checkasm_call_new(dst_a, src, WIDTH); checkasm_check1d(uint16_t, dst_c, dst_a, WIDTH, dst); checkasm_bench_new(checkasm_alternate(dst_a, dst_c), src, WIDTH); } } checkasm_report(process); }这里checkasm_randomize_mask16(src, WIDTH, bitdepth_max)保证输入始终落在合法位深范围如 10-bit 时不超过 1023避免无意义的比较。另一种思路是把测试文件本身编译多次用预处理器宏如-DBITDEPTH10区分位深。自定义失败报告当返回值或内部状态需要更细粒度的诊断时可以在checkasm_fail()分支内打印自定义信息static void check_complex(const DSPContext *dsp) { // ... if (checkasm_check_func(dsp-complex, complex)) { for (int param 0; param 16; param) { int result_c checkasm_call_ref(param); int result_a checkasm_call_new(param); // 检查返回值 if (result_c ! result_a) { if (checkasm_fail()) { // 仅当用户请求详细错误诊断时才执行此分支 fprintf(stderr, return mismatch for param%d: %d vs %d\n, param, result_c, result_a); } } } } checkasm_report(complex); }checkasm_fail()的返回值语义在 test.h 中有明确文档1 表示失败详情应当被详细打印0 表示静默。它展开为checkasm_fail_func(%s:%d, __FILE__, __LINE__)自动附带失败位置。通过 wrapper 间接调用函数当被测函数必须通过一个 wrapper 间接调用时使用checkasm_call()和checkasm_call_checked()调用任意辅助函数。此时checkasm_declare()声明的函数类型必须是 wrapper 的类型而不是传给checkasm_check_func()的内部函数类型内部函数的指针可以通过无类型的checkasm_key_ref/checkasm_key_new取得typedef int (my_func)(int); // 调用实际函数的 wrapper static int sum_upto_n(my_func *func, int count) { int sum 0; for (int i 0; i count; i) sum func(i); return sum; } static void check_wrapper(void) { // 声明 wrapper 的签名而非内部函数的签名 checkasm_declare(int, my_func *, int); if (checkasm_check_func(get_my_func(), my_wrapped_func)) { const int count checkasm_rand() % 100; // 把 checkasm_key_new 转型为合适的类型并传给 wrapper const my_func *my_ref (my_func *) checkasm_key_ref; const my_func *my_new (my_func *) checkasm_key_new; int sum_c checkasm_call(sum_upto_n, my_ref, count); int sum_a checkasm_call_checked(sum_upto_n, my_new, count); if (sum_c ! sum_a) checkasm_fail(); } }注意在这种模式下传给checkasm_check_func()的值甚至不必是函数指针——它可以是任意指针或指针大小的整数如配置结构体、分发表索引只要它能唯一标识被测的底层实现即可。checkasm_call()只提供信号处理用于调用参考实现等名义上安全的代码checkasm_call_checked()才做完整校验栈保护、寄存器 clobber 检测等见 test.h因此被测的优化实现应当用checkasm_call_checked()。MMX 函数的特殊处理x86x86 上的 MMX 函数常常在返回前省略emms指令期望调用方在循环结束后手动执行。emms用于在任何浮点代码执行前清空 MMX 状态但它非常慢所以优化过的循环通常在循环末尾才执行一次以最小化开销。对这种不合 ABI 的函数使用checkasm_declare_emms()static void check_sad_mmx(const DSPContext *dsp) { #define SIZE 16 CHECKASM_ALIGN(uint8_t src [SIZE * SIZE]); CHECKASM_ALIGN(uint8_t ref [SIZE * SIZE]); // 声明 CPU_FLAG_MMX 后每当该标志激活时会在调用本函数后自动执行 emms checkasm_declare_emms(CPU_FLAG_MMX, int, const uint8_t *src, const uint8_t *ref, ptrdiff_t stride); if (checkasm_check_func(dsp-sad_16x16, sad_16x16)) { INITIALIZE_BUF(src); INITIALIZE_BUF(ref); // checkasm 会在 checkasm_call_new() 之后自动调用 emms int result_c checkasm_call_ref(src, ref, SIZE); int result_a checkasm_call_new(src, ref, SIZE); if (result_c ! result_a) { if (checkasm_fail()) { fprintf(stderr, sad mismatch: %d vs %d\n, result_c, result_a); } } // 基准迭代之后 checkasm 同样会调用 emms checkasm_bench_new(src, ref, SIZE); } checkasm_report(sad); }第一个参数是 CPU 标志掩码如CPU_FLAG_MMX | CPU_FLAG_MMXEXT当掩码中的任一 CPU 标志激活时checkasm 会在每次checkasm_call_new()和基准运行之后调用emms。在非 x86 平台上checkasm_declare_emms()等价于checkasm_declare()见 test.h 的条件宏定义。现代 SIMD 指令集SSE 及以后不使用 MMX 寄存器因此不需要emms。只对纯 MMX 代码或显式使用 MMX 寄存器的 MMXEXT 函数使用checkasm_declare_emms()。提示与技巧确定性测试固定种子checkasm 使用带种子的 PRNG 保证测试可复现。传入一个位置参数即可使用指定种子./checkasm 12345 # 使用种子 12345失败时会打印实际使用的种子便于复现checkasm: using random seed 987654321 ... sad_16x16: FAILED (ref:1234 new:1235)从 checkasm.h 的CheckasmConfig看种子相关的字段是seed与seed_setseed非零或seed_set非零时使用给定种子否则按当前时间取种repeat字段还可以让测试用连续多个种子重复运行设为 -1 时几乎穷举所有种子用于穷尽式测试。选择性测试只测试匹配特定模式的函数或测试模块# 只测试匹配模式的函数 ./checkasm --functionadd_* # 只测试特定的测试模块 ./checkasm --testmath # 两者组合 ./checkasm --testdsp --functionblend_*--test/--function对应CheckasmConfig中的test_pattern/function_pattern字段shell 风格通配符NULL 表示运行全部。还可使用--list-tests、--list-functions、--list-cpu-flags查看可用的测试、函数与 CPU 特性清单getting-started.md 中的--help输出列出了全部选项。详细输出启用 verbose 模式以获取详细的失败信息./checkasm --verbose当使用内置的checkasm_check*()系列缓冲比较助手时verbose 模式会自动对被差异区域输出 hexdump该行为在 utils.h 的checkasm_check2d()文档中有明确说明。基准测试用适当的时长运行基准# 快速基准默认时长 ./checkasm --bench # 更长时间的基准以获得更精确结果每个函数 10ms ./checkasm --bench --duration10000 # 以不同格式导出结果 ./checkasm --bench --csv results.csv ./checkasm --bench --json results.json ./checkasm --bench --html results.html--duration以微秒为单位控制每个函数的基准时长--csv/--json/--html对应CheckasmConfig.format的CHECKASM_FORMAT_*枚举值。checkasm 的统计方法对数正态分布建模、log 空间线性回归、几何平均、no-op 开销校正与结果解读cycles、时间、相对加速比(vs ref)列详见 benchmarking.md。辅助宏用辅助宏减少重复代码#define TEST_FILTER(name, w, h) \ if (checkasm_check_func(dsp-name, #name _%dx%d, w, h)) { \ test_filter_##name(dsp, w, h); \ checkasm_bench_new(dst, dst_stride, src, src_stride, w, h); \ } // 用法 TEST_FILTER(blur, 16, 16); TEST_FILTER(blur, 32, 32); TEST_FILTER(sharpen, 16, 16);与框架配置衔接从测试到完整程序writing-tests.md聚焦于测试函数内部的写法而一个可运行的 checkasm 程序还需要在main()中注册测试与 CPU 特性。这部分的完整示例见 getting-started.md 的 Quick Start核心模式是填充CheckasmConfig定义于 checkasm.h后交给checkasm_main()int main(int argc, const char *argv[]) { CheckasmConfig config { .tests tests, // CheckasmTest 数组{0} 结尾 .cpu_flags cpu_flags, // CheckasmCpuInfo 数组{0} 结尾 .cpu detect_cpu_flags(), }; return checkasm_main(config, argc, argv); }checkasm 会按cpu_flags数组中声明的顺序增量式遍历 CPU 特性集合每个后续特性继承之前已激活的标志set_cpu_flags回调负责更新你的全局函数分发表测试函数内可用checkasm_get_cpu_flags()查询当前激活标志以选择对应实现。框架内部还提供了一系列校验能力信号处理checkasm_set_signal_handler_state()、栈破坏检测checkasm_push_stack_guard()/checkasm_pop_stack_guard()、x86 上 MMX 状态清理checkasm_clear_cpu_state()这些在 test.h 的 internal 分组中均有定义。checkasm 自身的自测用例如 tests/x86/tests.c就通过故意 clobber 寄存器DEF_NOOP_FUNC(clobber_r0)等与故意写坏栈corrupt_stack_x86来验证这些检测机制确实生效。下一步掌握测试编写之后可以继续阅读 benchmarking.md学习如何利用 checkasm 的基准测试能力精确度量并比较优化实现的性能——包括对数正态分布统计建模、log 空间线性回归、几何平均汇总、no-op 开销校正以及关闭 CPU 频率缩放、禁用 turbo boost 等系统级最佳实践。赞分享音视频【免费下载链接】vlcVLC media player - plays everything, runs anywhere. Code here: https://code.videolan.org/videolan/vlc项目地址https://gitcode.com/gh_mirrors/vl/vlc点击查看免费下载相关推荐如何用 XPipe 统一管理所有服务器一份完整上手指南如何用 XPipe 统一管理所有服务器一份完整上手指南 XPipe 是一款开源的本地连接中枢加远程文件管理器让你在桌面上用统一界面管理 SSH、Docker音视频3步掌握WanVideo_comfyComfyUI视频生成模型终极整合指南3步掌握WanVideo_comfyComfyUI视频生成模型终极整合指南 WanVideo_comfy是一个专为ComfyUI设计的视频生成模型整合工具它音视频Snapshot report for test.jsSnapshot report for test.js The actual snapshot is saved in test.js.snap . Gener测试上一篇【亲测免费】 jOOQ - 更优雅地处理 Java SQL 编程下一篇【亲测免费】 Crawler4J - Web爬虫框架创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表