4cacfcbdac
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).
48 lines
1.6 KiB
C
48 lines
1.6 KiB
C
// 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;
|
|
}
|