ffi 1.5: intern Obj-C selectors — one static SEL slot per unique name
101/101 regression tests pass; the IR snapshot for the selector-
sharing test diff flips from four per-call `sel_registerName` calls
to two (one per unique selector) routed through a module-init
constructor — matching what clang emits for `@selector(...)`.
Hot-path cost collapses from a libobjc hashtable lookup per call to
a single load of a static `SEL*` slot:
Before (Phase 1.3):
%sel = call ptr @sel_registerName(<"init">)
call ptr @objc_msgSend(<recv>, %sel)
After (Phase 1.5):
%sel = load ptr, ptr @OBJC_SELECTOR_REFERENCES_init
call ptr @objc_msgSend(<recv>, %sel)
+ @OBJC_SELECTOR_REFERENCES_init = internal global ptr null
+ @OBJC_SELECTOR_REFERENCES_release = internal global ptr null
+ define internal void @__sx_objc_selector_init() {
+ %sel = call ptr @sel_registerName(ptr @OBJC_METH_VAR_NAME_)
+ store ptr %sel, ptr @OBJC_SELECTOR_REFERENCES_init
+ %sel1 = call ptr @sel_registerName(ptr @OBJC_METH_VAR_NAME_.2)
+ store ptr %sel1, ptr @OBJC_SELECTOR_REFERENCES_release
+ ret void
+ }
+ @llvm.global_ctors = appending global [1 x { i32, ptr, ptr }]
+ [{ ..., ptr @__sx_objc_selector_init, ptr null }]
Implementation:
module.zig | new `objc_selector_cache: ArrayList(ObjcSelectorEntry)`
with `lookupObjcSelector` / `appendObjcSelector`. List
(not hashmap) keeps emit order stable across builds so
the IR snapshot doesn't flicker on rehash.
lower.zig | `internObjcSelector(sel)` creates the slot on first
use, returns the same `GlobalId` on every subsequent
call to the same selector. lowerFfiIntrinsicCall now
emits `global_addr + load` for literal selectors.
Non-literal selectors keep the `sel_registerName`
fallback. Declaring `sel_registerName` lazily on
first intern so emit_llvm finds it for the
constructor body.
emit_llvm.zig | new `emitObjcSelectorInit` pass synthesizes a void
constructor that loops over the cache, calls
`sel_registerName` for each unique selector string,
stores the result in the slot. Constructor is
registered in `@llvm.global_ctors` with default
priority (65535) so dyld runs it before main.
The `@OBJC_METH_VAR_NAME_` private string globals and unnamed-addr
flag match clang's exact emission shape — picked up by the system
linker into the right Mach-O sections on macOS / iOS. Chess
Android + iOS-sim still build clean (no `#objc_call` in chess yet —
phase-3 migration will start exercising this).
This commit is contained in:
@@ -26,14 +26,24 @@ pub const Module = struct {
|
||||
globals: std.ArrayList(Global),
|
||||
/// Maps (protocol_ty, concrete_ty) → list of method FuncIds.
|
||||
impl_table: ImplTable,
|
||||
/// Interned Obj-C selectors. Kept as an insertion-ordered list of
|
||||
/// (selector_string, slot_GlobalId) so emit_llvm.zig produces the
|
||||
/// init constructor in a stable order across builds (the
|
||||
/// selector-sharing IR snapshot would otherwise flicker on
|
||||
/// hashtable rehash). `#objc_call` lowering uses
|
||||
/// `lookupObjcSelector` / `appendObjcSelector` to read/write it.
|
||||
objc_selector_cache: std.ArrayList(ObjcSelectorEntry),
|
||||
alloc: Allocator,
|
||||
|
||||
pub const ObjcSelectorEntry = struct { sel: []const u8, slot: GlobalId };
|
||||
|
||||
pub fn init(alloc: Allocator) Module {
|
||||
return .{
|
||||
.types = TypeTable.init(alloc),
|
||||
.functions = std.ArrayList(Function).empty,
|
||||
.globals = std.ArrayList(Global).empty,
|
||||
.impl_table = ImplTable.init(alloc),
|
||||
.objc_selector_cache = std.ArrayList(ObjcSelectorEntry).empty,
|
||||
.alloc = alloc,
|
||||
};
|
||||
}
|
||||
@@ -45,9 +55,24 @@ pub const Module = struct {
|
||||
self.functions.deinit(self.alloc);
|
||||
self.globals.deinit(self.alloc);
|
||||
self.impl_table.deinit();
|
||||
self.objc_selector_cache.deinit(self.alloc);
|
||||
self.types.deinit();
|
||||
}
|
||||
|
||||
/// Linear scan — N is the count of UNIQUE selectors per program,
|
||||
/// not the count of call sites. Real programs hit dozens, not
|
||||
/// millions; a hashmap would be premature here.
|
||||
pub fn lookupObjcSelector(self: *const Module, sel: []const u8) ?GlobalId {
|
||||
for (self.objc_selector_cache.items) |entry| {
|
||||
if (std.mem.eql(u8, entry.sel, sel)) return entry.slot;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn appendObjcSelector(self: *Module, sel: []const u8, slot: GlobalId) void {
|
||||
self.objc_selector_cache.append(self.alloc, .{ .sel = sel, .slot = slot }) catch unreachable;
|
||||
}
|
||||
|
||||
pub fn addFunction(self: *Module, func: Function) FuncId {
|
||||
const id = FuncId.fromIndex(@intCast(self.functions.items.len));
|
||||
self.functions.append(self.alloc, func) catch unreachable;
|
||||
|
||||
Reference in New Issue
Block a user