authorgravatar for luuk@degram.devLuuk de Gram <luuk@degram.dev> 2022-02-03 21:31:35+01:00
committergravatar for luuk@degram.devLuuk de Gram <luuk@degram.dev> 2022-02-03 21:53:48+01:00
loge35414bf5c356798f201be85303101f59220326c
tree88e88220419d4f11ed00d02544337974ff99eef4
parentae1e3c8f9bc86eeefb5a83233884a134f7b974f4
signaturelock-open Commit is signed but in an unrecognized format.

wasm: Refactor stack to account for alignment

We now calculate the total stack size required for the current frame. The default alignment of the stack is 16 bytes, and will be overwritten when the alignment of a given type is larger than that. After we have generated all instructions for the body, we calculate the total stack size by forward aligning the stack size while accounting for the max alignment. We then insert a prologue into the body, where we substract this size from the stack pointer and save it inside a bottom stackframe local. We use this local then, to calculate the stack pointer locals of all variables we allocate into the stack. In a future iteration we can improve this further by storing the offsets as a new `stack_offset` `WValue`. This has the benefit of not having to spend runtime cost of storing those offsets, but instead we append those offsets whenever we need the value that lives in the stack.

1 files changed, 110 insertions(+), 58 deletions(-)

src/arch/wasm/CodeGen.zig+110-58
......@@ -560,6 +560,9 @@ mir_extra: std.ArrayListUnmanaged(u32) = .{},
560560/// When a function is executing, we store the the current stack pointer's value within this local.
561561/// This value is then used to restore the stack pointer to the original value at the return of the function.
562562initial_stack_value: WValue = .none,
563/// The current stack pointer substracted with the stack size. From this value, we will calculate
564/// all offsets of the stack values.
565bottom_stack_value: WValue = .none,
563566/// Arguments of this function declaration
564567/// This will be set after `resolveCallingConventionValues`
565568args: []WValue = &.{},
......@@ -567,6 +570,14 @@ args: []WValue = &.{},
567570/// When it returns a pointer to the stack, the `.local` tag will be active and must be populated
568571/// before this function returns its execution to the caller.
569572return_value: WValue = .none,
573/// The size of the stack this function occupies. In the function prologue
574/// we will move the stack pointer by this number, forward aligned with the `stack_alignment`.
575stack_size: u32 = 0,
576/// The stack alignment, which is 16 bytes by default. This is specified by the
577/// tool-conventions: https://github.com/WebAssembly/tool-conventions/blob/main/BasicCABI.md
578/// and also what the llvm backend will emit.
579/// However, local variables or the usage of `@setAlignStack` can overwrite this default.
580stack_alignment: u32 = 16,
570581
571582const InnerError = error{
572583 OutOfMemory,
......@@ -654,13 +665,6 @@ fn addInst(self: *Self, inst: Mir.Inst) error{OutOfMemory}!void {
654665 try self.mir_instructions.append(self.gpa, inst);
655666}
656667
657/// Inserts a Mir instruction at the given `offset`.
658/// Asserts offset is within bound.
659fn addInstAt(self: *Self, offset: usize, inst: Mir.Inst) error{OutOfMemory}!void {
660 try self.mir_instructions.ensureUnusedCapacity(self.gpa, 1);
661 self.mir_instructions.insertAssumeCapacity(offset, inst);
662}
663
664668fn addTag(self: *Self, tag: Mir.Inst.Tag) error{OutOfMemory}!void {
665669 try self.addInst(.{ .tag = tag, .data = .{ .tag = {} } });
666670}
......@@ -845,10 +849,43 @@ pub fn genFunc(self: *Self) InnerError!void {
845849 try self.addTag(.@"unreachable");
846850 }
847851 }
848
849852 // End of function body
850853 try self.addTag(.end);
851854
855 // check if we have to initialize and allocate anything into the stack frame.
856 // If so, create enough stack space and insert the instructions at the front of the list.
857 if (self.stack_size > 0) {
858 var prologue = std.ArrayList(Mir.Inst).init(self.gpa);
859 defer prologue.deinit();
860
861 // load stack pointer
862 try prologue.append(.{ .tag = .global_get, .data = .{ .label = 0 } });
863 // store stack pointer so we can restore it when we return from the function
864 try prologue.append(.{ .tag = .local_tee, .data = .{ .label = self.initial_stack_value.local } });
865 // get the total stack size
866 const aligned_stack = std.mem.alignForwardGeneric(u32, self.stack_size, self.stack_alignment);
867 try prologue.append(.{ .tag = .i32_const, .data = .{ .imm32 = @intCast(i32, aligned_stack) } });
868 // substract it from the current stack pointer
869 try prologue.append(.{ .tag = .i32_sub, .data = .{ .tag = {} } });
870 // Get negative stack aligment
871 try prologue.append(.{ .tag = .i32_const, .data = .{ .imm32 = @intCast(i32, self.stack_alignment) * -1 } });
872 // Bit and the value to get the new stack pointer to ensure the pointers are aligned with the abi alignment
873 try prologue.append(.{ .tag = .i32_and, .data = .{ .tag = {} } });
874 // store the current stack pointer as the bottom, which will be used to calculate all stack pointer offsets
875 try prologue.append(.{ .tag = .local_tee, .data = .{ .label = self.bottom_stack_value.local } });
876 // Store the current stack pointer value into the global stack pointer so other function calls will
877 // start from this value instead and not overwrite the current stack.
878 try prologue.append(.{ .tag = .global_set, .data = .{ .label = 0 } });
879
880 // reserve space and insert all prologue instructions at the front of the instruction list
881 // We insert them in reserve order as there is no insertSlice in multiArrayList.
882 try self.mir_instructions.ensureUnusedCapacity(self.gpa, prologue.items.len);
883 for (prologue.items) |_, index| {
884 const inst = prologue.items[prologue.items.len - 1 - index];
885 self.mir_instructions.insertAssumeCapacity(0, inst);
886 }
887 }
888
852889 var mir: Mir = .{
853890 .instructions = self.mir_instructions.toOwnedSlice(),
854891 .extra = self.mir_extra.toOwnedSlice(self.gpa),
......@@ -1137,7 +1174,7 @@ pub const DeclGen = struct {
11371174 },
11381175 .decl_ref => {
11391176 const decl = val.castTag(.decl_ref).?.data;
1140 return self.lowerDeclRefValue(ty, val, decl, writer, 0);
1177 return self.lowerDeclRefValue(ty, val, decl, 0);
11411178 },
11421179 .slice => {
11431180 const slice = val.castTag(.slice).?.data;
......@@ -1161,9 +1198,9 @@ pub const DeclGen = struct {
11611198 const elem_ptr = val.castTag(.elem_ptr).?.data;
11621199 const elem_size = ty.childType().abiSize(self.target());
11631200 const offset = elem_ptr.index * elem_size;
1164 return self.lowerParentPtr(elem_ptr.array_ptr, writer, offset);
1201 return self.lowerParentPtr(elem_ptr.array_ptr, offset);
11651202 },
1166 .int_u64 => return self.genTypedValue(Type.usize, val, writer),
1203 .int_u64 => return self.genTypedValue(Type.usize, val),
11671204 else => return self.fail("TODO: Implement zig decl gen for pointer type value: '{s}'", .{@tagName(val.tag())}),
11681205 },
11691206 .ErrorUnion => {
......@@ -1309,22 +1346,16 @@ fn resolveCallingConventionValues(self: *Self, fn_ty: Type) InnerError!CallWValu
13091346 return result;
13101347}
13111348
1312/// Retrieves the stack pointer's value from the global variable and stores
1313/// it in a local
1349/// Creates a local for the initial stack value
13141350/// Asserts `initial_stack_value` is `.none`
13151351fn initializeStack(self: *Self) !void {
13161352 assert(self.initial_stack_value == .none);
1317 // reserve space for immediate value
1318 // get stack pointer global
1319 try self.addLabel(.global_get, 0);
1320
13211353 // Reserve a local to store the current stack pointer
13221354 // We can later use this local to set the stack pointer back to the value
13231355 // we have stored here.
1324 self.initial_stack_value = try self.allocLocal(Type.initTag(.i32));
1325
1326 // save the value to the local
1327 try self.addLabel(.local_set, self.initial_stack_value.local);
1356 self.initial_stack_value = try self.allocLocal(Type.usize);
1357 // Also reserve a local to store the bottom stack value
1358 self.bottom_stack_value = try self.allocLocal(Type.usize);
13281359}
13291360
13301361/// Reads the stack pointer from `Context.initial_stack_value` and writes it
......@@ -1339,36 +1370,75 @@ fn restoreStackPointer(self: *Self) !void {
13391370 try self.addLabel(.global_set, 0);
13401371}
13411372
1342/// Moves the stack pointer by given `offset`
1343/// It does this by retrieving the stack pointer, subtracting `offset` and storing
1344/// the result back into the stack pointer.
1345fn moveStack(self: *Self, offset: u32, local: u32) !void {
1346 if (offset == 0) return;
1347 try self.addLabel(.global_get, 0);
1348 try self.addImm32(@bitCast(i32, offset));
1349 try self.addTag(.i32_sub);
1350 try self.addLabel(.local_tee, local);
1351 try self.addLabel(.global_set, 0);
1373/// Saves the current stack size's stack pointer position into a given local
1374/// It does this by retrieving the bottom stack pointer, adding `self.stack_size` and storing
1375/// the result back into the local.
1376fn saveStack(self: *Self) !WValue {
1377 const local = try self.allocLocal(Type.usize);
1378 try self.addLabel(.local_get, self.bottom_stack_value.local);
1379 try self.addImm32(@intCast(i32, self.stack_size));
1380 try self.addTag(.i32_add);
1381 try self.addLabel(.local_set, local.local);
1382 return local;
13521383}
13531384
13541385/// From a given type, will create space on the virtual stack to store the value of such type.
13551386/// This returns a `WValue` with its active tag set to `local`, containing the index to the local
13561387/// that points to the position on the virtual stack. This function should be used instead of
1357/// moveStack unless a local was already created to store the point.
1388/// moveStack unless a local was already created to store the pointer.
13581389///
13591390/// Asserts Type has codegenbits
13601391fn allocStack(self: *Self, ty: Type) !WValue {
13611392 assert(ty.hasRuntimeBits());
1393 if (self.initial_stack_value == .none) {
1394 try self.initializeStack();
1395 }
13621396
1363 // calculate needed stack space
13641397 const abi_size = std.math.cast(u32, ty.abiSize(self.target)) catch {
1365 return self.fail("Given type '{}' too big to fit into stack frame", .{ty});
1398 return self.fail("Type {} with ABI size of {d} exceeds stack frame size", .{ ty, ty.abiSize(self.target) });
13661399 };
1400 const abi_align = ty.abiAlignment(self.target);
13671401
1368 // allocate a local using wasm's pointer size
1369 const local = try self.allocLocal(Type.@"usize");
1370 try self.moveStack(abi_size, local.local);
1371 return local;
1402 if (abi_align > self.stack_alignment) {
1403 self.stack_alignment = abi_align;
1404 }
1405
1406 const offset = std.mem.alignForwardGeneric(u32, self.stack_size, abi_align);
1407 defer self.stack_size = offset + abi_size;
1408
1409 // store the stack pointer and return a local to it
1410 return self.saveStack();
1411}
1412
1413/// From a given AIR instruction generates a pointer to the stack where
1414/// the value of its type will live.
1415/// This is different from allocStack where this will use the pointer's alignment
1416/// if it is set, to ensure the stack alignment will be set correctly.
1417fn allocStackPtr(self: *Self, inst: Air.Inst.Index) !WValue {
1418 const ptr_ty = self.air.typeOfIndex(inst);
1419 const pointee_ty = ptr_ty.childType();
1420
1421 if (self.initial_stack_value == .none) {
1422 try self.initializeStack();
1423 }
1424
1425 if (!pointee_ty.hasRuntimeBits()) {
1426 return self.allocStack(Type.usize); // create a value containing just the stack pointer.
1427 }
1428
1429 const abi_alignment = ptr_ty.ptrAlignment(self.target);
1430 const abi_size = std.math.cast(u32, pointee_ty.abiSize(self.target)) catch {
1431 return self.fail("Type {} with ABI size of {d} exceeds stack frame size", .{ pointee_ty, pointee_ty.abiSize(self.target) });
1432 };
1433 if (abi_alignment > self.stack_alignment) {
1434 self.stack_alignment = abi_alignment;
1435 }
1436
1437 const offset = std.mem.alignForwardGeneric(u32, self.stack_size, abi_alignment);
1438 defer self.stack_size = offset + abi_size;
1439
1440 // store the stack pointer and return a local to it
1441 return self.saveStack();
13721442}
13731443
13741444/// From given zig bitsize, returns the wasm bitsize
......@@ -1667,12 +1737,7 @@ fn airRetPtr(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
16671737 if (isByRef(child_type, self.target)) {
16681738 return self.return_value;
16691739 }
1670
1671 // Initialize the stack
1672 if (self.initial_stack_value == .none) {
1673 try self.initializeStack();
1674 }
1675 return self.allocStack(child_type);
1740 return self.allocStackPtr(inst);
16761741}
16771742
16781743fn airRetLoad(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
......@@ -1764,20 +1829,7 @@ fn airCall(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
17641829}
17651830
17661831fn airAlloc(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
1767 const pointee_type = self.air.typeOfIndex(inst).childType();
1768
1769 // Initialize the stack
1770 if (self.initial_stack_value == .none) {
1771 try self.initializeStack();
1772 }
1773
1774 if (!pointee_type.hasRuntimeBits()) {
1775 // when the pointee is zero-sized, we still want to create a pointer.
1776 // but instead use a default pointer type as storage.
1777 const zero_ptr = try self.allocStack(Type.usize);
1778 return zero_ptr;
1779 }
1780 return self.allocStack(pointee_type);
1832 return self.allocStackPtr(inst);
17811833}
17821834
17831835fn airStore(self: *Self, inst: Air.Inst.Index) InnerError!WValue {