This guide covers best practices and common patterns for writing checkasm tests.
Basic Test Structure
Before diving into advanced patterns and best practices, familiarize yourself with the basic test structure by reading the Quick Start Example.
The typical test workflow is:
- Allocate (aligned) buffers for test data
- Declare the function signature with checkasm_declare()
- Check if the function should be tested with checkasm_check_func()
- Initialize test inputs and clear output buffers
- Call both reference (checkasm_call_ref()) and new (checkasm_call_new()) implementations
- Compare results with checkasm_check()
- Benchmark the new implementation with checkasm_bench_new()
- Report results with checkasm_report() (optional)
API Naming Conventions
checkasm supports two API naming styles:
Modern API (Recommended)
The modern API uses the checkasm_ prefix for all functions:
#define checkasm_check(type,...)
Compare two 2D buffers and fail test if different.
Definition utils.h:480
#define checkasm_bench_new(...)
Benchmark the optimized implementation.
Definition test.h:407
#define checkasm_call_ref(...)
Call the reference implementation.
Definition test.h:327
CHECKASM_API void checkasm_report(const char *name,...) CHECKASM_PRINTF(1
Report test outcome for a named group of functions.
#define checkasm_alternate(a, b)
Alternate between two values during benchmarking.
Definition test.h:429
#define checkasm_fail()
Mark the current test as failed.
Definition test.h:116
#define checkasm_call_new(...)
Call the implementation being tested with validation.
Definition test.h:342
#define checkasm_declare(ret,...)
Declare a function signature for testing.
Definition test.h:166
#define checkasm_check_func(func,...)
Check if a function should be tested and set up function references.
Definition test.h:76
Legacy/Short API
For convenience and backwards compatibility, shorter aliases are available:
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);
bench_new(dst_a, src, len);
report("func_name");
fail()
alternate(buf0, buf1)
- Note
- Both naming styles are fully supported and can be mixed within the same test file. However, for consistency and readability in documentation and new code, we recommend using the modern checkasm_ prefixed names.
Best Practices
The remainder of this guide focuses on best practices, common patterns, and advanced testing scenarios.
Buffer Allocation
Always use properly aligned buffers for testing:
#define CHECKASM_ALIGN(x)
Declare a variable with platform-specific alignment requirements.
Definition utils.h:408
#define BUF_RECT(type, name, w, h)
Declare an aligned, padded rectangular buffer.
Definition utils.h:553
Important: Apply CHECKASM_ALIGN() to each buffer individually:
Buffer Initialization
checkasm provides several buffer initialization functions:
CHECKASM_API void checkasm_randomize(void *buf, size_t bytes)
Fill a buffer with uniformly chosen random bytes.
CHECKASM_API void checkasm_clear16(uint16_t *buf, int width, uint16_t val)
Fill a uint16_t buffer with a constant value.
CHECKASM_API void checkasm_clear(void *buf, size_t bytes)
Clear a buffer to a pre-determined pattern (currently 0xAA).
CHECKASM_API void checkasm_randomize_mask16(uint16_t *buf, int width, uint16_t mask)
Fill a uint16_t buffer with random values chosen uniformly within a mask.
CHECKASM_API void checkasm_randomize_mask8(uint8_t *buf, int width, uint8_t mask)
Fill a uint8_t buffer with random values chosen uniformly within a mask.
#define RANDOMIZE_BUF(buf)
Fill a fixed size buffer wth random data (convenience macro).
Definition utils.h:261
#define CLEAR_BUF(buf)
Clear a fixed size buffer (convenience macro).
Definition utils.h:254
#define INITIALIZE_BUF(buf)
Fill a fixed size buffer with pathological test data (convenience macro).
Definition utils.h:269
CHECKASM_API void checkasm_init(void *buf, size_t bytes)
Initialize a buffer with pathological test patterns.
CHECKASM_API void checkasm_init_mask16(uint16_t *buf, int width, uint16_t mask)
Initialize a uint16_t buffer with pathological values within a mask.
For 2D buffers created with BUF_RECT():
#define RANDOMIZE_BUF_RECT(name)
Randomize a rectangular buffer (including padding).
Definition utils.h:586
#define CLEAR_BUF_RECT(name)
Clear a rectangular buffer (including padding).
Definition utils.h:570
#define INITIALIZE_BUF_RECT(name)
Initialize a rectangular buffer (including padding) with pathological values.
Definition utils.h:578
See Memory Initialization for a full list of buffer initialization functions.
Testing Multiple Configurations
Test functions across multiple configurations (e.g. block sizes) to ensure comprehensive coverage:
const uint8_t *src, ptrdiff_t src_stride,
int w, int h);
for (int h = 4; h <= 128; h <<= 1) {
for (int w = 4; w <= 128; w <<= 1) {
dst_a, dst_a_stride, w, h, "dst");
src, src_stride, w, h);
}
}
}
#define checkasm_check_rect_padded(rect1,...)
Compare two rectangular buffers including padding.
Definition utils.h:604
Using Random Parameters
Use checkasm_rand() and related functions to generate diverse test inputs:
CHECKASM_API void checkasm_randomize_normf(float *buf, int width)
Fill a float buffer with values from a standard normal distribution.
CHECKASM_API int checkasm_rand(void)
Generate a random non-negative integer.
CHECKASM_API double checkasm_randf(void)
Generate a random double-precision floating-point number.
CHECKASM_API uint32_t checkasm_rand_uint32(void)
Generate a random 32-bit unsigned integer.
CHECKASM_API double checkasm_rand_norm(void)
Generate a random number from the standard normal distribution.
See Random Number Generation for a full list of random number generation functions.
Organizing Reports
Group related functions and use checkasm_report() strategically:
static void check_add_functions(const DSPContext *dsp)
{
for (int bpc = 8; bpc <= 12; bpc += 2) {
}
}
}
}
For very simple tests with only a few functions, for which there is no logical grouping, or for miscellaneous functions, this can be left out. Any functions without a checkasm_report() call after them will be implicitly reported under the name of the test itself. Note that this only makes sense if such functions are the last functions being tested in a given test, since any later checkasm_report() call would otherwise include all prior functions.
Common Test Patterns
2D Buffer Processing
For functions operating on 2D buffers with stride:
static void check_filter(const DSPContext *dsp)
{
const uint8_t *src, ptrdiff_t src_stride,
int w, int h);
for (int w = 4; w <= 64; w <<= 1) {
for (int h = 4; h <= 64; h <<= 1) {
dst_a, dst_a_stride,
w, h, "dst");
}
src, src_stride, w, 64);
}
}
}
State-Based Functions
For functions that modify internal state or have side effects:
static void check_decoder(const DecoderContext *dec)
{
DecoderState state_c, state_a;
uint8_t bitstream[128];
init_decoder_state(&state_c, bitstream, 128);
init_decoder_state(&state_a, bitstream, 128);
if (result_c != result_a) {
fprintf(stderr, "return value mismatch: %d vs %d\n",
result_c, result_a);
}
}
sizeof(DecoderState), 1, "decoder state");
}
}
Multiple Outputs
For functions that produce multiple output values:
static void check_stats(const DSPContext *dsp)
{
unsigned *variance, unsigned *sum);
unsigned var_c, var_a, sum_c, sum_a;
if (result_c != result_a || var_c != var_a || sum_c != sum_a) {
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);
}
}
}
}
Custom Input Generation
For functions requiring specific test patterns:
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
for (int pattern = 0; pattern < 2; pattern++) {
if (pattern == 0) {
} else {
generate_worst_case(src, WIDTH, 32767);
}
}
}
}
Advanced Topics
Floating-Point Comparison
For functions producing floating-point results, use tolerance-based comparison:
static void check_float_func(const DSPContext *dsp)
{
#define WIDTH 128
const int max_ulp = 1;
checkasm_check(float_ulp, dst_c, 0, dst_a, 0, WIDTH, 1,
"dst", max_ulp);
}
}
Padding and Over-Write Detection
Detect when functions write beyond their intended boundaries:
static void check_bounds(const DSPContext *dsp)
{
const int w = 64, h = 64;
dst_a, dst_a_stride, w, h, "dst");
dst_a, dst_a_stride, w, h, "dst");
dst_a, dst_a_stride,
w, h, "dst", 16, 1);
}
}
#define checkasm_check_rect_padded_align(rect1,...)
Compare two rectangular buffers, with custom alignment (over-write).
Definition utils.h:620
Benchmarking Multiple Configurations
For functions that can be benchmarked at multiple configurations:
for (int h = 4; h <= 64; h <<= 1) {
dst_a, dst_a_stride, w, h, "dst");
src, src_stride, w, h);
}
}
Alternatively, you could call checkasm_check_func() on each configuration to get a separate benchmark report for each size, or call checkasm_bench_new() only on the largest input size to test the limiting behavior.
Multi-Bitdepth Testing
For codecs supporting multiple bit depths:
static void check_pixfunc(void)
{
DSPContext dsp;
#define WIDTH 64
for (int bpc = 10; bpc <= 12; bpc += 2) {
const int bitdepth_max = (1 << bpc) - 1;
}
}
}
CHECKASM_API CheckasmCpu checkasm_get_cpu_flags(void)
Get the current active set of CPU flags.
Alternatively, you may prefer to compile the test file itself multiple times, using preprocessor definitions like -DBITDEPTH=10 etc.
Custom Failure Reporting
Provide detailed diagnostics when tests fail:
static void check_complex(const DSPContext *dsp)
{
for (int param = 0; param < 16; param++) {
if (result_c != result_a) {
fprintf(stderr, "return mismatch for param=%d: %d vs %d\n",
param, result_c, result_a);
}
}
}
}
}
Calling Functions Through Wrappers
When the function being tested must be called indirectly through a wrapper, you may use checkasm_call() and checkasm_call_checked() to invoke an arbitrary helper function.
In this case, the declared function type must be the type of the wrapper, not the inner function passed to checkasm_check_func(). You may then access the untyped reference/tested function pointers via checkasm_key_ref and checkasm_key_new:
typedef int (my_func)(int);
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)
{
if (sum_c != sum_a)
}
}
static CheckasmKey checkasm_key_ref
Key identifying the reference implementation.
Definition test.h:278
#define checkasm_call(func,...)
Call a function with signal handling.
Definition test.h:213
static CheckasmKey checkasm_key_new
Key identifying the implementation being tested.
Definition test.h:288
#define checkasm_call_checked(func,...)
Call an assembly function with full validation.
Definition test.h:233
- Note
- When using a pattern like this, the value passed to checkasm_check_func() may not even need to be a function pointer. It could be an arbitrary pointer or pointer-sized integer, such as a configuration struct or index into a dispatch table, so long as it uniquely identifies the underlying implementation being tested.
MMX Functions (x86)
MMX functions on x86 often omit the emms instruction before returning, expecting the caller to execute it manually after a loop. The emms instruction is necessary to clear MMX state before any floating-point code can execute, but it can be very slow, so optimized loops that call into MMX kernels usually defer it to the loop end to minimize overhead.
Use checkasm_declare_emms() for such (non-ABI-compliant) functions:
static void check_sad_mmx(const DSPContext *dsp)
{
#define SIZE 16
const uint8_t *ref, ptrdiff_t stride);
if (result_c != result_a) {
fprintf(stderr, "sad mismatch: %d vs %d\n", result_c, result_a);
}
}
}
}
#define checkasm_declare_emms(cpu_flags, ret,...)
Declare signature for non-ABI compliant MMX functions (x86 only).
Definition test.h:195
The first parameter is a CPU flag mask (e.g., CPU_FLAG_MMX | CPU_FLAG_MMXEXT). When any of the specified CPU flags are active, checkasm will call emms after each checkasm_call_new() and benchmark run. On non-x86 platforms, checkasm_declare_emms() is equivalent to checkasm_declare().
- Note
- Modern SIMD instruction sets (SSE and later) do not use MMX registers and therefore don't require emms. Only use checkasm_declare_emms() for legacy MMX-only code or MMXEXT functions that explicitly use MMX registers.
Tips and Tricks
Deterministic Testing
checkasm uses a seeded PRNG for reproducible tests. To test with a specific seed:
./checkasm 12345 # Use seed 12345
Failed tests will print the seed used, allowing you to reproduce failures:
checkasm: using random seed 987654321
...
sad_16x16: FAILED (ref:1234 new:1235)
Selective Testing
Test specific functions or groups:
# Test only functions matching pattern
./checkasm --function='add_*'
# Test only a specific test module
./checkasm --test=math
# Combine both
./checkasm --test=dsp --function='blend_*'
Verbose Output
Enable verbose mode for detailed failure information:
This shows hexdumps of differing buffer regions automatically, when using the built-in checkasm_check() series of buffer comparison helpers.
Benchmarking Tips
Run benchmarks with appropriate duration:
# Quick benchmark (default)
./checkasm --bench
# Longer benchmark for more accurate results (10ms per function)
./checkasm --bench --duration=10000
# Export results in different formats
./checkasm --bench --csv > results.csv
./checkasm --bench --json > results.json
./checkasm --bench --html > results.html
Helper Macros
Create helper macros to reduce repetition in your tests:
#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);
Next Steps
Now that you've mastered writing tests, learn how to accurately measure and compare the performance of your optimized implementations using checkasm's benchmarking capabilities.
Next: Benchmarking