Files
roto/upb_test/hackers_bench.c
T

410 lines
14 KiB
C
Raw Normal View History

2026-05-04 14:40:11 -07:00
/*
* hackers_bench.c — C/upb benchmark mirroring benches/hackers_bench.rs
*
* Proto: proto/hackers.proto
* Generated files: hackers.upb.h / .c, hackers.upb_minitable.h / .c
*
* Build: make
* Run: ./hackers_bench
*
* Data files are read from ../data/bench/<name>.pb — the same files
* produced by `cargo run --release --bin gen_bench_data -- --preset <name>`.
*
* The four benchmark groups match the Rust/criterion groups exactly:
*
* shallow_parse — Campaign_parse() + Arena_Free() per iteration.
* upb fully decodes the message; roto merely scans
* for field offsets. This is the most important
* comparison: total cost to "be ready to read".
*
* deep_parse — parse + walk Campaign → Operations → every Hacker,
* touching each Hacker's handle field.
*
* field_access — message pre-parsed once outside the loop; each
* micro-benchmark times a single field read.
* upb: direct struct lookup. roto: decode at offset.
*
* iterate — count_operations: parse + count top-level repeated.
* count_all_crew: parse + count nested repeated.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <time.h>
#include "hackers.upb.h"
#include "hackers.upb_minitable.h"
/* ==========================================================================
* Timing
* ========================================================================== */
static uint64_t now_ns(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec;
}
/* ==========================================================================
* Black-box sink — prevents the compiler from optimising away benchmark work.
* We write the result of every meaningful computation here.
* ========================================================================== */
static volatile uintptr_t g_sink;
/* ==========================================================================
* File I/O
* ========================================================================== */
typedef struct {
uint8_t *data;
size_t len;
char path[256];
} BenchData;
static bool load_bench_data(BenchData *out, const char *name) {
2026-05-04 23:37:36 -07:00
snprintf(out->path, sizeof(out->path), "data/bench/%s.pb", name);
2026-05-04 14:40:11 -07:00
FILE *f = fopen(out->path, "rb");
if (!f) {
printf("[skip] %s not found — "
"run `cargo run --release --bin gen_bench_data -- --preset %s` first\n",
out->path, name);
out->data = NULL;
out->len = 0;
return false;
}
fseek(f, 0, SEEK_END);
out->len = (size_t)ftell(f);
rewind(f);
out->data = malloc(out->len);
if (!out->data) { fclose(f); return false; }
fread(out->data, 1, out->len, f);
fclose(f);
return true;
}
static void free_bench_data(BenchData *d) {
free(d->data);
d->data = NULL;
}
/* ==========================================================================
* Benchmark runner
*
* Finds a batch size such that one batch takes ≥1 ms, then runs batches
* until at least BENCH_MIN_SECS of wall time has elapsed. Reports the
* mean ns/iter and, if bytes > 0, the MB/s throughput.
* ========================================================================== */
#define BENCH_MIN_SECS 0.5
typedef void (*bench_fn)(void *state);
static void run_bench(bench_fn fn, void *state, size_t bytes, const char *label) {
/* warmup */
for (int i = 0; i < 5; i++) fn(state);
/* calibrate: find batch size so one batch ≥ 1 ms */
uint64_t batch = 1;
while (batch < 10000000ULL) {
uint64_t t0 = now_ns();
for (uint64_t i = 0; i < batch; i++) fn(state);
if (now_ns() - t0 >= 1000000ULL) break; /* 1 ms */
batch *= 4;
}
/* measure */
uint64_t target_ns = (uint64_t)(BENCH_MIN_SECS * 1e9);
uint64_t total_ns = 0;
uint64_t total_its = 0;
while (total_ns < target_ns) {
uint64_t t0 = now_ns();
for (uint64_t i = 0; i < batch; i++) fn(state);
total_ns += now_ns() - t0;
total_its += batch;
}
double ns_per_iter = (double)total_ns / (double)total_its;
if (bytes > 0) {
double mb_per_sec = (double)bytes / ns_per_iter * 1000.0;
printf(" %-46s %9.2f ns/iter %8.2f MB/s\n",
label, ns_per_iter, mb_per_sec);
} else {
printf(" %-46s %9.2f ns/iter\n", label, ns_per_iter);
}
}
/* ==========================================================================
* shallow_parse — Campaign_parse() + upb_Arena_Free() per iteration
*
* Measures the full cost of becoming "ready to access any field", matching
* the Rust `Campaign::new()` benchmark. upb fully decodes; roto only scans.
* ========================================================================== */
static void fn_shallow_parse(void *state) {
BenchData *d = state;
upb_Arena *arena = upb_Arena_New();
Campaign *c = Campaign_parse((const char *)d->data, d->len, arena);
g_sink = (uintptr_t)c;
upb_Arena_Free(arena);
}
static void bench_shallow_parse(void) {
const char *sizes[] = {"tiny", "small", "medium", "large", NULL};
printf("\n=== shallow_parse ===\n");
for (int i = 0; sizes[i]; i++) {
BenchData d;
if (!load_bench_data(&d, sizes[i])) continue;
char label[80];
snprintf(label, sizeof(label), "Campaign_parse/%s [%zu B]", sizes[i], d.len);
run_bench(fn_shallow_parse, &d, d.len, label);
free_bench_data(&d);
}
}
/* ==========================================================================
* deep_parse — parse + walk Campaign → Operations → Hackers
*
* After Campaign_parse(), upb has already decoded everything. The "deep"
* walk is pointer-chasing through the decoded tree. In roto each level
* calls ::new(), paying another linear scan over that sub-message's bytes.
* ========================================================================== */
static void fn_deep_parse(void *state) {
BenchData *d = state;
upb_Arena *arena = upb_Arena_New();
Campaign *c = Campaign_parse((const char *)d->data, d->len, arena);
size_t n_ops;
const Operation * const *ops = Campaign_operations(c, &n_ops);
size_t hacker_count = 0;
for (size_t i = 0; i < n_ops; i++) {
size_t n_crew;
const Hacker * const *crew = Operation_crew(ops[i], &n_crew);
for (size_t j = 0; j < n_crew; j++) {
upb_StringView handle = Hacker_handle(crew[j]);
g_sink = (uintptr_t)handle.data;
hacker_count++;
}
}
g_sink = hacker_count;
upb_Arena_Free(arena);
}
static void bench_deep_parse(void) {
const char *sizes[] = {"tiny", "small", "medium", NULL};
printf("\n=== deep_parse ===\n");
for (int i = 0; sizes[i]; i++) {
BenchData d;
if (!load_bench_data(&d, sizes[i])) continue;
char label[80];
snprintf(label, sizeof(label), "Campaign+Ops+Hackers/%s [%zu B]", sizes[i], d.len);
run_bench(fn_deep_parse, &d, d.len, label);
free_bench_data(&d);
}
}
/* ==========================================================================
* field_access — individual field reads on a pre-parsed message
*
* Parse once outside the loop; each micro-benchmark measures the accessor
* call itself. upb: a struct-field read with a MiniTable lookup.
* roto: decode the value at a pre-recorded byte offset.
* ========================================================================== */
typedef struct {
upb_Arena *arena;
Campaign *campaign;
Operation *op;
Hacker *hacker;
Worm *worm;
} FieldState;
static void fn_field_campaign_name(void *s) {
upb_StringView v = Campaign_name(((FieldState *)s)->campaign);
g_sink = (uintptr_t)v.data;
}
static void fn_field_total_bytes_stolen(void *s) {
g_sink = (uintptr_t)(uint64_t)Campaign_total_bytes_stolen(((FieldState *)s)->campaign);
}
static void fn_field_op_codename(void *s) {
upb_StringView v = Operation_codename(((FieldState *)s)->op);
g_sink = (uintptr_t)v.data;
}
static void fn_field_op_timestamp(void *s) {
g_sink = (uintptr_t)(uint64_t)Operation_timestamp(((FieldState *)s)->op);
}
static void fn_field_op_successful(void *s) {
g_sink = (uintptr_t)Operation_successful(((FieldState *)s)->op);
}
static void fn_field_hacker_handle(void *s) {
upb_StringView v = Hacker_handle(((FieldState *)s)->hacker);
g_sink = (uintptr_t)v.data;
}
static void fn_field_hacker_skill_level(void *s) {
/* store float bits to avoid FPU → int conversion costs */
float f = Hacker_skill_level(((FieldState *)s)->hacker);
uint32_t bits; memcpy(&bits, &f, 4);
g_sink = bits;
}
static void fn_field_hacker_is_elite(void *s) {
g_sink = (uintptr_t)Hacker_is_elite(((FieldState *)s)->hacker);
}
static void fn_field_worm_polymorphic(void *s) {
g_sink = (uintptr_t)Worm_polymorphic(((FieldState *)s)->worm);
}
static void fn_field_worm_payload(void *s) {
upb_StringView v = Worm_payload(((FieldState *)s)->worm);
g_sink = (uintptr_t)v.data;
}
static void bench_field_access(void) {
BenchData d;
if (!load_bench_data(&d, "small")) return;
upb_Arena *arena = upb_Arena_New();
Campaign *campaign = Campaign_parse((const char *)d.data, d.len, arena);
if (!campaign) { fprintf(stderr, "parse failed\n"); return; }
size_t n_ops;
const Operation * const *ops = Campaign_operations(campaign, &n_ops);
if (n_ops == 0) { fprintf(stderr, "no operations\n"); return; }
Operation *op = (Operation *)ops[0]; /* cast away const for state */
size_t n_crew;
const Hacker * const *crew = Operation_crew(op, &n_crew);
if (n_crew == 0) { fprintf(stderr, "no crew\n"); return; }
Hacker *hacker = (Hacker *)crew[0];
const Worm *worm = Operation_worm(op);
if (!worm) { fprintf(stderr, "no worm\n"); return; }
FieldState state = {
.arena = arena,
.campaign = campaign,
.op = op,
.hacker = hacker,
.worm = (Worm *)worm,
};
printf("\n=== field_access ===\n");
run_bench(fn_field_campaign_name, &state, 0, "campaign::name");
run_bench(fn_field_total_bytes_stolen, &state, 0, "campaign::total_bytes_stolen");
run_bench(fn_field_op_codename, &state, 0, "operation::codename");
run_bench(fn_field_op_timestamp, &state, 0, "operation::timestamp");
run_bench(fn_field_op_successful, &state, 0, "operation::successful");
run_bench(fn_field_hacker_handle, &state, 0, "hacker::handle");
run_bench(fn_field_hacker_skill_level, &state, 0, "hacker::skill_level (f32)");
run_bench(fn_field_hacker_is_elite, &state, 0, "hacker::is_elite (bool)");
run_bench(fn_field_worm_polymorphic, &state, 0, "worm::polymorphic (bool)");
run_bench(fn_field_worm_payload, &state, 0, "worm::payload (bytes)");
upb_Arena_Free(arena);
free_bench_data(&d);
}
/* ==========================================================================
* iterate — count repeated fields at different depths
*
* count_operations: after parsing, Campaign_operations() returns pointer+count
* in O(1) — upb already decoded the array.
* roto's Campaign::new() scan IS the counting work.
*
* count_all_crew: parse + walk ops + sum crew sizes.
* ========================================================================== */
static void fn_count_operations(void *state) {
BenchData *d = state;
upb_Arena *arena = upb_Arena_New();
Campaign *c = Campaign_parse((const char *)d->data, d->len, arena);
size_t n;
Campaign_operations(c, &n);
g_sink = n;
upb_Arena_Free(arena);
}
static void fn_count_all_crew(void *state) {
BenchData *d = state;
upb_Arena *arena = upb_Arena_New();
Campaign *c = Campaign_parse((const char *)d->data, d->len, arena);
size_t n_ops;
const Operation * const *ops = Campaign_operations(c, &n_ops);
size_t total = 0;
for (size_t i = 0; i < n_ops; i++) {
size_t n_crew;
Operation_crew(ops[i], &n_crew);
total += n_crew;
}
g_sink = total;
upb_Arena_Free(arena);
}
2026-05-04 23:13:53 -07:00
static void fn_read_update_write(void *state) {
BenchData *d = state;
upb_Arena *arena = upb_Arena_New();
Campaign *c = Campaign_parse((const char *)d->data, d->len, arena);
upb_StringView name = { .data = "updated", .size = 7 };
Campaign_set_name(c, name);
size_t len;
char *out = Campaign_serialize(c, arena, &len);
g_sink = (uintptr_t)out;
upb_Arena_Free(arena);
}
static void bench_read_update_write(void) {
const char *sizes[] = {"tiny", "small", "medium", NULL};
printf("\n=== read_update_write ===\n");
for (int i = 0; sizes[i]; i++) {
BenchData d;
if (!load_bench_data(&d, sizes[i])) continue;
char label[80];
snprintf(label, sizeof(label), "Campaign_parse+set+serialize/%s [%zu B]", sizes[i], d.len);
run_bench(fn_read_update_write, &d, d.len, label);
free_bench_data(&d);
}
}
2026-05-04 14:40:11 -07:00
static void bench_iterate(void) {
const char *sizes[] = {"tiny", "small", "medium", NULL};
printf("\n=== iterate ===\n");
for (int i = 0; sizes[i]; i++) {
BenchData d;
if (!load_bench_data(&d, sizes[i])) continue;
char label[80];
snprintf(label, sizeof(label), "count_operations/%s [%zu B]", sizes[i], d.len);
run_bench(fn_count_operations, &d, d.len, label);
snprintf(label, sizeof(label), "count_all_crew/%s [%zu B]", sizes[i], d.len);
run_bench(fn_count_all_crew, &d, d.len, label);
free_bench_data(&d);
}
}
/* ==========================================================================
* main
* ========================================================================== */
int main(void) {
printf("hackers_bench (upb / protobuf %s)\n", "33.1");
printf("Data files: ../data/bench/<name>.pb\n");
printf("Run `cargo run --release --bin gen_bench_data -- --preset <name>` to generate.\n");
bench_shallow_parse();
bench_deep_parse();
bench_field_access();
bench_iterate();
2026-05-04 23:13:53 -07:00
bench_read_update_write();
2026-05-04 14:40:11 -07:00
printf("\n");
return 0;
}