/* * 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/.pb — the same files * produced by `cargo run --release --bin gen_bench_data -- --preset `. * * 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 #include #include #include #include #include #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) { snprintf(out->path, sizeof(out->path), "../data/bench/%s.pb", name); 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); } 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/.pb\n"); printf("Run `cargo run --release --bin gen_bench_data -- --preset ` to generate.\n"); bench_shallow_parse(); bench_deep_parse(); bench_field_access(); bench_iterate(); printf("\n"); return 0; }