kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit 537cb4cc23c3e08565bb8bd04a6bf6ca9d04c399
parent 064a82b6a17001f25f47a4e05ed1df0cf653d60c
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 13 Jun 2026 09:31:02 -0700

perf(mc): typed-store byte sink — mc_emit32 cursor, kill per-insn libc memcpy

aa_emit32/rv64_emit32 staged each instruction word into u8[4] then called the
out-of-line mc_emit_bytes -> buf_write -> memcpy(n=4); the call boundary blocked
const-propagating n=4 so it lowered to a libc memcpy call (~6.94M Ir / 434K
calls, the dominant memcpy in the compile).

Add mc_emit32(MCEmitter*, u32): a typed-store fast path that writes the word
straight through a {cur,cur_end} cursor cached on the MCEmitter base (into the
active section's tail chunk) with one const-width 4-byte store, then bumps
cur_chunk->used + cur_bytes->total inline so the Buf stays coherent at all
times. No staging buffer, no out-of-line memcpy call. The coherent-always
invariant (cur == cur_chunk->data + cur_chunk->used; used/total kept live)
means every random-access reader (obj_pos/buf_patch/mc_pos, plus arch-direct
obj_pos/obj_patch) sees the live frontier with no explicit sync. The cursor is
(re)pointed via mc_cursor_repoint at set_section and after any non-cursor write
(mc_emit_bytes / mc_emit_fill). aarch64 + riscv64 leaf emitters call it
directly; x86-64 (variable-width) keeps mc_emit_bytes unchanged.

Byte-identical: perf identity gate 61/61 PASS (objects -O0/-O1, -g DWARF, -S
asm, linked exe) + cross-arch rv64/x64/aa64 object/-g/-S byte-identity diff.

Diffstat:
Msrc/arch/aa64/native.c | 9++++-----
Msrc/arch/mc.c | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/arch/mc.h | 26++++++++++++++++++++++++++
Msrc/arch/riscv/native.c | 14+++++++++-----
4 files changed, 94 insertions(+), 10 deletions(-)

diff --git a/src/arch/aa64/native.c b/src/arch/aa64/native.c @@ -327,16 +327,15 @@ extern void debug_func_pc_range(Debug*, ObjSecId text_section, u32 begin_ofs, u32 end_ofs); static void aa_emit32(MCEmitter* mc, u32 word) { - u8 b[4]; - wr_u32_le(b, word); /* obj_pos is a section lookup + buf_pos; only the -g line table needs it, so - * skip it on the common no-debug compile (one fewer lookup per instruction). */ + * skip it on the common no-debug compile (one fewer lookup per instruction). + * The pre-write offset must be read BEFORE mc_emit32 advances the cursor. */ if (mc->debug) { u32 ofs = obj_pos(mc->obj, mc->section_id); - mc_emit_bytes(mc, b, sizeof b); + mc_emit32(mc, word); debug_emit_row(mc->debug, mc->section_id, ofs, mc->loc); } else { - mc_emit_bytes(mc, b, sizeof b); + mc_emit32(mc, word); } } diff --git a/src/arch/mc.c b/src/arch/mc.c @@ -30,6 +30,7 @@ #include "arch/arch.h" #include "core/arena.h" #include "core/buf.h" +#include "core/bytes.h" #include "core/heap.h" #include "core/pool.h" #include "core/strbuf.h" @@ -234,6 +235,28 @@ ObjSymId mc_label_symbol(MCEmitter* m, MCLabel id) { /* ---- emission ops (called directly by the arch backends) ---- */ +/* (Re)point the typed-store cursor at the active section's tail chunk write + * frontier. Called after set_section and after any non-cursor write path + * (mc_emit_bytes / fills) that may have grown or replaced the tail chunk. The + * cursor is left NULL/empty (forcing the slow path) when there is no PROGBITS + * buffer or the tail chunk is full or absent — buf_write_slow then mints/grows + * the chunk and the next repoint picks it up. The invariant maintained + * everywhere: cur == cur_chunk->data + cur_chunk->used (when cur_chunk != NULL), + * with cur_chunk->used / cur_bytes->total kept coherent by mc_emit32. */ +static void mc_cursor_repoint(MCEmitter* m) { + Buf* b = m->cur_bytes; + BufChunk* t = b ? b->tail : NULL; + if (t) { + m->cur_chunk = t; + m->cur = t->data + t->used; + m->cur_end = t->data + t->cap; + } else { + m->cur_chunk = NULL; + m->cur = NULL; + m->cur_end = NULL; + } +} + void mc_emit_bytes(MCEmitter* m, const u8* data, size_t n) { /* Fast path: append straight to the cached section buffer (inlined * buf_write). cur_bytes is NULL for NOBITS/.bss/none, where obj_write does @@ -242,6 +265,34 @@ void mc_emit_bytes(MCEmitter* m, const u8* data, size_t n) { buf_write(m->cur_bytes, data, n); else obj_write(m->obj, m->section_id, data, n); + /* buf_write may have grown/replaced the tail chunk and bumped used/total; + * re-point the typed-store cursor so the next mc_emit32 stays coherent. */ + mc_cursor_repoint(m); +} + +/* Out-of-line slow path for mc_emit32: the tail chunk is full, absent, or the + * section is NOBITS. Route through the normal byte sink (which keeps used/total + * coherent and re-points the cursor), so the fast path can branch on a simple + * pointer-bounds test. */ +static void mc_emit32_slow(MCEmitter* m, u32 word) { + u8 b[4]; + wr_u32_le(b, word); + mc_emit_bytes(m, b, sizeof b); +} + +void mc_emit32(MCEmitter* m, u32 word) { + /* Typed-store fast path: one const-width 4-byte store through the cursor, + * then bump the chunk/buffer lengths to keep the Buf coherent. No staging + * buffer, no out-of-line memcpy call. */ + u8* cur = m->cur; + if (cur && cur + 4 <= m->cur_end) { + wr_u32_le(cur, word); + m->cur = cur + 4; + m->cur_chunk->used += 4u; + m->cur_bytes->total += 4u; + } else { + mc_emit32_slow(m, word); + } } u32 mc_pos(MCEmitter* m) { return obj_pos(m->obj, m->section_id); } @@ -254,6 +305,7 @@ void mc_set_section(MCEmitter* m, u32 section_id) { * per-instruction Sections_at deref + nobits branch. NULL for NOBITS/.bss * (or none): emit then falls back to obj_write for bss_size accounting. */ m->cur_bytes = obj_section_bytes(m->obj, section_id); + mc_cursor_repoint(m); } MCLabel mc_label_new(MCEmitter* m) { @@ -315,6 +367,9 @@ void mc_emit_fill(MCEmitter* m, size_t n, u8 byte) { obj_write(m->obj, m->section_id, buf, k); n -= k; } + /* obj_write grew the tail directly (bypassing the typed-store cursor); + * re-point so a following mc_emit32 stays coherent. */ + mc_cursor_repoint(m); } void mc_emit_align(MCEmitter* m, u32 align, u8 fill) { diff --git a/src/arch/mc.h b/src/arch/mc.h @@ -59,6 +59,23 @@ struct MCEmitter { * re-points it. */ Buf* cur_bytes; + /* Typed-store fast-path cursor into the active section's tail chunk. The + * per-instruction emit_word path (mc_emit32) writes a fixed-width word + * straight through `cur` and bumps `cur_chunk->used` + `cur_bytes->total` + * inline, so the Buf stays coherent at all times — no deferred sync, no + * libc memcpy call. `cur` always equals `cur_chunk->data + cur_chunk->used` + * and `cur_end` is `cur_chunk->data + cur_chunk->cap`; the cursor is + * (re)pointed from cur_bytes->tail by mc_cursor_repoint() at set_section and + * after any non-cursor write (mc_emit_bytes / fills) that may have grown or + * replaced the tail chunk. All three are NULL/empty when the active section + * is NOBITS/.bss (or none): emit then falls to obj_write via mc_emit_bytes. + * Because used/total stay coherent, every random-access reader (obj_pos, + * buf_patch, mc_pos, arch-direct obj_pos/obj_patch) sees the live frontier + * without any explicit sync. */ + BufChunk* cur_chunk; + u8* cur; + u8* cur_end; + /* Pending source location, updated by set_loc. Promoted to the base so * arch backends' emit-bytes choke point can read it without reaching * into the per-arch impl (used to feed debug_emit_row). */ @@ -95,6 +112,15 @@ void mc_free(MCEmitter*); /* Append machine-code bytes to the active section (the hot emit path). */ void mc_emit_bytes(MCEmitter*, const u8* data, size_t n); +/* Append one little-endian 4-byte instruction word to the active section. + * Typed-store fast path: const-width store straight through the section + * cursor (no u8[4] staging, no out-of-line memcpy call). Fixed-width-insn + * arches (aarch64, riscv64) call this from their leaf emitter instead of + * staging a word into u8[4] + mc_emit_bytes. Variable-width arches (x86-64) + * keep using mc_emit_bytes. Keeps the Buf coherent, so any random-access + * reader still sees the live frontier. Falls to the buf_write path when the + * tail chunk can't hold the word or the section is NOBITS. */ +void mc_emit32(MCEmitter*, u32 word); /* Current byte offset within the active section. */ u32 mc_pos(MCEmitter*); /* Stamp the pending source location read by the per-arch emit choke point. */ diff --git a/src/arch/riscv/native.c b/src/arch/riscv/native.c @@ -79,11 +79,15 @@ extern void debug_func_pc_range(Debug*, ObjSecId text_section, u32 begin_ofs, /* ============================ low-level emit ============================ */ void rv64_emit32(MCEmitter* mc, u32 word) { - u8 b[4]; - u32 ofs = obj_pos(mc->obj, mc->section_id); - wr_u32_le(b, word); - mc_emit_bytes(mc, b, sizeof b); - if (mc->debug) debug_emit_row(mc->debug, mc->section_id, ofs, mc->loc); + /* Read the pre-write offset (only the -g line table needs it) before + * mc_emit32 advances the cursor. */ + if (mc->debug) { + u32 ofs = obj_pos(mc->obj, mc->section_id); + mc_emit32(mc, word); + debug_emit_row(mc->debug, mc->section_id, ofs, mc->loc); + } else { + mc_emit32(mc, word); + } } void rv64_emit16(MCEmitter* mc, u32 halfword) {