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) {
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
}
|