commit fe7b3634e9edffdaffc357413b74ccd933045dc8
parent f8c7eacf57f036647a13db18ba687a4160515635
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Sat, 13 Jun 2026 09:27:11 -0700
perf(abi): memoize c_abi_record_layout — kill the O(M×N) per-access rebuild
c_abi_record_layout rebuilt a fresh ABIRecordLayout on every member access:
arena_znew + a per-field loop calling kit_cg_type_record_field +
kit_cg_type_size. Mirror cg_id by caching the computed layout pointer on the
Type node (new const ABIRecordLayout* abi_layout, NULL = not built). The layout
is a pure function of the complete record + fixed target, immutable and
arena-lived, so sharing the pointer (read-only callers) is safe. Gated on
complete records only (abi_info_cacheable); incomplete records never cache.
Byte-identical: perf_identity_gate.sh PASS across all categories; test-cg-api
0 failures.
Diffstat:
2 files changed, 21 insertions(+), 1 deletion(-)
diff --git a/lang/c/abi/c_abi.c b/lang/c/abi/c_abi.c
@@ -84,10 +84,16 @@ u32 c_abi_alignof(KitCompiler* a, Pool* p, const Type* t) {
const ABIRecordLayout* c_abi_record_layout(KitCompiler* a, Pool* p,
const Type* t) {
- KitCgTypeId id = type_cg_id_in_pool(a, p, t);
+ KitCgTypeId id;
ABIRecordLayout* L;
ABIFieldLayout* fl = NULL;
u32 nfields;
+ /* Memoized on the Type node (mirrors cg_id). The layout is a pure function of
+ * the complete record + the fixed target, so once built it never changes; an
+ * incomplete record (no stable layout) is never cached. Collapses the former
+ * O(M member-accesses x N fields) rebuild to one build per distinct record. */
+ if (abi_info_cacheable(t) && t->abi_layout) return t->abi_layout;
+ id = type_cg_id_in_pool(a, p, t);
if (kit_cg_type_kind(a, id) != KIT_CG_TYPE_RECORD) return NULL;
nfields = kit_cg_type_record_nfields(a, id);
L = arena_znew(p->arena, ABIRecordLayout);
@@ -114,6 +120,10 @@ const ABIRecordLayout* c_abi_record_layout(KitCompiler* a, Pool* p,
L->align = kit_cg_type_align(a, id);
L->nfields = nfields;
L->fields = fl;
+ /* Cache for subsequent accesses. Only a complete record is cacheable; the
+ * RECORD-kind check above already implies the id lowered, but guard anyway so
+ * an incomplete record (which can't reach here via a RECORD id) never sticks. */
+ if (abi_info_cacheable(t)) ((Type*)t)->abi_layout = L;
return L;
}
diff --git a/lang/c/type/type.h b/lang/c/type/type.h
@@ -62,6 +62,10 @@ typedef enum TypeQual {
typedef struct Type Type;
+/* Defined in abi/c_abi.h (which includes this header), forward-declared here so
+ * Type can cache a computed record layout pointer without a header cycle. */
+struct ABIRecordLayout;
+
typedef enum FieldFlag {
FIELD_NONE = 0,
FIELD_BITFIELD = 1u << 0,
@@ -92,6 +96,12 @@ struct Type {
* set compares named fields and type_tagged_eq compares the rec/enm payload,
* so this mutable cache never participates in interning. See type_cg_lower. */
KitCgTypeId cg_id;
+ /* Memoized record layout for TY_STRUCT/TY_UNION (NULL = not yet built). Like
+ * cg_id this is a mutable cache, NOT part of type identity — the interning
+ * comparisons never read it. Only c_abi_record_layout writes it, and only for
+ * a complete record, whose layout is immutable + arena-lived for the pool's
+ * lifetime, so sharing the pointer across qualified variants is safe. */
+ const struct ABIRecordLayout* abi_layout;
union {
struct {
const Type* pointee;