authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-12-06 10:05:23+00:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-12-06 10:41:42+00:00
log65922a2d4319ca2e9f3e67650d018e269d018a77
tree41aa53f75c76a514500ac1bdea96c267487ac41d
parent621e1d7b1e560901e2a5cbdca39f173f6592a004
signaturelock-open Commit is signed but in an unrecognized format.

std: make stack unwinding faster on macOS

https://github.com/ziglang/zig/issues/26027#issuecomment-3571227050 tracked some bad performance in `DebugAllocator` on macOS down to a function in dyld which `std.debug.SelfInfo` was calling into. It turns out `dladdr`'s symbol lookup logic is horrendously slow (looking at its source code, it appears to be doing a *linear scan* over all symbols in the image?!). However, we don't actually need the symbol, so we want to try and avoid this logic. Luckily, dyld has more precise APIs for what we need! Unluckily, Apple, in their infinite wisdom, decided they should be deprecated in favour of `dladdr`, despite the latter being several times slower (and by "several times", I have measured a 50x slowdown on repeated calls to `dladdr` compared to the other API). But luckily again, the deprecated APIs are still exposed. So, after a careful analysis of the situation (reading dyld code and cursing Apple engineers), I think it makes sense to just use these deprecated APIs for now. If they ever go away, we can write our own cache for this data to bypass Apple's awfully slow code, but I suspect these functions will stick around for the foreseeable future. Uh, and if `_dyld_get_image_header_containing_address` goes away, there's also `dyld_image_header_containing_address`, which is a seemingly identical function, exported by dyld just the same, but with a separate (functionally identical) implementation, and not documented in the public header file. Apple work in mysterious ways, I guess.

3 files changed, 33 insertions(+), 21 deletions(-)

lib/std/c.zig+2
......@@ -11324,6 +11324,8 @@ pub const _dyld_get_image_header = darwin._dyld_get_image_header;
1132411324pub const _dyld_get_image_name = darwin._dyld_get_image_name;
1132511325pub const _dyld_get_image_vmaddr_slide = darwin._dyld_get_image_vmaddr_slide;
1132611326pub const _dyld_image_count = darwin._dyld_image_count;
11327pub const _dyld_get_image_header_containing_address = darwin._dyld_get_image_header_containing_address;
11328pub const dyld_image_path_containing_address = darwin.dyld_image_path_containing_address;
1132711329pub const _host_page_size = darwin._host_page_size;
1132811330pub const boolean_t = darwin.boolean_t;
1132911331pub const clock_get_time = darwin.clock_get_time;
lib/std/c/darwin.zig+2
......@@ -354,6 +354,8 @@ pub extern "c" fn _dyld_image_count() u32;
354354pub extern "c" fn _dyld_get_image_header(image_index: u32) ?*mach_header;
355355pub extern "c" fn _dyld_get_image_vmaddr_slide(image_index: u32) usize;
356356pub extern "c" fn _dyld_get_image_name(image_index: u32) [*:0]const u8;
357pub extern "c" fn _dyld_get_image_header_containing_address(address: *const anyopaque) ?*mach_header;
358pub extern "c" fn dyld_image_path_containing_address(address: *const anyopaque) ?[*:0]const u8;
357359pub extern "c" fn dladdr(addr: *const anyopaque, info: *dl_info) c_int;
358360
359361pub const dl_info = extern struct {
lib/std/debug/SelfInfo/MachO.zig+29-21
......@@ -78,9 +78,14 @@ pub fn getSymbol(si: *SelfInfo, gpa: Allocator, io: Io, address: usize) Error!st
7878 };
7979}
8080pub fn getModuleName(si: *SelfInfo, gpa: Allocator, address: usize) Error![]const u8 {
81 const module = try si.findModule(gpa, address);
82 defer si.mutex.unlock();
83 return module.name;
81 _ = si;
82 _ = gpa;
83 // This function is marked as deprecated; however, it is significantly more
84 // performant than `dladdr` (since the latter also does a very slow symbol
85 // lookup), so let's use it since it's still available.
86 return std.mem.span(std.c.dyld_image_path_containing_address(
87 @ptrFromInt(address),
88 ) orelse return error.MissingDebugInfo);
8489}
8590pub fn getModuleSlide(si: *SelfInfo, gpa: Allocator, address: usize) Error!usize {
8691 const module = try si.findModule(gpa, address);
......@@ -426,28 +431,26 @@ fn unwindFrameInner(si: *SelfInfo, gpa: Allocator, context: *UnwindContext) !usi
426431
427432/// Acquires the mutex on success.
428433fn findModule(si: *SelfInfo, gpa: Allocator, address: usize) Error!*Module {
429 var info: std.c.dl_info = undefined;
430 if (std.c.dladdr(@ptrFromInt(address), &info) == 0) {
431 return error.MissingDebugInfo;
432 }
434 // This function is marked as deprecated; however, it is significantly more
435 // performant than `dladdr` (since the latter also does a very slow symbol
436 // lookup), so let's use it since it's still available.
437 const text_base = std.c._dyld_get_image_header_containing_address(
438 @ptrFromInt(address),
439 ) orelse return error.MissingDebugInfo;
433440 si.mutex.lock();
434441 errdefer si.mutex.unlock();
435 const gop = try si.modules.getOrPutAdapted(gpa, @intFromPtr(info.fbase), Module.Adapter{});
442 const gop = try si.modules.getOrPutAdapted(gpa, @intFromPtr(text_base), Module.Adapter{});
436443 errdefer comptime unreachable;
437 if (!gop.found_existing) {
438 gop.key_ptr.* = .{
439 .text_base = @intFromPtr(info.fbase),
440 .name = std.mem.span(info.fname),
441 .unwind = null,
442 .file = null,
443 };
444 }
444 if (!gop.found_existing) gop.key_ptr.* = .{
445 .text_base = @intFromPtr(text_base),
446 .unwind = null,
447 .file = null,
448 };
445449 return gop.key_ptr;
446450}
447451
448452const Module = struct {
449453 text_base: usize,
450 name: []const u8,
451454 unwind: ?(Error!Unwind),
452455 file: ?(Error!MachOFile),
453456
......@@ -544,10 +547,15 @@ const Module = struct {
544547 }
545548
546549 fn getFile(module: *Module, gpa: Allocator) Error!*MachOFile {
547 if (module.file == null) module.file = MachOFile.load(gpa, module.name, builtin.cpu.arch) catch |err| switch (err) {
548 error.InvalidMachO, error.InvalidDwarf => error.InvalidDebugInfo,
549 error.MissingDebugInfo, error.OutOfMemory, error.UnsupportedDebugInfo, error.ReadFailed => |e| e,
550 };
550 if (module.file == null) {
551 const path = std.mem.span(
552 std.c.dyld_image_path_containing_address(@ptrFromInt(module.text_base)).?,
553 );
554 module.file = MachOFile.load(gpa, path, builtin.cpu.arch) catch |err| switch (err) {
555 error.InvalidMachO, error.InvalidDwarf => error.InvalidDebugInfo,
556 error.MissingDebugInfo, error.OutOfMemory, error.UnsupportedDebugInfo, error.ReadFailed => |e| e,
557 };
558 }
551559 return if (module.file.?) |*f| f else |err| err;
552560 }
553561};