/* * test_kernels.c - correctness checks for the fossbench assembly kernels * * The benchmark's own best-of-N run guards against non-determinism, but a * kernel can be perfectly deterministic and still wrong. This file is the * "single C file to poke at and test with": it validates each kernel against * an independent reference or an invariant, so a mistake in the assembly is * caught here rather than silently skewing a score. * * Every check (except the single-threaded pointer-chase) is run concurrently * on all available cores. The kernels take their buffers as arguments and hold * no shared state, so a correct kernel must give identical, correct results no * matter how many copies run at once; a hidden global or a reentrancy bug would * survive a single-threaded run but fail here. * * Build: cc -O2 -pthread test_kernels.c fossbench.S -o test_kernels -lm * Exit status is 0 iff every check passes. */ #include #include #include #include #include #include #include #include extern uint64_t fb_int_math(uint64_t iters); extern uint64_t fb_fp_math(uint64_t iters); extern uint64_t fb_primes(uint64_t limit, uint8_t *sieve); extern uint64_t fb_simd(uint64_t iters, void *buf); extern uint64_t fb_compress(const uint8_t *src, uint64_t len, uint32_t *ht); extern uint64_t fb_chacha20(uint8_t *buf, uint64_t len, const uint8_t key[32], uint64_t rounds); extern uint64_t fb_physics(double *bodies, uint64_t n, uint64_t steps); extern uint64_t fb_sort(uint32_t *a, uint64_t n); extern uint64_t fb_chase(void **ptrs, uint64_t steps); static int failures = 0; static int checks = 0; /* * Concurrency plumbing. Each check runs on every core at once; the counters and * stdout are shared, so ok()/note() serialise on this lock. `fb_primary` is set * on exactly one thread per check (the one running on the main thread): it owns * the human-readable output so the "[ ok ]" lines and diagnostics appear once, * not once per core. Every thread still evaluates every assertion, so a failure * on any core - even a silent secondary - is reported and counted. */ static pthread_mutex_t io_lock = PTHREAD_MUTEX_INITIALIZER; static __thread int fb_primary = 1; static long fb_ncores = 1; static void ok(const char *what, int cond) { pthread_mutex_lock(&io_lock); if (fb_primary) { checks++; if (cond) { printf(" [ ok ] %s\n", what); } else { printf(" [FAIL] %s\n", what); failures++; } } else if (!cond) { /* a secondary core disagrees: surface it explicitly */ printf(" [FAIL] %s (concurrent core)\n", what); failures++; } pthread_mutex_unlock(&io_lock); } /* Diagnostic output that should appear once per check, not once per core. */ static void note(const char *fmt, ...) { va_list ap; if (!fb_primary) return; pthread_mutex_lock(&io_lock); va_start(ap, fmt); vprintf(fmt, ap); va_end(ap); pthread_mutex_unlock(&io_lock); } /* Run `check` on every core simultaneously. The main thread is the primary; * fb_ncores-1 workers run the same check as silent secondaries. */ static void *fb_worker(void *arg) { void (*check)(void) = *(void (**)(void))arg; fb_primary = 0; check(); return NULL; } static void parallel(void (*check)(void)) { long extra = fb_ncores - 1; pthread_t *th = NULL; long i, spawned = 0; if (extra > 0) { th = calloc((size_t)extra, sizeof *th); if (th) { for (i = 0; i < extra; i++) if (pthread_create(&th[spawned], NULL, fb_worker, &check) == 0) spawned++; } } check(); /* primary runs on this thread */ for (i = 0; i < spawned; i++) pthread_join(th[i], NULL); free(th); } /* ---------- reference implementations ---------- */ static uint64_t ref_prime_count(uint64_t limit) { uint8_t *s = calloc(limit, 1); uint64_t count = 0, i, j; for (i = 2; i * i < limit; i++) if (!s[i]) for (j = i * i; j < limit; j += i) s[j] = 1; for (i = 2; i < limit; i++) if (!s[i]) count++; free(s); return count; } /* A textbook scalar ChaCha20 block function, used both to anchor against the * RFC 8439 known-answer vector and to validate the NEON kernel block-for-block. * `out` receives 64 keystream bytes for the given counter and 12-byte nonce. */ #define ROTL32(x, n) (((x) << (n)) | ((x) >> (32 - (n)))) static void ref_chacha_block(uint32_t out_words[16], const uint8_t key[32], uint32_t counter, const uint8_t nonce[12]) { static const uint32_t c[4] = { 0x61707865, 0x3320646e, 0x79622d32, 0x6b206574 }; uint32_t s[16], x[16]; int i; for (i = 0; i < 4; i++) s[i] = c[i]; for (i = 0; i < 8; i++) s[4 + i] = (uint32_t)key[4 * i] | (uint32_t)key[4 * i + 1] << 8 | (uint32_t)key[4 * i + 2] << 16 | (uint32_t)key[4 * i + 3] << 24; s[12] = counter; for (i = 0; i < 3; i++) s[13 + i] = (uint32_t)nonce[4 * i] | (uint32_t)nonce[4 * i + 1] << 8 | (uint32_t)nonce[4 * i + 2] << 16 | (uint32_t)nonce[4 * i + 3] << 24; memcpy(x, s, sizeof x); #define QR(a, b, cc, d) \ x[a] += x[b]; x[d] ^= x[a]; x[d] = ROTL32(x[d], 16); \ x[cc] += x[d]; x[b] ^= x[cc]; x[b] = ROTL32(x[b], 12); \ x[a] += x[b]; x[d] ^= x[a]; x[d] = ROTL32(x[d], 8); \ x[cc] += x[d]; x[b] ^= x[cc]; x[b] = ROTL32(x[b], 7) for (i = 0; i < 10; i++) { QR(0, 4, 8, 12); QR(1, 5, 9, 13); QR(2, 6, 10, 14); QR(3, 7, 11, 15); QR(0, 5, 10, 15); QR(1, 6, 11, 12); QR(2, 7, 8, 13); QR(3, 4, 9, 14); } #undef QR for (i = 0; i < 16; i++) out_words[i] = x[i] + s[i]; } /* ---------- checks ---------- */ static void check_int(void) { /* determinism and non-triviality: the checksum must be stable and * must actually change with the iteration count */ uint64_t a = fb_int_math(1000); uint64_t b = fb_int_math(1000); uint64_t c = fb_int_math(2000); ok("int_math is deterministic", a == b); ok("int_math depends on iters", a != c); ok("int_math(0) is zero", fb_int_math(0) == 0); } static void check_fp(void) { uint64_t a = fb_fp_math(1000); uint64_t b = fb_fp_math(1000); double da; memcpy(&da, &a, sizeof da); ok("fp_math is deterministic", a == b); ok("fp_math result is finite", isfinite(da)); ok("fp_math(0) is zero", fb_fp_math(0) == 0); } static void check_primes(void) { enum { LIM = 1000000 }; uint8_t *sieve = malloc(LIM); uint64_t got = fb_primes(LIM, sieve); uint64_t ref = ref_prime_count(LIM); note(" primes < %d: got %llu, expected %llu\n", LIM, (unsigned long long)got, (unsigned long long)ref); ok("primes matches reference sieve", got == ref); ok("primes < 10 == 4", fb_primes(10, sieve) == 4); /* 2,3,5,7 */ ok("primes < 2 == 0", fb_primes(2, sieve) == 0); free(sieve); } static void check_simd(void) { uint8_t *buf = aligned_alloc(16, 256); uint64_t a, b; memset(buf, 0xA5, 256); a = fb_simd(500, buf); memset(buf, 0xA5, 256); b = fb_simd(500, buf); ok("simd is deterministic", a == b); ok("simd(0) is zero", fb_simd(0, buf) == 0); free(buf); } static void check_compress(void) { enum { N = 65536 }; uint8_t *src = malloc(N); uint32_t *ht = malloc((1 << 16) * sizeof(uint32_t)); uint64_t incompressible, compressible; size_t i; /* genuinely incompressible data (splitmix64 output): with no matches * to exploit, an LZ coder's output must be at least the input size */ { uint64_t st = 0x1234567890abcdefULL; for (i = 0; i < N; i++) { uint64_t z = (st += 0x9e3779b97f4a7c15ULL); z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9ULL; z = (z ^ (z >> 27)) * 0x94d049bb133111ebULL; src[i] = (uint8_t)(z ^ (z >> 31)); } } incompressible = fb_compress(src, N, ht); /* all-zero data is maximally compressible: it must shrink hugely */ memset(src, 0, N); compressible = fb_compress(src, N, ht); note(" 64KiB random -> %llu bytes, 64KiB zeros -> %llu bytes\n", (unsigned long long)incompressible, (unsigned long long)compressible); ok("compress expands random data", incompressible >= N); ok("compress shrinks constant data", compressible < N / 10); ok("compress is deterministic", fb_compress(src, N, ht) == compressible); free(src); free(ht); } static void check_crypto(void) { uint8_t key[32]; size_t i; /* (1) anchor the scalar reference to the RFC 8439 s.2.3.2 vector: * key = 00,01,...,1f; counter = 1; nonce = 00,00,00,09,...,4a,... * serialised keystream begins 10 f1 e7 e4. */ { uint32_t w[16]; uint8_t rnonce[12] = {0,0,0,9, 0,0,0,0x4a, 0,0,0,0}; uint8_t ks0[4]; for (i = 0; i < 32; i++) key[i] = (uint8_t)i; ref_chacha_block(w, key, 1, rnonce); for (i = 0; i < 4; i++) ks0[i] = (uint8_t)(w[0] >> (8 * i)); note(" ref keystream[0..3] = %02x %02x %02x %02x " "(RFC 8439 expects 10 f1 e7 e4)\n", ks0[0], ks0[1], ks0[2], ks0[3]); ok("scalar ChaCha20 matches RFC 8439 vector", ks0[0] == 0x10 && ks0[1] == 0xf1 && ks0[2] == 0xe7 && ks0[3] == 0xe4); } /* (2) validate the NEON kernel against that reference. The kernel * hardwires nonce = 0 and starts the block counter at 0, so we * compare its keystream to the reference block-for-block. */ { uint8_t buf[128]; uint8_t zero_nonce[12] = {0}; uint32_t ref0[16], ref1[16]; int match = 1; for (i = 0; i < 32; i++) key[i] = (uint8_t)(i * 5 + 1); memset(buf, 0, sizeof buf); /* zeros -> raw keystream */ fb_chacha20(buf, sizeof buf, key, 1); ref_chacha_block(ref0, key, 0, zero_nonce); ref_chacha_block(ref1, key, 1, zero_nonce); for (i = 0; i < 16; i++) { uint32_t k0 = (uint32_t)buf[4 * i] | (uint32_t)buf[4 * i + 1] << 8 | (uint32_t)buf[4 * i + 2] << 16 | (uint32_t)buf[4 * i + 3] << 24; uint32_t k1 = (uint32_t)buf[64 + 4 * i] | (uint32_t)buf[64 + 4 * i + 1] << 8 | (uint32_t)buf[64 + 4 * i + 2] << 16 | (uint32_t)buf[64 + 4 * i + 3] << 24; if (k0 != ref0[i] || k1 != ref1[i]) match = 0; } ok("NEON ChaCha20 matches scalar reference (2 blocks)", match); } /* (3) the cipher is a real XOR stream: applying it twice is identity */ { uint8_t plain[128], work[128], k2[32]; for (i = 0; i < 128; i++) plain[i] = (uint8_t)(i * 7 + 1); for (i = 0; i < 32; i++) k2[i] = (uint8_t)(i * 3); memcpy(work, plain, 128); fb_chacha20(work, 128, k2, 1); ok("chacha20 actually changes data", memcmp(work, plain, 128) != 0); fb_chacha20(work, 128, k2, 1); ok("chacha20 round-trips (XOR is involutive)", memcmp(work, plain, 128) == 0); } } static void check_physics(void) { /* two equal masses released from rest must accelerate toward each * other: symmetric, momentum-conserving, and bounded. */ double bodies[2 * 8] = {0}; double total_p; bodies[0] = -1.0; bodies[3] = 1.0; /* body 0 at x=-1, mass 1 */ bodies[8] = 1.0; bodies[11] = 1.0; /* body 1 at x=+1, mass 1 */ fb_physics(bodies, 2, 200); /* velocities must be equal and opposite (Newton's third law) */ total_p = bodies[4] + bodies[12]; /* vx0 + vx1 */ note(" 2-body: vx0=%.6f vx1=%.6f (sum should be ~0)\n", bodies[4], bodies[12]); ok("physics conserves momentum", fabs(total_p) < 1e-9); ok("physics: bodies attract", bodies[4] > 0.0 && bodies[12] < 0.0); ok("physics values stay finite", isfinite(bodies[0]) && isfinite(bodies[8])); } static int cmp_u32(const void *p, const void *q) { uint32_t x = *(const uint32_t *)p, y = *(const uint32_t *)q; return (x > y) - (x < y); } static int is_sorted(const uint32_t *a, size_t n) { for (size_t i = 1; i < n; i++) if (a[i - 1] > a[i]) return 0; return 1; } static void check_sort(void) { enum { N = 10000 }; uint32_t *a = malloc(N * sizeof(uint32_t)); uint32_t *b = malloc(N * sizeof(uint32_t)); uint64_t s; size_t i; uint32_t r = 12345; for (i = 0; i < N; i++) { r = r * 1103515245u + 12345u; a[i] = r; } memcpy(b, a, N * sizeof(uint32_t)); s = fb_sort(a, N); ok("sort produces sorted output", is_sorted(a, N)); /* multiset is preserved: sort the reference with the C library and * compare element by element */ qsort(b, N, sizeof(uint32_t), cmp_u32); ok("sort is a permutation of the input", memcmp(a, b, N * sizeof(uint32_t)) == 0); /* already-sorted input stays sorted and gives the same checksum */ { uint64_t s2 = fb_sort(a, N); ok("sort is idempotent on sorted data", is_sorted(a, N) && s2 == s); } ok("sort of empty array is zero", fb_sort(a, 0) == 0); free(a); free(b); } static void check_chase(void) { /* build a tiny 4-node cycle by hand and confirm the walk returns to * the start after exactly `n` steps (offset 0 relative to entry) */ void *nodes[4]; nodes[0] = &nodes[1]; nodes[1] = &nodes[2]; nodes[2] = &nodes[3]; nodes[3] = &nodes[0]; /* 4 hops from &nodes[0] returns to &nodes[0]; fb_chase returns the * final pointer minus the starting pointer, so a full loop gives 0 */ ok("chase completes a full cycle", fb_chase(nodes, 4) == 0); ok("chase(0) is zero", fb_chase(nodes, 0) == 0); /* one hop lands on &nodes[1], i.e. one pointer-width past the start */ ok("chase single hop offset", fb_chase(nodes, 1) == (uint64_t)((char *)&nodes[1] - (char *)&nodes[0])); } int main(void) { long n = sysconf(_SC_NPROCESSORS_ONLN); fb_ncores = n > 0 ? n : 1; printf("\nfossbench kernel correctness tests\n"); printf("=================================\n"); printf("running each check on %ld core%s in parallel\n\n", fb_ncores, fb_ncores == 1 ? "" : "s"); printf("Integer Math:\n"); parallel(check_int); printf("Floating Point Math:\n"); parallel(check_fp); printf("Prime Numbers:\n"); parallel(check_primes); printf("Extended Instructions:\n"); parallel(check_simd); printf("Compression:\n"); parallel(check_compress); printf("Encryption:\n"); parallel(check_crypto); printf("Physics:\n"); parallel(check_physics); printf("Sorting:\n"); parallel(check_sort); /* the pointer chase is the single-threaded test: run it on one core */ printf("Single-Threaded (chase):\n"); check_chase(); printf("\n=================================\n"); printf("%d checks, %d failures\n\n", checks, failures); return failures ? 1 : 0; }