kit

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

commit d953be125bc6d503b325833cce8d7cafb0e7fb47
parent 3e0e8146073711b21af37610484a9ca06dbeffb6
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 13 Jun 2026 10:10:08 -0700

perf(cg): narrow register-only NativeTarget hooks — slim the -O0 NativeLoc crossing

Diffstat:
Msrc/arch/aa64/native.c | 29+++++++++++++++++++++++++++++
Msrc/arch/native_target.h | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/arch/riscv/native.c | 29+++++++++++++++++++++++++++++
Msrc/arch/x64/native.c | 29+++++++++++++++++++++++++++++
Msrc/cg/native_direct_target.c | 49+++++++++++++++++++++++++++++++++++++++----------
5 files changed, 199 insertions(+), 10 deletions(-)

diff --git a/src/arch/aa64/native.c b/src/arch/aa64/native.c @@ -2571,6 +2571,31 @@ static void aa_convert(NativeTarget* t, ConvKind op, NativeLoc dst, } } +/* §E.3 narrow register-only entry points. The NDT crosses 16 B NativeRegLoc + * here; these reconstruct the NativeLoc the fat hook expects and delegate, so + * the emitted bytes are identical. Used only on the -O0 NDT path; the opt + * replay path calls aa_binop/aa_move/aa_cmp/aa_convert directly. */ +static void aa_binop_rr(NativeTarget* t, BinOp op, NativeRegLoc dst, + NativeRegLoc a, NativeRegLoc b) { + aa_binop(t, op, native_loc_from_reg(dst), native_loc_from_reg(a), + native_loc_from_reg(b)); +} + +static void aa_move_rr(NativeTarget* t, NativeRegLoc dst, NativeRegLoc src) { + aa_move(t, native_loc_from_reg(dst), native_loc_from_reg(src)); +} + +static void aa_cmp_rr(NativeTarget* t, CmpOp op, NativeRegLoc dst, + NativeRegLoc a, NativeRegLoc b) { + aa_cmp(t, op, native_loc_from_reg(dst), native_loc_from_reg(a), + native_loc_from_reg(b)); +} + +static void aa_convert_rr(NativeTarget* t, ConvKind op, NativeRegLoc dst, + NativeRegLoc src) { + aa_convert(t, op, native_loc_from_reg(dst), native_loc_from_reg(src)); +} + static void aa_alloca(NativeTarget* t, NativeLoc dst, NativeLoc size, u32 align) { AANativeTarget* a = aa_of(t); @@ -4114,6 +4139,10 @@ NativeTarget* aa64_native_target_new(Compiler* c, ObjBuilder* obj, t->unop = aa_unop; t->cmp = aa_cmp; t->convert = aa_convert; + t->binop_rr = aa_binop_rr; + t->move_rr = aa_move_rr; + t->cmp_rr = aa_cmp_rr; + t->convert_rr = aa_convert_rr; t->alloca_ = aa_alloca; t->spill = aa_spill; t->reload = aa_reload; diff --git a/src/arch/native_target.h b/src/arch/native_target.h @@ -289,6 +289,30 @@ typedef struct NativeLoc { } v; } NativeLoc; +/* Narrow register-only operand descriptor for the -O0 NativeDirectTarget hot + * path. NDT resolves every post-materialization binop/move/cmp/convert operand + * to a hard register (or, for an arithmetic/compare RHS, a target-legal + * immediate), then crosses into the per-arch NativeTarget. The fat 48 B + * NativeLoc carries a union wide enough for memory/global/addr forms that these + * ops never receive; passing it by value copies 48 B per operand (144 B/op) + * only for the arch to unwrap a register number + the cached width. NativeRegLoc + * carries exactly the fields the arch reads — register/immediate, class, the + * §E.2 cached width descriptor, and the type for the cold width-fallback — in + * 16 B. The narrow *_rr hooks below take it; they are byte-identical to the fat + * hooks because native_loc_from_reg reconstructs the same NativeLoc the fat path + * would have received. */ +typedef struct NativeRegLoc { + u8 cls; /* NativeAllocClass for the register/value */ + u8 szinfo; /* cached scalar-width descriptor, same encoding as NativeLoc */ + u8 is_imm; /* 1 => v.imm holds a target-legal immediate (RHS only); else reg */ + u8 pad; + KitCgTypeId type; + union { + Reg reg; + i64 imm; + } v; +} NativeRegLoc; + typedef struct NativeInst NativeInst; typedef enum NativePatchKind { @@ -522,6 +546,19 @@ struct NativeTarget { NativeLoc b_reg_or_imm); void (*convert)(NativeTarget*, ConvKind, NativeLoc dst_reg, NativeLoc src_reg); + /* Narrow register-only entry points for the -O0 NativeDirectTarget hot path + * (§E.3). Same semantics and byte output as the fat binop/move/cmp/convert, + * but the operands cross as 16 B NativeRegLoc instead of 48 B NativeLoc. NDT + * calls these when installed (always, for the native arch backends); the + * fat hooks above remain for the opt/-O1 replay path, which is unchanged. + * Optional: a backend that leaves them NULL keeps the fat path. */ + void (*binop_rr)(NativeTarget*, BinOp, NativeRegLoc dst, NativeRegLoc a, + NativeRegLoc b_reg_or_imm); + void (*move_rr)(NativeTarget*, NativeRegLoc dst, NativeRegLoc src); + void (*cmp_rr)(NativeTarget*, CmpOp, NativeRegLoc dst, NativeRegLoc a, + NativeRegLoc b_reg_or_imm); + void (*convert_rr)(NativeTarget*, ConvKind, NativeRegLoc dst, + NativeRegLoc src); void (*alloca_)(NativeTarget*, NativeLoc dst_reg, NativeLoc size_reg, u32 align); @@ -630,6 +667,42 @@ static inline int native_loc_is_fp(NativeLoc loc) { return (NativeAllocClass)loc.cls == NATIVE_REG_FP; } +/* Narrow a register-or-immediate NativeLoc to the 16 B NativeRegLoc the §E.3 + * *_rr hooks take. Only NATIVE_LOC_REG / NATIVE_LOC_IMM forms reach these hooks + * (the post-materialization contract), so this carries the register/immediate, + * class, the cached width descriptor, and the type for the cold fallback. */ +static inline NativeRegLoc native_reg_loc_of(NativeLoc loc) { + NativeRegLoc r; + r.cls = loc.cls; + r.szinfo = loc.szinfo; + r.is_imm = (u8)(loc.kind == NATIVE_LOC_IMM); + r.pad = 0; + r.type = loc.type; + if (loc.kind == NATIVE_LOC_IMM) + r.v.imm = loc.v.imm; + else + r.v.reg = loc.v.reg; + return r; +} + +/* Reconstruct the full NativeLoc a fat binop/move/cmp/convert hook would have + * received from a NativeRegLoc. Byte-identical by construction: the arch reads + * only kind / cls / szinfo / type / v.{reg,imm}, all preserved here, and the + * rest of the 48 B union is never consulted on this path. */ +static inline NativeLoc native_loc_from_reg(NativeRegLoc r) { + NativeLoc loc; + loc.kind = (u8)(r.is_imm ? NATIVE_LOC_IMM : NATIVE_LOC_REG); + loc.cls = r.cls; + loc.szinfo = r.szinfo; + loc.pad[0] = 0; + loc.type = r.type; + if (r.is_imm) + loc.v.imm = r.v.imm; + else + loc.v.reg = r.v.reg; + return loc; +} + /* NativeLoc.szinfo layout (see the field comment on NativeLoc). */ #define NATIVE_SZINFO_VALID 0x01u #define NATIVE_SZINFO_SIZE_LOG2_SHIFT 1u diff --git a/src/arch/riscv/native.c b/src/arch/riscv/native.c @@ -1359,6 +1359,31 @@ static void rv_convert(NativeTarget* t, ConvKind op, NativeLoc dst, } } +/* §E.3 narrow register-only entry points. The NDT crosses 16 B NativeRegLoc + * here; these reconstruct the NativeLoc the fat hook expects and delegate, so + * the emitted bytes are identical. Used only on the -O0 NDT path; the opt + * replay path calls rv_binop/rv_move/rv_cmp/rv_convert directly. */ +static void rv_binop_rr(NativeTarget* t, BinOp op, NativeRegLoc dst, + NativeRegLoc a, NativeRegLoc b) { + rv_binop(t, op, native_loc_from_reg(dst), native_loc_from_reg(a), + native_loc_from_reg(b)); +} + +static void rv_move_rr(NativeTarget* t, NativeRegLoc dst, NativeRegLoc src) { + rv_move(t, native_loc_from_reg(dst), native_loc_from_reg(src)); +} + +static void rv_cmp_rr(NativeTarget* t, CmpOp op, NativeRegLoc dst, + NativeRegLoc a, NativeRegLoc b) { + rv_cmp(t, op, native_loc_from_reg(dst), native_loc_from_reg(a), + native_loc_from_reg(b)); +} + +static void rv_convert_rr(NativeTarget* t, ConvKind op, NativeRegLoc dst, + NativeRegLoc src) { + rv_convert(t, op, native_loc_from_reg(dst), native_loc_from_reg(src)); +} + /* ============================ spill / reload ============================ */ static void rv_spill(NativeTarget* t, NativeLoc src, NativeFrameSlot slot, @@ -4070,6 +4095,10 @@ NativeTarget* rv64_native_target_new(Compiler* c, ObjBuilder* obj, t->unop = rv_unop; t->cmp = rv_cmp; t->convert = rv_convert; + t->binop_rr = rv_binop_rr; + t->move_rr = rv_move_rr; + t->cmp_rr = rv_cmp_rr; + t->convert_rr = rv_convert_rr; t->alloca_ = rv_alloca; t->spill = rv_spill; t->reload = rv_reload; diff --git a/src/arch/x64/native.c b/src/arch/x64/native.c @@ -1462,6 +1462,31 @@ static void x64_convert(NativeTarget* t, ConvKind k, NativeLoc dst, } } +/* §E.3 narrow register-only entry points. The NDT crosses 16 B NativeRegLoc + * here; these reconstruct the NativeLoc the fat hook expects and delegate, so + * the emitted bytes are identical. Used only on the -O0 NDT path; the opt + * replay path calls x64_binop/x64_move/x64_cmp/x64_convert directly. */ +static void x64_binop_rr(NativeTarget* t, BinOp op, NativeRegLoc dst, + NativeRegLoc a, NativeRegLoc b) { + x64_binop(t, op, native_loc_from_reg(dst), native_loc_from_reg(a), + native_loc_from_reg(b)); +} + +static void x64_move_rr(NativeTarget* t, NativeRegLoc dst, NativeRegLoc src) { + x64_move(t, native_loc_from_reg(dst), native_loc_from_reg(src)); +} + +static void x64_cmp_rr(NativeTarget* t, CmpOp op, NativeRegLoc dst, + NativeRegLoc a, NativeRegLoc b) { + x64_cmp(t, op, native_loc_from_reg(dst), native_loc_from_reg(a), + native_loc_from_reg(b)); +} + +static void x64_convert_rr(NativeTarget* t, ConvKind op, NativeRegLoc dst, + NativeRegLoc src) { + x64_convert(t, op, native_loc_from_reg(dst), native_loc_from_reg(src)); +} + /* ============================ spill / reload ============================ */ static void x64_spill(NativeTarget* t, NativeLoc src, NativeFrameSlot slot, @@ -4334,6 +4359,10 @@ NativeTarget* x64_native_target_new(Compiler* c, ObjBuilder* obj, t->unop = x64_unop; t->cmp = x64_cmp; t->convert = x64_convert; + t->binop_rr = x64_binop_rr; + t->move_rr = x64_move_rr; + t->cmp_rr = x64_cmp_rr; + t->convert_rr = x64_convert_rr; t->alloca_ = x64_alloca; t->spill = x64_spill; t->reload = x64_reload; diff --git a/src/cg/native_direct_target.c b/src/cg/native_direct_target.c @@ -846,8 +846,13 @@ static void nd_copy_to_reg(NativeDirectTarget* d, NativeLoc dst, switch ((NativeLocKind)src.kind) { case NATIVE_LOC_REG: if (src.v.reg != dst.v.reg || src.cls != dst.cls) { - ND_REQUIRE_NATIVE(d, move, "target does not emit register moves"); - d->native->move(d->native, dst, src); + if (d->native->move_rr) + d->native->move_rr(d->native, native_reg_loc_of(dst), + native_reg_loc_of(src)); + else { + ND_REQUIRE_NATIVE(d, move, "target does not emit register moves"); + d->native->move(d->native, dst, src); + } } break; case NATIVE_LOC_FRAME: @@ -1771,8 +1776,17 @@ static void nd_binop(CgTarget* t, BinOp op, Operand dst, Operand a, Operand b) { ar = nd_materialize_operand(d, a); br = nd_rhs_imm_or_reg(d, NATIVE_IMM_BINOP, (u32)op, b); dr = nd_dst_reg(d, dst); - ND_REQUIRE_NATIVE(d, binop, "target does not emit binary ops"); - d->native->binop(d->native, op, dr, ar, br); + /* §E.3: cross the post-materialization operands as 16 B NativeRegLoc when the + * backend installs the narrow hook (every native arch does); fall back to the + * fat 48 B hook otherwise. Byte-identical — the arch reconstructs the same + * NativeLoc. */ + if (d->native->binop_rr) + d->native->binop_rr(d->native, op, native_reg_loc_of(dr), + native_reg_loc_of(ar), native_reg_loc_of(br)); + else { + ND_REQUIRE_NATIVE(d, binop, "target does not emit binary ops"); + d->native->binop(d->native, op, dr, ar, br); + } nd_dst_writeback(d, dst, dr); nd_release_materialized(d, br); nd_release_materialized(d, ar); @@ -1823,8 +1837,13 @@ static void nd_cmp(CgTarget* t, CmpOp op, Operand dst, Operand a, Operand b) { ar = nd_materialize_operand(d, a); br = nd_rhs_imm_or_reg(d, NATIVE_IMM_CMP, (u32)op, b); dr = nd_dst_reg(d, dst); - ND_REQUIRE_NATIVE(d, cmp, "target does not emit compares"); - d->native->cmp(d->native, op, dr, ar, br); + if (d->native->cmp_rr) + d->native->cmp_rr(d->native, op, native_reg_loc_of(dr), + native_reg_loc_of(ar), native_reg_loc_of(br)); + else { + ND_REQUIRE_NATIVE(d, cmp, "target does not emit compares"); + d->native->cmp(d->native, op, dr, ar, br); + } nd_dst_writeback(d, dst, dr); nd_release_materialized(d, br); nd_release_materialized(d, ar); @@ -1861,16 +1880,26 @@ static void nd_convert(CgTarget* t, ConvKind op, Operand dst, Operand src) { NativeAllocClass cls = nd_class_for_type(d, dst.type); sr = nd_loc_reg(d, src.type, cls, r); dr = nd_loc_reg(d, dst.type, cls, r); - ND_REQUIRE_NATIVE(d, convert, "target does not emit converts"); - d->native->convert(d->native, op, dr, sr); + if (d->native->convert_rr) + d->native->convert_rr(d->native, op, native_reg_loc_of(dr), + native_reg_loc_of(sr)); + else { + ND_REQUIRE_NATIVE(d, convert, "target does not emit converts"); + d->native->convert(d->native, op, dr, sr); + } nd_dst_writeback(d, dst, dr); return; } } sr = nd_materialize_operand(d, src); dr = nd_dst_reg(d, dst); - ND_REQUIRE_NATIVE(d, convert, "target does not emit converts"); - d->native->convert(d->native, op, dr, sr); + if (d->native->convert_rr) + d->native->convert_rr(d->native, op, native_reg_loc_of(dr), + native_reg_loc_of(sr)); + else { + ND_REQUIRE_NATIVE(d, convert, "target does not emit converts"); + d->native->convert(d->native, op, dr, sr); + } nd_dst_writeback(d, dst, dr); nd_release_materialized(d, sr); }