commit 21fc76d188106a6891e6a316b6daffbba380f03f
parent 1812cce424a193d2044059895dde09565734344c
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Sat, 13 Jun 2026 17:46:52 -0700
refactor(cg): unify -O0 read-side materialize into nd_gv (PERF-O0-CODESIZE Lever 1, Part B)
Collapse nd_materialize_operand (materialize into any register) and
nd_materialize_operand_into (materialize into a caller-supplied register) into a
single nd_gv(d, op, want) primitive — kit's analog of tcc's gv(rc) ("generalize
a value into a register, no code if already there"). `want == NULL` is the
generic-class case (returns a freshly pinned scratch or the live cache reg);
`want != NULL` targets a specific already-pinned register, copying/loading the
value in. Both share one residence-resolution path (cache hit, or width-mismatch
flush + load from memory), so there is now a single soundness argument for the
-O0 read side. The two old names remain as thin wrappers.
want is a `const NativeLoc*` (not a bare Reg + REG_ANY sentinel): the into-case
caller (nd_copy) hands a destination whose type/width can differ from the
operand's, and that destination type drives nd_copy_to_reg — so the full loc
must be carried, and NULL cleanly denotes "any register" with no new sentinel.
Pure refactor: branches map 1:1 to the originals. sqlite3.c -c is byte-identical
to the prior commit; test-toy 1392/0, test-smoke-x64/-rv64 3/0.
Diffstat:
1 file changed, 40 insertions(+), 23 deletions(-)
diff --git a/src/cg/native_direct_target.c b/src/cg/native_direct_target.c
@@ -955,44 +955,61 @@ static void nd_flush_operand_addr_locals(NativeDirectTarget* d, Operand op) {
if (op.v.ind.index != CG_LOCAL_NONE) nd_flush_local(d, op.v.ind.index);
}
-static NativeLoc nd_materialize_operand(NativeDirectTarget* d, Operand op) {
+/* Materialize OP's value into a register and return it — the tcc gv(rc) analog:
+ * "generalize this value into a register, doing nothing if it is already there."
+ *
+ * want == NULL : ANY register of OP's class (gv with a generic class). A cache
+ * hit returns the live register pinned, with no code; otherwise
+ * a fresh scratch is acquired and OP loaded into it. The caller
+ * releases the pin with nd_release_materialized.
+ * want != NULL : that SPECIFIC, already-acquired+pinned register (gv into a
+ * fixed reg). A cache-resident OP costs the one unavoidable
+ * reg-reg move; a memory / immediate / global / indirect OP is
+ * loaded straight into *want (no scratch round-trip). *want
+ * carries the destination type/width the copy uses, which may
+ * differ from OP's (e.g. a narrowing copy), so it — not OP —
+ * drives nd_copy_to_reg. Returns *want.
+ *
+ * Both modes share one residence resolution (cache hit, or width-mismatch flush
+ * then memory), so there is a single soundness argument for getting a value into
+ * a register at -O0. */
+static NativeLoc nd_gv(NativeDirectTarget* d, Operand op, const NativeLoc* want) {
NativeAllocClass cls = nd_class_for_type(d, op.type);
if (op.kind == OPK_LOCAL) {
NativeDirectLocal* l = nd_local(d, op.v.local);
if (l->reg != REG_NONE && op.type == l->type && nd_local_cacheable(d, l)) {
- /* Cache hit: pin and reuse the live register, no reload. */
- d->scratch_used[l->cls] |= 1u << l->reg;
nd_touch_local(d, l);
- return nd_loc_reg(d, op.type, (NativeAllocClass)l->cls, l->reg);
+ if (!want) {
+ /* Cache hit: pin and reuse the live register, no reload. */
+ d->scratch_used[l->cls] |= 1u << l->reg;
+ return nd_loc_reg(d, op.type, (NativeAllocClass)l->cls, l->reg);
+ }
+ nd_copy_to_reg(d, *want,
+ nd_loc_reg(d, op.type, (NativeAllocClass)l->cls, l->reg));
+ return *want;
}
/* A live entry under a different access width must reach memory before we
* bypass the cache for this access. */
if (l->reg != REG_NONE) nd_flush_local(d, op.v.local);
}
nd_flush_operand_addr_locals(d, op);
- return nd_materialize_loc(d, nd_loc_operand(d, op), cls, op.type);
+ if (!want)
+ return nd_materialize_loc(d, nd_loc_operand(d, op), cls, op.type);
+ nd_copy_to_reg(d, *want, nd_loc_operand(d, op));
+ return *want;
}
-/* Materialize OP straight into the already-acquired register WANT, skipping the
- * scratch round-trip nd_materialize_operand + a separate move would make for a
- * memory / immediate / global / indirect source: nd_copy_to_reg loads each of
- * those forms directly into its destination. A cache-resident local still costs
- * one reg-reg move (its value lives in another register); everything else lands
- * in WANT with a single load. WANT must already be pinned by the caller. */
+static NativeLoc nd_materialize_operand(NativeDirectTarget* d, Operand op) {
+ return nd_gv(d, op, NULL);
+}
+
+/* Materialize OP straight into the already-acquired register WANT (gv into a
+ * fixed reg), skipping the scratch round-trip + separate move a memory /
+ * immediate / global / indirect source would otherwise make. WANT must already
+ * be pinned by the caller. */
static void nd_materialize_operand_into(NativeDirectTarget* d, NativeLoc want,
Operand op) {
- if (op.kind == OPK_LOCAL) {
- NativeDirectLocal* l = nd_local(d, op.v.local);
- if (l->reg != REG_NONE && op.type == l->type && nd_local_cacheable(d, l)) {
- nd_touch_local(d, l);
- nd_copy_to_reg(d, want,
- nd_loc_reg(d, op.type, (NativeAllocClass)l->cls, l->reg));
- return;
- }
- if (l->reg != REG_NONE) nd_flush_local(d, op.v.local);
- }
- nd_flush_operand_addr_locals(d, op);
- nd_copy_to_reg(d, want, nd_loc_operand(d, op));
+ nd_gv(d, op, &want);
}
static NativeLoc nd_dst_scratch(NativeDirectTarget* d, Operand dst) {