kit

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

commit 6cc5c5774c44412d402a94214e98a72bc8533eaa
parent 834999ce5a1086ff57a599be687e3c10fa619e2e
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue, 16 Jun 2026 09:19:39 -0700

Speed up O1 optimizer hot paths

Diffstat:
Mdoc/plan/PERF.md | 27++++++++++++++++++++++-----
Msrc/core/metrics.h | 3+++
Msrc/opt/cg_ir_lower.c | 89+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Msrc/opt/pass_loop.c | 75+++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Msrc/opt/pass_lower.c | 72+++++++++++++++++++++++++++++++++++++++++++-----------------------------
5 files changed, 165 insertions(+), 101 deletions(-)

diff --git a/doc/plan/PERF.md b/doc/plan/PERF.md @@ -285,11 +285,28 @@ bitset/range helpers), while CGIR lowering plus record construction is another **26 %**. Built-in metrics on the same TU report **2,522 functions**, **121,075 blocks**, **98,516 PRegs**, **1,049,079 ranges**, and **29.1 M live bitset words touched**, which explains why the liveness/range bitset helpers are -visible. A cheap-looking cleanup also shows up: `metrics_scope_from_name` and -`metrics_count` burn ~2.7 % of O1 finalization even with `KIT_METRICS` unset, -because each hot scope/counter call string-decodes the name before discovering -the profiler sink is null. The O1 hot path should use enum-specific wrappers or -check the profiler before name decoding. +visible. A cheap cleanup from this profile was the disabled metrics fast path: +`metrics_scope_from_name` and `metrics_count` burned ~2.7 % of O1 finalization +with `KIT_METRICS` unset because each hot scope/counter call string-decoded the +name before discovering the profiler sink was null. `metrics_*` now checks the +sink before name decoding; enum-specific wrappers would still make metrics-on +collection cheaper. + +Follow-up cleanup on the same sqlite `-O1` workload made the hot shapes cheaper: +CGIR lowering precomputes local address-use, param lookup, emit order, and +fallthrough successors; regalloc builds coalesce-group member lists before +candidate scoring and uses them in group scans; loop-tree construction +precomputes backedges and uses generation-marked loop bodies. Bounded macOS +release timing for the single amalgamation compile is now **0.93 s best / 0.96 s +mean** (7 runs, 5 s per-run timeout); the ecosystem-style two-TU sqlite O1 build +is **1.20 s best / 1.22 s mean** (5 runs, 8 s timeout), down from the previous +**2.78 s** best row. `KIT_METRICS=1` ticks on the same single-amalgamation +command: `opt.o1.total` **27.09 M → 16.78 M** (−38 %), `opt.regalloc` +**13.81 M → 7.83 M** (−43 %), `opt.o1.cg_ir_lower` **4.94 M → 1.16 M** (−77 %), +and `opt.build_loop_tree` **1.41 M → 0.20 M** (−86 %). Linux callgrind rerun was +blocked in this pass because `podman machine` started then immediately stopped +at 3 GiB, 2.5 GiB, and the restored 2 GiB setting; the VM was left configured at +2 GiB/stopped. ### Code size is still locally spill-bound diff --git a/src/core/metrics.h b/src/core/metrics.h @@ -220,16 +220,19 @@ static inline KitProfileCounter metrics_counter_from_name(const char* name) { } static inline void metrics_scope_begin(Compiler* c, const char* name) { + if (!profile_sink(c)) return; KitProfileScope scope = metrics_scope_from_name(name); if (scope != KIT_PROFILE_SCOPE_NONE) profile_scope_begin(c, scope); } static inline void metrics_scope_end(Compiler* c, const char* name) { + if (!profile_sink(c)) return; KitProfileScope scope = metrics_scope_from_name(name); if (scope != KIT_PROFILE_SCOPE_NONE) profile_scope_end(c, scope); } static inline void metrics_count(Compiler* c, const char* name, u64 value) { + if (!profile_sink(c)) return; KitProfileCounter counter = metrics_counter_from_name(name); if (counter != KIT_PROFILE_COUNTER_NONE) profile_count(c, counter, value); } diff --git a/src/opt/cg_ir_lower.c b/src/opt/cg_ir_lower.c @@ -32,9 +32,12 @@ typedef struct CgIrLower { Func* f; OptLocalMap* locals; u32 nlocals; + u8* local_addr_used; + const CgIrParam** param_by_local; u32* label_block; u32 nlabels; u32* inst_block; + u32* fallthrough_by_block; u8* leader; CGLocal mat_local[CG_IR_LOWER_MAX_MAT]; u8 mat_role[CG_IR_LOWER_MAX_MAT]; @@ -112,12 +115,6 @@ static int local_needs_home(const CgIrLocal* in) { (in->desc.flags & (CG_LOCAL_ADDR_TAKEN | CG_LOCAL_MEMORY_REQUIRED)); } -static int operand_uses_local_addr(const Operand* op, CGLocal local) { - if (!op) return 0; - if (op->kind == OPK_LOCAL) return op->v.local == local; - return 0; -} - /* AGG_COPY/AGG_SET take their dest/src as *pointer values* to the aggregate — * the emitter derefs an OPK_LOCAL pointer operand via pointer_addr_from_operand * (it loads the pointer; it does not address the local's own slot). So a @@ -125,43 +122,44 @@ static int operand_uses_local_addr(const Operand* op, CGLocal local) { * its address, and must not force the local to a frame home. Only a non-pointer * operand (the aggregate-typed local itself) genuinely addresses its storage. * (STORE/LOAD/ADDR_OF use addr_from_operand, where an OPK_LOCAL always - * addresses the slot, so they keep operand_uses_local_addr.) */ -static int operand_uses_local_agg_addr(Compiler* c, const Operand* op, - CGLocal local) { - if (!op || op->kind != OPK_LOCAL || op->v.local != local) return 0; - return !cg_type_is_ptr(c, op->type); + * addresses the slot, so they keep the direct-address marking below.) */ +static void note_local_addr_use(CgIrLower* l, const Operand* op) { + if (!op || op->kind != OPK_LOCAL) return; + CGLocal local = op->v.local; + if (local == CG_LOCAL_NONE || local > l->nlocals) return; + l->local_addr_used[local] = 1; } -static int local_address_used_in_cg_ir(Compiler* c, const CgIrFunc* f, - CGLocal local) { +static void note_local_agg_addr_use(CgIrLower* l, const Operand* op) { + if (!op || op->kind != OPK_LOCAL || cg_type_is_ptr(l->c, op->type)) return; + CGLocal local = op->v.local; + if (local == CG_LOCAL_NONE || local > l->nlocals) return; + l->local_addr_used[local] = 1; +} + +static void mark_local_address_uses(CgIrLower* l) { + const CgIrFunc* f = l->src; + l->local_addr_used = arena_zarray(l->f->arena, u8, l->nlocals + 1u); for (u32 i = 0; i < f->ninsts; ++i) { const CgIrInst* in = &f->insts[i]; switch ((CgIrOp)in->op) { case CG_IR_LOAD: case CG_IR_BITFIELD_LOAD: - if (in->nopnds > 1u && operand_uses_local_addr(&in->opnds[1], local)) - return 1; + if (in->nopnds > 1u) note_local_addr_use(l, &in->opnds[1]); break; case CG_IR_STORE: case CG_IR_BITFIELD_STORE: - if (in->nopnds > 0u && operand_uses_local_addr(&in->opnds[0], local)) - return 1; + if (in->nopnds > 0u) note_local_addr_use(l, &in->opnds[0]); break; case CG_IR_AGG_SET: - if (in->nopnds > 0u && - operand_uses_local_agg_addr(c, &in->opnds[0], local)) - return 1; + if (in->nopnds > 0u) note_local_agg_addr_use(l, &in->opnds[0]); break; case CG_IR_ADDR_OF: - if (in->nopnds > 1u && operand_uses_local_addr(&in->opnds[1], local)) - return 1; + if (in->nopnds > 1u) note_local_addr_use(l, &in->opnds[1]); break; case CG_IR_AGG_COPY: - if ((in->nopnds > 0u && - operand_uses_local_agg_addr(c, &in->opnds[0], local)) || - (in->nopnds > 1u && - operand_uses_local_agg_addr(c, &in->opnds[1], local))) - return 1; + if (in->nopnds > 0u) note_local_agg_addr_use(l, &in->opnds[0]); + if (in->nopnds > 1u) note_local_agg_addr_use(l, &in->opnds[1]); break; /* VA_START/VA_ARG/VA_END/VA_COPY consume a pointer *value* (the address * of the va_list, produced by an earlier ADDR_OF); they do not take the @@ -171,13 +169,13 @@ static int local_address_used_in_cg_ir(Compiler* c, const CgIrFunc* f, break; } } - return 0; } static void lower_locals(CgIrLower* l) { l->nlocals = l->src->nlocals; l->locals = arena_zarray(l->f->arena, OptLocalMap, l->nlocals ? l->nlocals : 1u); + mark_local_address_uses(l); for (u32 i = 0; i < l->src->nlocals; ++i) { const CgIrLocal* in = &l->src->locals[i]; OptLocalMap* m; @@ -196,7 +194,7 @@ static void lower_locals(CgIrLower* l) { */ m->address_taken = local_needs_home(in) || - local_address_used_in_cg_ir(l->c, l->src, in->id) || + (in->id <= l->nlocals && l->local_addr_used[in->id]) || cg_type_is_aggregate(l->c, in->desc.type) || cg_type_size(l->c, in->desc.type) > (u64)l->c->target.ptr_size; @@ -222,10 +220,14 @@ static void lower_locals(CgIrLower* l) { } } -static const CgIrParam* find_param(const CgIrFunc* f, CGLocal local) { - for (u32 i = 0; i < f->nparams; ++i) - if (f->params[i].local == local) return &f->params[i]; - return NULL; +static void build_param_by_local(CgIrLower* l) { + l->param_by_local = + arena_zarray(l->f->arena, const CgIrParam*, l->nlocals + 1u); + for (u32 i = 0; i < l->src->nparams; ++i) { + const CgIrParam* p = &l->src->params[i]; + if (p->local == CG_LOCAL_NONE || p->local > l->nlocals) continue; + l->param_by_local[p->local] = p; + } } static void lower_params(CgIrLower* l) { @@ -239,10 +241,12 @@ static void lower_params(CgIrLower* l) { const ABIFuncInfo* fi = NULL; if (l->c && l->c->abi && l->f->desc.fn_type) fi = abi_cg_func_info(l->c->abi, l->f->desc.fn_type); + build_param_by_local(l); for (u32 i = 0; i < l->src->nlocals; ++i) { const CgIrLocal* loc = &l->src->locals[i]; if (!loc->is_param) continue; - const CgIrParam* p = find_param(l->src, loc->id); + const CgIrParam* p = + (loc->id <= l->nlocals) ? l->param_by_local[loc->id] : NULL; OptLocalMap* m = local_map(l, loc->id, loc->desc.loc); OptCGParamDesc d; memset(&d, 0, sizeof d); @@ -390,9 +394,6 @@ static void make_blocks(CgIrLower* l, const u32* label_place) { l->f->entry = ir_block_new(l->f); ir_note_emit(l->f, l->f->entry); } - l->f->emit_order_n = 0; - for (u32 i = 0; i < f->ninsts; ++i) ir_note_emit(l->f, l->inst_block[i]); - if (!f->ninsts) ir_note_emit(l->f, l->f->entry); } static void emit_param_decls(CgIrLower* l) { @@ -1260,6 +1261,13 @@ static void lower_one_inst(CgIrLower* l, u32 idx) { } static void add_fallthrough_succs(CgIrLower* l) { + l->fallthrough_by_block = + arena_array(l->f->arena, u32, l->f->nblocks ? l->f->nblocks : 1u); + for (u32 b = 0; b < l->f->nblocks; ++b) l->fallthrough_by_block[b] = UINT32_MAX; + for (u32 i = 0; i + 1u < l->f->emit_order_n; ++i) { + u32 b = l->f->emit_order[i]; + if (b < l->f->nblocks) l->fallthrough_by_block[b] = l->f->emit_order[i + 1u]; + } for (u32 b = 0; b < l->f->nblocks; ++b) { Block* bl = &l->f->blocks[b]; if (bl->nsucc) continue; @@ -1286,12 +1294,7 @@ static void add_fallthrough_succs(CgIrLower* l) { break; } } - for (u32 i = 0; i + 1u < l->f->emit_order_n; ++i) { - if (l->f->emit_order[i] == b) { - set_succ1(l, b, l->f->emit_order[i + 1u]); - break; - } - } + set_succ1(l, b, l->fallthrough_by_block[b]); } } diff --git a/src/opt/pass_loop.c b/src/opt/pass_loop.c @@ -3,12 +3,20 @@ #include "core/arena.h" #include "opt/opt_internal.h" +static void loop_body_mark(u32* mark, u32 gen, u32* body_list, u32* nbody, + u32 b) { + if (mark[b] == gen) return; + mark[b] = gen; + body_list[(*nbody)++] = b; +} + static void loop_mark_body(Func* f, const u8* visited, u32 header, u32 latch, - u8* body, u32* stack) { + u32* mark, u32 gen, u32* body_list, u32* nbody, + u32* stack) { u32 sp = 0; - if (!body[header]) body[header] = 1; - if (!body[latch]) { - body[latch] = 1; + loop_body_mark(mark, gen, body_list, nbody, header); + if (mark[latch] != gen) { + loop_body_mark(mark, gen, body_list, nbody, latch); stack[sp++] = latch; } @@ -18,8 +26,8 @@ static void loop_mark_body(Func* f, const u8* visited, u32 header, u32 latch, Block* bl = &f->blocks[b]; for (u32 p = 0; p < bl->npreds; ++p) { u32 pred = bl->preds[p]; - if (pred >= f->nblocks || !visited[pred] || body[pred]) continue; - body[pred] = 1; + if (pred >= f->nblocks || !visited[pred] || mark[pred] == gen) continue; + loop_body_mark(mark, gen, body_list, nbody, pred); stack[sp++] = pred; } } @@ -50,28 +58,47 @@ void opt_build_loop_tree(Func* f) { return; } - u8* body = arena_zarray(f->arena, u8, f->nblocks); + u32* first_edge = arena_array(f->arena, u32, f->nblocks); + for (u32 b = 0; b < f->nblocks; ++b) first_edge[b] = UINT32_MAX; + u32 max_edges = 0; + for (u32 b = 0; b < f->nblocks; ++b) + if (a.reachable[b]) max_edges += f->blocks[b].nsucc; + u32* edge_latch = arena_array(f->arena, u32, max_edges ? max_edges : 1u); + u32* edge_next = arena_array(f->arena, u32, max_edges ? max_edges : 1u); + u32 nedges = 0; + + for (u32 latch = 0; latch < f->nblocks; ++latch) { + if (!a.reachable[latch]) continue; + Block* lb = &f->blocks[latch]; + for (u32 s = 0; s < lb->nsucc; ++s) { + u32 header = lb->succ[s]; + if (header >= f->nblocks || !a.reachable[header]) continue; + if (!opt_analysis_dominates(&a, header, latch)) continue; + edge_latch[nedges] = latch; + edge_next[nedges] = first_edge[header]; + first_edge[header] = nedges++; + } + } + + u32* mark = arena_zarray(f->arena, u32, f->nblocks); + u32* body_list = arena_array(f->arena, u32, f->nblocks); u32* stack = arena_array(f->arena, u32, f->nblocks); + u32 gen = 1; for (u32 header = 0; header < f->nblocks; ++header) { - if (!a.reachable[header]) continue; - memset(body, 0, f->nblocks * sizeof body[0]); - int has_loop = 0; - - for (u32 latch = 0; latch < f->nblocks; ++latch) { - if (!a.reachable[latch]) continue; - Block* lb = &f->blocks[latch]; - for (u32 s = 0; s < lb->nsucc; ++s) { - if (lb->succ[s] != header) continue; - if (!opt_analysis_dominates(&a, header, latch)) continue; - has_loop = 1; - loop_mark_body(f, a.reachable, header, latch, body, stack); - } + if (first_edge[header] == UINT32_MAX) continue; + u32 nbody = 0; + for (u32 e = first_edge[header]; e != UINT32_MAX; e = edge_next[e]) + loop_mark_body(f, a.reachable, header, edge_latch[e], mark, gen, + body_list, &nbody, stack); + for (u32 i = 0; i < nbody; ++i) { + u32 b = body_list[i]; + if (f->blocks[b].loop_depth < 31) ++f->blocks[b].loop_depth; + } + if (++gen == 0) { + memset(mark, 0, f->nblocks * sizeof mark[0]); + gen = 1; } - - if (!has_loop) continue; - for (u32 b = 0; b < f->nblocks; ++b) - if (body[b] && f->blocks[b].loop_depth < 31) ++f->blocks[b].loop_depth; } for (u32 b = 0; b < f->nblocks; ++b) diff --git a/src/opt/pass_lower.c b/src/opt/pass_lower.c @@ -780,16 +780,42 @@ static int alloc_group_member(Func* f, PReg root, PReg v) { return alloc_coalesce_root(f, v) == root; } -static void alloc_group_info(Func* f, const OptLiveRangeSet* ranges, PReg root, +static void alloc_build_member_lists(Func* f, OptAllocator* a, + const OptLiveRangeSet* ranges) { + if (!f->opt_coalesce_parent) return; + u32 nr = opt_reg_count(f); + a->member_head = arena_zarray(f->arena, u32, nr ? nr : 1u); + a->member_next = arena_zarray(f->arena, u32, nr ? nr : 1u); + for (PReg v = 1; v < nr; ++v) { + if (ranges->first_range_by_preg[v] == OPT_RANGE_NONE) continue; + PReg root = alloc_coalesce_root(f, v); + a->member_next[v] = a->member_head[root]; + a->member_head[root] = v; + } +} + +static PReg alloc_group_first_member(const OptAllocator* a, PReg root) { + return a->member_head ? (PReg)a->member_head[root] : root; +} + +static PReg alloc_group_next_member(const OptAllocator* a, PReg root, + PReg member) { + (void)root; + if (!a->member_head) return 0; + return (PReg)a->member_next[member]; +} + +static void alloc_group_info(Func* f, const OptAllocator* a, + const OptLiveRangeSet* ranges, PReg root, OptAllocGroupInfo* out) { memset(out, 0, sizeof *out); out->root = root; out->first = (u32)~0u; out->tied_hard_reg = -1; out->cls = f->preg_info[root].cls; - for (PReg v = 1; v < opt_reg_count(f); ++v) { + for (PReg v = alloc_group_first_member(a, root); v != 0; + v = alloc_group_next_member(a, root, v)) { if (ranges->first_range_by_preg[v] == OPT_RANGE_NONE) continue; - if (!alloc_group_member(f, root, v)) continue; OptPRegInfo* vi = &f->preg_info[v]; out->spill_cost += vi->frequency ? vi->frequency : vi->spill_cost; out->live_length += vi->live_length; @@ -952,10 +978,11 @@ static int spill_slot_compatible(Func* f, FrameSlot fs, PReg v) { static void alloc_compute_group_conflicts(Func* f, OptAllocator* a, const OptLiveRangeSet* ranges, PReg root) { + (void)f; for (u32 w = 0; w < a->loc_words; ++w) a->conflict_locs[w] = 0; - for (PReg v = 1; v < opt_reg_count(f); ++v) { + for (PReg v = alloc_group_first_member(a, root); v != 0; + v = alloc_group_next_member(a, root, v)) { if (ranges->first_range_by_preg[v] == OPT_RANGE_NONE) continue; - if (!alloc_group_member(f, root, v)) continue; for (u32 ri = ranges->first_range_by_preg[v]; ri != OPT_RANGE_NONE; ri = ranges->ranges[ri].next) { const OptLiveRange* lr = &ranges->ranges[ri]; @@ -977,11 +1004,12 @@ static void alloc_compute_group_conflicts(Func* f, OptAllocator* a, static void alloc_mark_group_loc(Func* f, OptAllocator* a, const OptLiveRangeSet* ranges, PReg root, u32 loc_bit) { + (void)f; u32 w = loc_bit / 64u; u64 mask = 1ull << (loc_bit % 64u); - for (PReg v = 1; v < opt_reg_count(f); ++v) { + for (PReg v = alloc_group_first_member(a, root); v != 0; + v = alloc_group_next_member(a, root, v)) { if (ranges->first_range_by_preg[v] == OPT_RANGE_NONE) continue; - if (!alloc_group_member(f, root, v)) continue; for (u32 ri = ranges->first_range_by_preg[v]; ri != OPT_RANGE_NONE; ri = ranges->ranges[ri].next) { const OptLiveRange* lr = &ranges->ranges[ri]; @@ -999,9 +1027,9 @@ static void alloc_assign_group_hard(Func* f, OptAllocator* a, Reg r) { u8 cls = f->preg_info[root].cls; u32 bit = hard_loc_bit(cls, r); - for (PReg v = 1; v < opt_reg_count(f); ++v) { + for (PReg v = alloc_group_first_member(a, root); v != 0; + v = alloc_group_next_member(a, root, v)) { if (ranges->first_range_by_preg[v] == OPT_RANGE_NONE) continue; - if (!alloc_group_member(f, root, v)) continue; OptPRegInfo* vi = &f->preg_info[v]; vi->alloc_kind = OPT_ALLOC_HARD; vi->hard_reg = r; @@ -1040,9 +1068,9 @@ static void alloc_assign_group_stack(Func* f, OptAllocator* a, * groups sums all of their traffic — so the emitter's hot-slot-low ordering * sees the slot's total demand, not just the first group that created it. */ spill_slot_add_priority(f, slot, group_priority); - for (PReg v = 1; v < opt_reg_count(f); ++v) { + for (PReg v = alloc_group_first_member(a, root); v != 0; + v = alloc_group_next_member(a, root, v)) { if (ranges->first_range_by_preg[v] == OPT_RANGE_NONE) continue; - if (!alloc_group_member(f, root, v)) continue; OptPRegInfo* vi = &f->preg_info[v]; vi->spill_slot = slot; vi->alloc_kind = OPT_ALLOC_SPILL; @@ -1054,9 +1082,9 @@ static void alloc_assign_group_stack(Func* f, OptAllocator* a, u32 bit = a->hard_loc_bits + stack_idx; u32 w = bit / 64u; u64 mask = 1ull << (bit % 64u); - for (PReg v = 1; v < opt_reg_count(f); ++v) { + for (PReg v = alloc_group_first_member(a, root); v != 0; + v = alloc_group_next_member(a, root, v)) { if (ranges->first_range_by_preg[v] == OPT_RANGE_NONE) continue; - if (!alloc_group_member(f, root, v)) continue; for (u32 ri = ranges->first_range_by_preg[v]; ri != OPT_RANGE_NONE; ri = ranges->ranges[ri].next) { const OptLiveRange* lr = &ranges->ranges[ri]; @@ -1089,6 +1117,7 @@ static void opt_assign_ranges(Func* f, const OptLiveRangeSet* ranges, a->stack_slots = NULL; a->stack_slot_count = 0; a->stack_slot_cap = 0; + alloc_build_member_lists(f, a, ranges); /* Build candidate list: every coalesce-root PReg that has live ranges. */ u32 ncands = 0; @@ -1105,26 +1134,11 @@ static void opt_assign_ranges(Func* f, const OptLiveRangeSet* ranges, PReg root = alloc_coalesce_root(f, v); if (root != v) continue; cands[n].v = v; - alloc_group_info(f, ranges, root, &cands[n].gi); + alloc_group_info(f, a, ranges, root, &cands[n].gi); ++n; } alloc_sort_candidates(cands, n); - /* Build per-root member lists once (only meaningful with W3 coalescing). The - * hint-path precise interference check uses these to enumerate a candidate - * group's members cheaply instead of rescanning all PRegs per probe. */ - if (f->opt_coalesce_parent) { - u32 nr = opt_reg_count(f); - a->member_head = arena_zarray(f->arena, u32, nr ? nr : 1u); - a->member_next = arena_zarray(f->arena, u32, nr ? nr : 1u); - for (PReg v = 1; v < nr; ++v) { - if (ranges->first_range_by_preg[v] == OPT_RANGE_NONE) continue; - PReg root = alloc_coalesce_root(f, v); - a->member_next[v] = a->member_head[root]; - a->member_head[root] = v; - } - } - for (u32 i = 0; i < n; ++i) { PReg v = cands[i].v; OptAllocGroupInfo gi = cands[i].gi;