bench: mono-vs-vdisp micro-benchmark + revised Decision 11 rationale
Hypothesis-driven measurement of "did monomorphisation actually buy us performance?" on a 100M-iter LCG hot loop, AILang mono'd code vs. four C reference variants (direct-inlinable, direct- noinline, indirect-monomorphic, indirect-polymorphic). Zen 3, clang -O2, median-of-15. Headline: H1 supported, but the mechanism is inlining, not dispatch shape. AILang mono = hand-C direct (1.000x). Indirect- monomorphic = direct-noinline (1.000x) — saturating branch predictor makes the indirect-call cost vanish on this hardware. Inlining is the actual 3.31x win; polymorphic indirect adds another 21% predictor-miss penalty. DESIGN.md Decision 11 gains a rationale paragraph reframing mono as inlining-enabler rather than indirect-call-eliminator, with explicit pointer to the bench. JOURNAL entry records the full methodology, ratios, limitations, and the side-effect mono-pass env.globals-seeding bug surfaced while building the AILang fixture (separate RED-first debug iter to follow).
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
// Hand-C reference for bench_mono_dispatch — DIRECT, INLINABLE variant.
|
||||
//
|
||||
// Mirrors the post-monomorphisation AILang IR: a tail-recursive loop
|
||||
// that calls `foo(i)` at every step. `foo` here has no `noinline`
|
||||
// attribute, so clang -O2 will inline it into the loop body. This is
|
||||
// the OPTIMISTIC bound on what AILang's mono pass can achieve once
|
||||
// LLVM optimises the resulting IR.
|
||||
//
|
||||
// Workload: loop_call(N, 0) with
|
||||
// acc' = acc + foo(acc + i)
|
||||
// foo(x) = x * 1103515245 + 12345 (LCG-ish, prevents closed-form)
|
||||
//
|
||||
// N = 100_000_000.
|
||||
//
|
||||
// The result is data-dependent on every prior iteration, so clang
|
||||
// cannot algebraically close-form-fold the loop; the wall-time is
|
||||
// dominated by the body.
|
||||
//
|
||||
// Build: clang -O2 -o bench_mono_direct bench_mono_direct.c
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
|
||||
static int64_t foo(int64_t x) {
|
||||
return x * 1103515245 + 12345;
|
||||
}
|
||||
|
||||
static int64_t loop_call(int64_t i, int64_t acc) {
|
||||
while (i != 0) {
|
||||
acc = acc + foo(acc + i);
|
||||
i = i - 1;
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("%lld\n", (long long)loop_call(100000000, 0));
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Hand-C reference for bench_mono_dispatch — DIRECT, NOINLINE variant.
|
||||
//
|
||||
// Same as bench_mono_direct.c, but `foo` is marked `noinline` so the
|
||||
// compiler must emit a real call instruction at every iteration. This
|
||||
// isolates the "real call" cost when the target is statically known —
|
||||
// the upper bound on what monomorphised dispatch can achieve when the
|
||||
// callee body is not inlinable.
|
||||
//
|
||||
// Pair this with bench_mono_indirect.c (same noinline, but called via
|
||||
// fnptr) to isolate "direct vs indirect call" cost on this hardware.
|
||||
//
|
||||
// Build: clang -O2 -o bench_mono_direct_noinline bench_mono_direct_noinline.c
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
|
||||
static int64_t foo(int64_t x) __attribute__((noinline));
|
||||
static int64_t foo(int64_t x) {
|
||||
return x * 1103515245 + 12345;
|
||||
}
|
||||
|
||||
static int64_t loop_call(int64_t i, int64_t acc) {
|
||||
while (i != 0) {
|
||||
acc = acc + foo(acc + i);
|
||||
i = i - 1;
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("%lld\n", (long long)loop_call(100000000, 0));
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Hand-C reference for bench_mono_dispatch — INDIRECT (fnptr) variant.
|
||||
//
|
||||
// Same algorithm as bench_mono_direct_noinline.c, but `foo` is reached
|
||||
// through a function pointer assigned at runtime. This represents the
|
||||
// "virtual dispatch" / "dictionary-passing" lower bound: the call
|
||||
// target is opaque to the optimiser at compile time, so clang cannot
|
||||
// inline and emits an indirect call (`callq *<reg>`).
|
||||
//
|
||||
// The fnptr is assigned from main via a volatile-pointer indirection
|
||||
// to defeat any speculative devirtualisation clang -O2 might attempt
|
||||
// (without PGO it does not devirt monomorphic-target indirect calls,
|
||||
// but the volatile guard makes that explicit and stable across clang
|
||||
// versions).
|
||||
//
|
||||
// `foo` keeps `noinline` so we measure dispatch-shape, not body size.
|
||||
// The pair (direct_noinline vs indirect) isolates exactly the
|
||||
// indirect-call cost on this hardware.
|
||||
//
|
||||
// Build: clang -O2 -o bench_mono_indirect bench_mono_indirect.c
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
|
||||
static int64_t foo(int64_t x) __attribute__((noinline));
|
||||
static int64_t foo(int64_t x) {
|
||||
return x * 1103515245 + 12345;
|
||||
}
|
||||
|
||||
typedef int64_t (*foo_fn_t)(int64_t);
|
||||
|
||||
static int64_t loop_call(foo_fn_t fp, int64_t i, int64_t acc) {
|
||||
while (i != 0) {
|
||||
acc = acc + fp(acc + i);
|
||||
i = i - 1;
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
// Volatile barrier so the optimiser sees the target as
|
||||
// dynamically-determined — it cannot prove the fnptr is
|
||||
// monomorphic at the call site without PGO.
|
||||
foo_fn_t volatile fp_v = &foo;
|
||||
foo_fn_t fp = fp_v;
|
||||
printf("%lld\n", (long long)loop_call(fp, 100000000, 0));
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Hand-C reference for bench_mono_dispatch — INDIRECT POLYMORPHIC variant.
|
||||
//
|
||||
// Same structure as bench_mono_indirect.c, but the fnptr is selected
|
||||
// per-iteration from a small array of FOUR distinct (and genuinely
|
||||
// different) `foo` implementations. This models a polymorphic class
|
||||
// hierarchy under hypothetical vdisp / dict-passing: the indirect
|
||||
// target varies, so the branch predictor cannot lock onto a single
|
||||
// destination. The four bodies are deliberately distinct (different
|
||||
// constants and operations) so clang's mergefunc cannot collapse them.
|
||||
//
|
||||
// This is the LOWER bound on what mono offers — predictor misses on
|
||||
// indirect dispatch when the call site sees more than one instance.
|
||||
//
|
||||
// IMPORTANT: this binary's stdout does NOT match the monomorphic
|
||||
// variants — different `foo` semantics produce a different final acc.
|
||||
// The harness skips the equality check for this binary; only the
|
||||
// timing matters here.
|
||||
//
|
||||
// Build: clang -O2 -o bench_mono_indirect_polymorphic bench_mono_indirect_polymorphic.c
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
|
||||
static int64_t foo_a(int64_t x) __attribute__((noinline));
|
||||
static int64_t foo_b(int64_t x) __attribute__((noinline));
|
||||
static int64_t foo_c(int64_t x) __attribute__((noinline));
|
||||
static int64_t foo_d(int64_t x) __attribute__((noinline));
|
||||
|
||||
// Four genuinely different bodies — different constants, different
|
||||
// op shapes. Same instruction count and roughly same cost so the
|
||||
// "average" body cost matches the monomorphic case; what differs
|
||||
// is the dispatch shape.
|
||||
static int64_t foo_a(int64_t x) { return x * 1103515245 + 12345; }
|
||||
static int64_t foo_b(int64_t x) { return x * 1664525 + 1013904223; }
|
||||
static int64_t foo_c(int64_t x) { return x * 22695477 + 1; }
|
||||
static int64_t foo_d(int64_t x) { return x * 214013 + 2531011; }
|
||||
|
||||
typedef int64_t (*foo_fn_t)(int64_t);
|
||||
|
||||
static int64_t loop_call(foo_fn_t * volatile fps, int64_t i, int64_t acc) {
|
||||
while (i != 0) {
|
||||
// i & 3 cycles through the 4 fnptrs every iteration.
|
||||
foo_fn_t fp = fps[i & 3];
|
||||
acc = acc + fp(acc + i);
|
||||
i = i - 1;
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
foo_fn_t fps[4] = { &foo_a, &foo_b, &foo_c, &foo_d };
|
||||
foo_fn_t * volatile fps_v = fps;
|
||||
printf("%lld\n", (long long)loop_call(fps_v, 100000000, 0));
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user