authorgravatar for jacobly@ziglang.orgJacob Young <jacobly@ziglang.org> 2025-03-28 10:05:52-04:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-07-20 10:38:38-07:00
log1216e807ac03e62c981ecfc74d8b18b39533c2d9
treed6cda78aa6221bc17cba885cdb94413d55a9c7cf
parent048a40dac72fc26fe145ac8426745e22b188d156

EventLoop: move context after the async closure

This avoids needing to store more sizes and alignments. Only the result alignment needs to be stored, because `Fiber` is at a fixed zero offset.

1 files changed, 86 insertions(+), 92 deletions(-)

lib/std/Io/EventLoop.zig+86-92
...@@ -4,28 +4,23 @@ const assert = std.debug.assert;...@@ -4,28 +4,23 @@ const assert = std.debug.assert;
4const Allocator = std.mem.Allocator;4const Allocator = std.mem.Allocator;
5const Io = std.Io;5const Io = std.Io;
6const EventLoop = @This();6const EventLoop = @This();
7const Alignment = std.mem.Alignment;
78
8gpa: Allocator,9gpa: Allocator,
9mutex: std.Thread.Mutex,10mutex: std.Thread.Mutex,
10cond: std.Thread.Condition,11cond: std.Thread.Condition,
11queue: std.DoublyLinkedList(void),12queue: std.DoublyLinkedList(void),
12free: std.DoublyLinkedList(void),13free: std.DoublyLinkedList(void),
13main_fiber_buffer: [@sizeOf(Fiber) + max_result_len]u8 align(@alignOf(Fiber)),14main_context: Context,
14exit_awaiter: ?*Fiber,15exit_awaiter: ?*Fiber,
15idle_count: usize,16idle_count: usize,
16threads: std.ArrayListUnmanaged(Thread),17threads: std.ArrayListUnmanaged(Thread),
1718
18threadlocal var current_idle_context: *Context = undefined;19threadlocal var current_idle_context: *Context = undefined;
19threadlocal var current_fiber_context: *Context = undefined;20threadlocal var current_context: *Context = undefined;
2021
21/// Also used for context.22/// Empirically saw 10KB being used by the self-hosted backend for logging.
22const max_result_len = 64;
23/// Also used for context.
24const max_result_align: std.mem.Alignment = .@"16";
25
26const min_stack_size = 4 * 1024 * 1024;
27const idle_stack_size = 32 * 1024;23const idle_stack_size = 32 * 1024;
28const stack_align = 16;
2924
30const Thread = struct {25const Thread = struct {
31 thread: std.Thread,26 thread: std.Thread,
...@@ -33,45 +28,53 @@ const Thread = struct {...@@ -33,45 +28,53 @@ const Thread = struct {
33};28};
3429
35const Fiber = struct {30const Fiber = struct {
36 _: void align(max_result_align.toByteUnits()) = {},
37
38 context: Context,31 context: Context,
39 awaiter: ?*Fiber,32 awaiter: ?*Fiber,
40 queue_node: std.DoublyLinkedList(void).Node,33 queue_node: std.DoublyLinkedList(void).Node,
34 result_align: Alignment,
4135
42 const finished: ?*Fiber = @ptrFromInt(std.mem.alignBackward(usize, std.math.maxInt(usize), @alignOf(Fiber)));36 const finished: ?*Fiber = @ptrFromInt(std.mem.alignBackward(usize, std.math.maxInt(usize), @alignOf(Fiber)));
4337
44 fn allocatedSlice(f: *Fiber) []align(@alignOf(Fiber)) u8 {38 const max_result_align: Alignment = .@"16";
45 const base: [*]align(@alignOf(Fiber)) u8 = @ptrCast(f);39 const max_result_size = max_result_align.forward(64);
46 return base[0..std.mem.alignForward(40 /// This includes any stack realignments that need to happen, and also the
41 /// initial frame return address slot and argument frame, depending on target.
42 const min_stack_size = 4 * 1024 * 1024;
43 const max_context_align: Alignment = .@"16";
44 const max_context_size = max_context_align.forward(1024);
45 const allocation_size = std.mem.alignForward(
46 usize,
47 std.mem.alignForward(
47 usize,48 usize,
48 resultOffset() + max_result_len + min_stack_size,49 max_result_align.forward(@sizeOf(Fiber)) + max_result_size + min_stack_size,
49 std.heap.page_size_max,50 @max(@alignOf(AsyncClosure), max_context_align.toByteUnits()),
50 )];51 ) + @sizeOf(AsyncClosure) + max_context_size,
51 }52 std.heap.page_size_max,
5253 );
53 fn argsOffset() usize {54
54 return max_result_align.forward(@sizeOf(Fiber));55 fn allocate(el: *EventLoop) error{OutOfMemory}!*Fiber {
55 }56 return if (free_node: {
5657 el.mutex.lock();
57 fn resultOffset() usize {58 defer el.mutex.unlock();
58 return max_result_align.forward(argsOffset() + max_result_len);59 break :free_node el.free.pop();
59 }60 }) |free_node|
6061 @alignCast(@fieldParentPtr("queue_node", free_node))
61 fn argsSlice(f: *Fiber) []u8 {62 else
62 const base: [*]align(@alignOf(Fiber)) u8 = @ptrCast(f);63 @ptrCast(try el.gpa.alignedAlloc(u8, @alignOf(Fiber), allocation_size));
63 return base[argsOffset()..][0..max_result_len];
64 }64 }
6565
66 fn resultSlice(f: *Fiber) []u8 {66 fn allocatedSlice(f: *Fiber) []align(@alignOf(Fiber)) u8 {
67 const base: [*]align(@alignOf(Fiber)) u8 = @ptrCast(f);67 return @as([*]align(@alignOf(Fiber)) u8, @ptrCast(f))[0..allocation_size];
68 return base[resultOffset()..][0..max_result_len];
69 }68 }
7069
71 fn stackEndPointer(f: *Fiber) [*]u8 {70 fn allocatedEnd(f: *Fiber) [*]u8 {
72 const allocated_slice = f.allocatedSlice();71 const allocated_slice = f.allocatedSlice();
73 return allocated_slice[allocated_slice.len..].ptr;72 return allocated_slice[allocated_slice.len..].ptr;
74 }73 }
74
75 fn resultPointer(f: *Fiber) [*]u8 {
76 return @ptrFromInt(f.result_align.forward(@intFromPtr(f) + @sizeOf(Fiber)));
77 }
75};78};
7679
77pub fn io(el: *EventLoop) Io {80pub fn io(el: *EventLoop) Io {
...@@ -88,7 +91,7 @@ pub fn init(el: *EventLoop, gpa: Allocator) error{OutOfMemory}!void {...@@ -88,7 +91,7 @@ pub fn init(el: *EventLoop, gpa: Allocator) error{OutOfMemory}!void {
88 const threads_bytes = ((std.Thread.getCpuCount() catch 1) -| 1) * @sizeOf(Thread);91 const threads_bytes = ((std.Thread.getCpuCount() catch 1) -| 1) * @sizeOf(Thread);
89 const idle_context_offset = std.mem.alignForward(usize, threads_bytes, @alignOf(Context));92 const idle_context_offset = std.mem.alignForward(usize, threads_bytes, @alignOf(Context));
90 const idle_stack_end_offset = std.mem.alignForward(usize, idle_context_offset + idle_stack_size, std.heap.page_size_max);93 const idle_stack_end_offset = std.mem.alignForward(usize, idle_context_offset + idle_stack_size, std.heap.page_size_max);
91 const allocated_slice = try gpa.alignedAlloc(u8, @max(@alignOf(Thread), @alignOf(Context), stack_align), idle_stack_end_offset);94 const allocated_slice = try gpa.alignedAlloc(u8, @max(@alignOf(Thread), @alignOf(Context)), idle_stack_end_offset);
92 errdefer gpa.free(allocated_slice);95 errdefer gpa.free(allocated_slice);
93 el.* = .{96 el.* = .{
94 .gpa = gpa,97 .gpa = gpa,
...@@ -96,13 +99,13 @@ pub fn init(el: *EventLoop, gpa: Allocator) error{OutOfMemory}!void {...@@ -96,13 +99,13 @@ pub fn init(el: *EventLoop, gpa: Allocator) error{OutOfMemory}!void {
96 .cond = .{},99 .cond = .{},
97 .queue = .{},100 .queue = .{},
98 .free = .{},101 .free = .{},
99 .main_fiber_buffer = undefined,102 .main_context = undefined,
100 .exit_awaiter = null,103 .exit_awaiter = null,
101 .idle_count = 0,104 .idle_count = 0,
102 .threads = .initBuffer(@ptrCast(allocated_slice[0..threads_bytes])),105 .threads = .initBuffer(@ptrCast(allocated_slice[0..threads_bytes])),
103 };106 };
104 const main_idle_context: *Context = @alignCast(std.mem.bytesAsValue(Context, allocated_slice[idle_context_offset..][0..@sizeOf(Context)]));107 const main_idle_context: *Context = @alignCast(std.mem.bytesAsValue(Context, allocated_slice[idle_context_offset..][0..@sizeOf(Context)]));
105 const idle_stack_end: [*]align(stack_align) usize = @alignCast(@ptrCast(allocated_slice[idle_stack_end_offset..].ptr));108 const idle_stack_end: [*]align(@max(@alignOf(Thread), @alignOf(Context))) usize = @alignCast(@ptrCast(allocated_slice[idle_stack_end_offset..].ptr));
106 (idle_stack_end - 1)[0..1].* = .{@intFromPtr(el)};109 (idle_stack_end - 1)[0..1].* = .{@intFromPtr(el)};
107 main_idle_context.* = .{110 main_idle_context.* = .{
108 .rsp = @intFromPtr(idle_stack_end - 1),111 .rsp = @intFromPtr(idle_stack_end - 1),
...@@ -111,9 +114,8 @@ pub fn init(el: *EventLoop, gpa: Allocator) error{OutOfMemory}!void {...@@ -111,9 +114,8 @@ pub fn init(el: *EventLoop, gpa: Allocator) error{OutOfMemory}!void {
111 };114 };
112 std.log.debug("created main idle {*}", .{main_idle_context});115 std.log.debug("created main idle {*}", .{main_idle_context});
113 current_idle_context = main_idle_context;116 current_idle_context = main_idle_context;
114 const current_fiber: *Fiber = @ptrCast(&el.main_fiber_buffer);117 std.log.debug("created main {*}", .{&el.main_context});
115 std.log.debug("created main fiber {*}", .{current_fiber});118 current_context = &el.main_context;
116 current_fiber_context = &current_fiber.context;
117}119}
118120
119pub fn deinit(el: *EventLoop) void {121pub fn deinit(el: *EventLoop) void {
...@@ -125,27 +127,11 @@ pub fn deinit(el: *EventLoop) void {...@@ -125,27 +127,11 @@ pub fn deinit(el: *EventLoop) void {
125 }127 }
126 const idle_context_offset = std.mem.alignForward(usize, el.threads.items.len * @sizeOf(Thread), @alignOf(Context));128 const idle_context_offset = std.mem.alignForward(usize, el.threads.items.len * @sizeOf(Thread), @alignOf(Context));
127 const idle_stack_end = std.mem.alignForward(usize, idle_context_offset + idle_stack_size, std.heap.page_size_max);129 const idle_stack_end = std.mem.alignForward(usize, idle_context_offset + idle_stack_size, std.heap.page_size_max);
128 const allocated_ptr: [*]align(@max(@alignOf(Thread), @alignOf(Context), stack_align)) u8 = @alignCast(@ptrCast(el.threads.items.ptr));130 const allocated_ptr: [*]align(@max(@alignOf(Thread), @alignOf(Context))) u8 = @alignCast(@ptrCast(el.threads.items.ptr));
129 for (el.threads.items) |*thread| thread.thread.join();131 for (el.threads.items) |*thread| thread.thread.join();
130 el.gpa.free(allocated_ptr[0..idle_stack_end]);132 el.gpa.free(allocated_ptr[0..idle_stack_end]);
131}133}
132134
133fn allocateFiber(el: *EventLoop) error{OutOfMemory}!*Fiber {
134 const free_node = free_node: {
135 el.mutex.lock();
136 defer el.mutex.unlock();
137 break :free_node el.free.pop();
138 } orelse {
139 const n = std.mem.alignForward(
140 usize,
141 Fiber.resultOffset() + max_result_len + min_stack_size,
142 std.heap.page_size_max,
143 );
144 return @alignCast(@ptrCast(try el.gpa.alignedAlloc(u8, @alignOf(Fiber), n)));
145 };
146 return @alignCast(@fieldParentPtr("queue_node", free_node));
147}
148
149fn yield(el: *EventLoop, optional_fiber: ?*Fiber, register_awaiter: ?*?*Fiber) void {135fn yield(el: *EventLoop, optional_fiber: ?*Fiber, register_awaiter: ?*?*Fiber) void {
150 const ready_context: *Context = ready_context: {136 const ready_context: *Context = ready_context: {
151 const ready_fiber: *Fiber = optional_fiber orelse if (ready_node: {137 const ready_fiber: *Fiber = optional_fiber orelse if (ready_node: {
...@@ -159,7 +145,7 @@ fn yield(el: *EventLoop, optional_fiber: ?*Fiber, register_awaiter: ?*?*Fiber) v...@@ -159,7 +145,7 @@ fn yield(el: *EventLoop, optional_fiber: ?*Fiber, register_awaiter: ?*?*Fiber) v
159 break :ready_context &ready_fiber.context;145 break :ready_context &ready_fiber.context;
160 };146 };
161 const message: SwitchMessage = .{147 const message: SwitchMessage = .{
162 .prev_context = current_fiber_context,148 .prev_context = current_context,
163 .ready_context = ready_context,149 .ready_context = ready_context,
164 .register_awaiter = register_awaiter,150 .register_awaiter = register_awaiter,
165 };151 };
...@@ -189,14 +175,13 @@ fn schedule(el: *EventLoop, fiber: *Fiber) void {...@@ -189,14 +175,13 @@ fn schedule(el: *EventLoop, fiber: *Fiber) void {
189175
190fn recycle(el: *EventLoop, fiber: *Fiber) void {176fn recycle(el: *EventLoop, fiber: *Fiber) void {
191 std.log.debug("recyling {*}", .{fiber});177 std.log.debug("recyling {*}", .{fiber});
192 fiber.awaiter = undefined;178 @memset(fiber.allocatedSlice(), undefined);
193 @memset(fiber.resultSlice(), undefined);
194 el.mutex.lock();179 el.mutex.lock();
195 defer el.mutex.unlock();180 defer el.mutex.unlock();
196 el.free.append(&fiber.queue_node);181 el.free.append(&fiber.queue_node);
197}182}
198183
199fn mainIdle(el: *EventLoop, message: *const SwitchMessage) callconv(.c) noreturn {184fn mainIdle(el: *EventLoop, message: *const SwitchMessage) callconv(.withStackAlign(.c, @max(@alignOf(Thread), @alignOf(Context)))) noreturn {
200 message.handle(el);185 message.handle(el);
201 el.yield(el.idle(), null);186 el.yield(el.idle(), null);
202 unreachable; // switched to dead fiber187 unreachable; // switched to dead fiber
...@@ -205,7 +190,7 @@ fn mainIdle(el: *EventLoop, message: *const SwitchMessage) callconv(.c) noreturn...@@ -205,7 +190,7 @@ fn mainIdle(el: *EventLoop, message: *const SwitchMessage) callconv(.c) noreturn
205fn threadEntry(el: *EventLoop, thread: *Thread) void {190fn threadEntry(el: *EventLoop, thread: *Thread) void {
206 std.log.debug("created thread idle {*}", .{&thread.idle_context});191 std.log.debug("created thread idle {*}", .{&thread.idle_context});
207 current_idle_context = &thread.idle_context;192 current_idle_context = &thread.idle_context;
208 current_fiber_context = &thread.idle_context;193 current_context = &thread.idle_context;
209 _ = el.idle();194 _ = el.idle();
210}195}
211196
...@@ -230,7 +215,7 @@ const SwitchMessage = extern struct {...@@ -230,7 +215,7 @@ const SwitchMessage = extern struct {
230 register_awaiter: ?*?*Fiber,215 register_awaiter: ?*?*Fiber,
231216
232 fn handle(message: *const SwitchMessage, el: *EventLoop) void {217 fn handle(message: *const SwitchMessage, el: *EventLoop) void {
233 current_fiber_context = message.ready_context;218 current_context = message.ready_context;
234 if (message.register_awaiter) |awaiter| {219 if (message.register_awaiter) |awaiter| {
235 const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.prev_context));220 const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.prev_context));
236 if (@atomicRmw(?*Fiber, awaiter, .Xchg, prev_fiber, .acq_rel) == Fiber.finished) el.schedule(prev_fiber);221 if (@atomicRmw(?*Fiber, awaiter, .Xchg, prev_fiber, .acq_rel) == Fiber.finished) el.schedule(prev_fiber);
...@@ -238,10 +223,13 @@ const SwitchMessage = extern struct {...@@ -238,10 +223,13 @@ const SwitchMessage = extern struct {
238 }223 }
239};224};
240225
241const Context = extern struct {226const Context = switch (builtin.cpu.arch) {
242 rsp: usize,227 .x86_64 => extern struct {
243 rbp: usize,228 rsp: u64,
244 rip: usize,229 rbp: u64,
230 rip: u64,
231 },
232 else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)),
245};233};
246234
247inline fn contextSwitch(message: *const SwitchMessage) *const SwitchMessage {235inline fn contextSwitch(message: *const SwitchMessage) *const SwitchMessage {
...@@ -299,42 +287,45 @@ fn fiberEntry() callconv(.naked) void {...@@ -299,42 +287,45 @@ fn fiberEntry() callconv(.naked) void {
299pub fn @"async"(287pub fn @"async"(
300 userdata: ?*anyopaque,288 userdata: ?*anyopaque,
301 result: []u8,289 result: []u8,
302 result_alignment: std.mem.Alignment,290 result_alignment: Alignment,
303 context: []const u8,291 context: []const u8,
304 context_alignment: std.mem.Alignment,292 context_alignment: Alignment,
305 start: *const fn (context: *const anyopaque, result: *anyopaque) void,293 start: *const fn (context: *const anyopaque, result: *anyopaque) void,
306) ?*std.Io.AnyFuture {294) ?*std.Io.AnyFuture {
307 assert(result_alignment.compare(.lte, max_result_align)); // TODO295 assert(result_alignment.compare(.lte, Fiber.max_result_align)); // TODO
308 assert(context_alignment.compare(.lte, max_result_align)); // TODO296 assert(context_alignment.compare(.lte, Fiber.max_context_align)); // TODO
309 assert(result.len <= max_result_len); // TODO297 assert(result.len <= Fiber.max_result_size); // TODO
310 assert(context.len <= max_result_len); // TODO298 assert(context.len <= Fiber.max_context_size); // TODO
311299
312 const event_loop: *EventLoop = @alignCast(@ptrCast(userdata));300 const event_loop: *EventLoop = @alignCast(@ptrCast(userdata));
313 const fiber = event_loop.allocateFiber() catch {301 const fiber = Fiber.allocate(event_loop) catch {
314 start(context.ptr, result.ptr);302 start(context.ptr, result.ptr);
315 return null;303 return null;
316 };304 };
317 fiber.awaiter = null;
318 fiber.queue_node = .{ .data = {} };
319 @memcpy(fiber.argsSlice()[0..context.len], context);
320 std.log.debug("allocated {*}", .{fiber});305 std.log.debug("allocated {*}", .{fiber});
321306
322 const closure: *AsyncClosure = @ptrFromInt(std.mem.alignBackward(307 const closure: *AsyncClosure = @ptrFromInt(Fiber.max_context_align.max(.of(AsyncClosure)).backward(
323 usize,308 @intFromPtr(fiber.allocatedEnd()) - Fiber.max_context_size,
324 @intFromPtr(fiber.stackEndPointer() - @sizeOf(AsyncClosure)),309 ) - @sizeOf(AsyncClosure));
325 @max(@alignOf(AsyncClosure), stack_align),310 fiber.* = .{
326 ));311 .context = switch (builtin.cpu.arch) {
312 .x86_64 => .{
313 .rsp = @intFromPtr(closure) - @sizeOf(usize),
314 .rbp = 0,
315 .rip = @intFromPtr(&fiberEntry),
316 },
317 else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)),
318 },
319 .awaiter = null,
320 .queue_node = undefined,
321 .result_align = result_alignment,
322 };
327 closure.* = .{323 closure.* = .{
328 .event_loop = event_loop,324 .event_loop = event_loop,
329 .fiber = fiber,325 .fiber = fiber,
330 .start = start,326 .start = start,
331 };327 };
332 const stack_end: [*]align(stack_align) usize = @alignCast(@ptrCast(closure));328 @memcpy(closure.contextPointer(), context);
333 fiber.context = .{
334 .rsp = @intFromPtr(stack_end - 1),
335 .rbp = 0,
336 .rip = @intFromPtr(&fiberEntry),
337 };
338329
339 event_loop.schedule(fiber);330 event_loop.schedule(fiber);
340 return @ptrCast(fiber);331 return @ptrCast(fiber);
...@@ -345,10 +336,14 @@ const AsyncClosure = struct {...@@ -345,10 +336,14 @@ const AsyncClosure = struct {
345 fiber: *Fiber,336 fiber: *Fiber,
346 start: *const fn (context: *const anyopaque, result: *anyopaque) void,337 start: *const fn (context: *const anyopaque, result: *anyopaque) void,
347338
348 fn call(closure: *AsyncClosure, message: *const SwitchMessage) callconv(.c) noreturn {339 fn contextPointer(closure: *AsyncClosure) [*]align(Fiber.max_context_align.toByteUnits()) u8 {
340 return @alignCast(@as([*]u8, @ptrCast(closure)) + @sizeOf(AsyncClosure));
341 }
342
343 fn call(closure: *AsyncClosure, message: *const SwitchMessage) callconv(.withStackAlign(.c, @alignOf(AsyncClosure))) noreturn {
349 message.handle(closure.event_loop);344 message.handle(closure.event_loop);
350 std.log.debug("{*} performing async", .{closure.fiber});345 std.log.debug("{*} performing async", .{closure.fiber});
351 closure.start(closure.fiber.argsSlice().ptr, closure.fiber.resultSlice().ptr);346 closure.start(closure.contextPointer(), closure.fiber.resultPointer());
352 const awaiter = @atomicRmw(?*Fiber, &closure.fiber.awaiter, .Xchg, Fiber.finished, .acq_rel);347 const awaiter = @atomicRmw(?*Fiber, &closure.fiber.awaiter, .Xchg, Fiber.finished, .acq_rel);
353 closure.event_loop.yield(awaiter, null);348 closure.event_loop.yield(awaiter, null);
354 unreachable; // switched to dead fiber349 unreachable; // switched to dead fiber
...@@ -358,8 +353,7 @@ const AsyncClosure = struct {...@@ -358,8 +353,7 @@ const AsyncClosure = struct {
358pub fn @"await"(userdata: ?*anyopaque, any_future: *std.Io.AnyFuture, result: []u8) void {353pub fn @"await"(userdata: ?*anyopaque, any_future: *std.Io.AnyFuture, result: []u8) void {
359 const event_loop: *EventLoop = @alignCast(@ptrCast(userdata));354 const event_loop: *EventLoop = @alignCast(@ptrCast(userdata));
360 const future_fiber: *Fiber = @alignCast(@ptrCast(any_future));355 const future_fiber: *Fiber = @alignCast(@ptrCast(any_future));
361 const result_src = future_fiber.resultSlice()[0..result.len];
362 if (@atomicLoad(?*Fiber, &future_fiber.awaiter, .acquire) != Fiber.finished) event_loop.yield(null, &future_fiber.awaiter);356 if (@atomicLoad(?*Fiber, &future_fiber.awaiter, .acquire) != Fiber.finished) event_loop.yield(null, &future_fiber.awaiter);
363 @memcpy(result, result_src);357 @memcpy(result, future_fiber.resultPointer());
364 event_loop.recycle(future_fiber);358 event_loop.recycle(future_fiber);
365}359}