commit 7f080a6f5976c89447a7c7b11e77ea44b71dde1b
parent bc5ef9d8e29b2fb214e596198202d19cc21c956d
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 10 Jun 2026 22:24:09 -0700
perf(emit): tail-chunk buffer fast path + batch aa64 prologue patch/reserve
Three byte-identical emit-path wins, biggest on fn-count (many tiny functions):
- buf_walk (src/core/buf.c): emit-time patch-ups and reads overwhelmingly target
the most-recently-written bytes (a function's own just-emitted prologue/
branches), which live in the last indexed chunk. Check it before the
directory binary search → O(1) common case instead of O(log n_chunks). Shared
by every object emit + the DWARF patcher.
- aa_patch_prologue: patch the reserved prologue region in ONE obj_patch over a
serialized byte buffer instead of up to 32 single-word aa_patch32 calls (each
a chunk-directory lookup); mirrors the x64 backend.
- aa_func_begin: reserve the 32-word worst-case prologue as one bulk emit_bytes
instead of 32 aa_emit32 calls. -g line rows replicated so debug output is
unchanged.
- aa_emit32: skip the per-instruction obj_pos (section lookup + buf_pos) on the
common no-debug compile; it is only needed for the -g line table.
fn-count 1049->787ms (-25%), body-size 2167->1917ms (-11%). Object output
byte-identical on aa64-macos/-linux including -g. Verified: test-isa/aa64-inline/
toy/debug/dwarf/link/elf/macho/smoke-x64/smoke-rv64 all green.
Diffstat:
2 files changed, 57 insertions(+), 22 deletions(-)
diff --git a/src/arch/aa64/native.c b/src/arch/aa64/native.c
@@ -316,11 +316,16 @@ extern void debug_func_pc_range(Debug*, ObjSecId text_section, u32 begin_ofs,
static void aa_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);
- /* Record one line-table row per instruction start (no-op when not -g). */
- if (mc->debug) debug_emit_row(mc->debug, mc->section_id, ofs, mc->loc);
+ /* 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). */
+ if (mc->debug) {
+ u32 ofs = obj_pos(mc->obj, mc->section_id);
+ mc->emit_bytes(mc, b, sizeof b);
+ debug_emit_row(mc->debug, mc->section_id, ofs, mc->loc);
+ } else {
+ mc->emit_bytes(mc, b, sizeof b);
+ }
}
static void aa_patch32(ObjBuilder* obj, ObjSecId sec, u32 off, u32 word) {
@@ -1143,7 +1148,22 @@ static void aa_func_begin(NativeTarget* t, const CGFuncDesc* fd) {
AANativeTarget* a = aa_of(t);
MCEmitter* mc = t->mc;
aa_func_begin_common(t, fd);
- for (u32 i = 0; i < AA_PROLOGUE_WORDS; ++i) aa_emit32(mc, 0xd503201fu);
+ /* Reserve the worst-case prologue as one bulk emit (32 NOP words) instead of
+ * 32 single-word aa_emit32 calls. Replicate the per-instruction -g line rows
+ * so debug output is unchanged. */
+ {
+ u8 nops[AA_PROLOGUE_WORDS * 4u];
+ for (u32 i = 0; i < AA_PROLOGUE_WORDS; ++i)
+ wr_u32_le(nops + i * 4u, 0xd503201fu);
+ if (mc->debug) {
+ u32 ofs = obj_pos(mc->obj, mc->section_id);
+ mc->emit_bytes(mc, nops, sizeof nops);
+ for (u32 i = 0; i < AA_PROLOGUE_WORDS; ++i)
+ debug_emit_row(mc->debug, mc->section_id, ofs + i * 4u, mc->loc);
+ } else {
+ mc->emit_bytes(mc, nops, sizeof nops);
+ }
+ }
aa_emit_entry_saves(a);
}
@@ -1501,8 +1521,14 @@ static void aa_patch_prologue(AANativeTarget* a, const AAFrameLayout* L,
words[n] = aa64_b(region - n);
for (u32 i = n + 1u; i < region; ++i) words[i] = 0xd503201fu;
}
- for (u32 i = 0; i < region; ++i)
- aa_patch32(a->base.obj, sec, a->prologue_pos + i * 4u, words[i]);
+ /* One contiguous patch over the reserved region rather than `region`
+ * separate obj_patch calls (each a chunk-directory lookup + small memcpy);
+ * mirrors the x64 backend. */
+ {
+ u8 pbytes[AA_PROLOGUE_WORDS * 4u];
+ for (u32 i = 0; i < region; ++i) wr_u32_le(pbytes + i * 4u, words[i]);
+ obj_patch(a->base.obj, sec, a->prologue_pos, pbytes, (size_t)region * 4u);
+ }
}
static void aa_emit_restore_frame(AANativeTarget* a, const AAFrameLayout* L) {
diff --git a/src/core/buf.c b/src/core/buf.c
@@ -116,22 +116,31 @@ static inline void buf_walk(const Buf* b, u32 ofs, void* external, size_t n,
BufChunk* c;
u32 chunk_start;
u8* ext = (u8*)external;
- /* Binary-search the directory for the last chunk whose start is <= ofs, then
- * walk forward from there (the range may span into following chunks). */
- u32 lo = 0, hi = b->ndir;
- while (lo < hi) {
- u32 mid = lo + ((hi - lo) >> 1);
- if (b->dir[mid].start <= ofs)
- lo = mid + 1;
- else
- hi = mid;
- }
- if (lo == 0) {
- c = b->head;
- chunk_start = 0;
+ /* Fast path: emit-time patch-ups and reads overwhelmingly target the
+ * most-recently-written bytes (a function's own just-emitted prologue and
+ * branches), which live in the last indexed chunk. Check it before the binary
+ * search so the common case is O(1) instead of O(log n_chunks). */
+ if (b->ndir > 0 && ofs >= b->dir[b->ndir - 1].start) {
+ c = b->dir[b->ndir - 1].chunk;
+ chunk_start = b->dir[b->ndir - 1].start;
} else {
- c = b->dir[lo - 1].chunk;
- chunk_start = b->dir[lo - 1].start;
+ /* Binary-search the directory for the last chunk whose start is <= ofs,
+ * then walk forward (the range may span into following chunks). */
+ u32 lo = 0, hi = b->ndir;
+ while (lo < hi) {
+ u32 mid = lo + ((hi - lo) >> 1);
+ if (b->dir[mid].start <= ofs)
+ lo = mid + 1;
+ else
+ hi = mid;
+ }
+ if (lo == 0) {
+ c = b->head;
+ chunk_start = 0;
+ } else {
+ c = b->dir[lo - 1].chunk;
+ chunk_start = b->dir[lo - 1].start;
+ }
}
while (c && n) {
u32 chunk_end = chunk_start + c->used;