diff --git a/src/link/SpirV.zig b/src/link/SpirV.zig index 4dddb7bd0d4b1229ee93aa7f7232b7c39530708c..27172c3fd30cc85fd23ada0b5c6af00dd9978659 100644 --- a/src/link/SpirV.zig +++ b/src/link/SpirV.zig @@ -245,26 +245,31 @@ pub fn flushModule(self: *SpirV, arena: Allocator, prog_node: *std.Progress.Node const module = try spv.finalize(arena, target); errdefer arena.free(module); - const new_module = self.lowerInstanceGlobals(arena, module) catch |err| switch (err) { + const linked_module = self.linkModule(arena, module) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => |other| { - std.debug.print("error while lowering instance globals: {s}\n", .{@errorName(other)}); + log.err("error while linking: {s}\n", .{@errorName(other)}); return error.FlushFailure; }, }; - defer arena.free(new_module); - try self.base.file.?.writeAll(std.mem.sliceAsBytes(new_module)); + try self.base.file.?.writeAll(std.mem.sliceAsBytes(linked_module)); } -fn lowerInstanceGlobals(self: *SpirV, a: Allocator, module: []Word) ![]Word { +fn linkModule(self: *SpirV, a: Allocator, module: []Word) ![]Word { _ = self; + const lower_invocation_globals = @import("SpirV/lower_invocation_globals.zig"); + const prune_unused = @import("SpirV/prune_unused.zig"); + var parser = try BinaryModule.Parser.init(a); defer parser.deinit(); - const binary = try parser.parse(module); - const lower_invocation_globals = @import("SpirV/lower_invocation_globals.zig"); - return try lower_invocation_globals.run(&parser, binary); + var binary = try parser.parse(module); + + try lower_invocation_globals.run(&parser, &binary); + try prune_unused.run(&parser, &binary); + + return binary.finalize(a); } fn writeCapabilities(spv: *SpvModule, target: std.Target) !void { diff --git a/src/link/SpirV/BinaryModule.zig b/src/link/SpirV/BinaryModule.zig index 50cadf2902b86d440aa8b23796edb077137c0bf9..0c9c32c98e04f406a3e86134a98503fe28dc489e 100644 --- a/src/link/SpirV/BinaryModule.zig +++ b/src/link/SpirV/BinaryModule.zig @@ -45,11 +45,30 @@ pub fn deinit(self: *BinaryModule, a: Allocator) void { } pub fn iterateInstructions(self: BinaryModule) Instruction.Iterator { - return Instruction.Iterator.init(self.instructions); + return Instruction.Iterator.init(self.instructions, 0); } pub fn iterateInstructionsFrom(self: BinaryModule, offset: usize) Instruction.Iterator { - return Instruction.Iterator.init(self.instructions[offset..]); + return Instruction.Iterator.init(self.instructions, offset); +} + +pub fn instructionAt(self: BinaryModule, offset: usize) Instruction { + var it = self.iterateInstructionsFrom(offset); + return it.next().?; +} + +pub fn finalize(self: BinaryModule, a: Allocator) ![]Word { + const result = try a.alloc(Word, 5 + self.instructions.len); + errdefer a.free(result); + + result[0] = spec.magic_number; + result[1] = @bitCast(self.version); + result[2] = spec.zig_generator_id; + result[3] = self.id_bound; + result[4] = 0; // Schema + + @memcpy(result[5..], self.instructions); + return result; } /// Errors that can be raised when the module is not correct. @@ -85,8 +104,8 @@ pub const Instruction = struct { index: usize = 0, offset: usize = 0, - pub fn init(words: []const Word) Iterator { - return .{ .words = words }; + pub fn init(words: []const Word, start_offset: usize) Iterator { + return .{ .words = words, .offset = start_offset }; } pub fn next(self: *Iterator) ?Instruction { @@ -159,6 +178,11 @@ pub const Parser = struct { return (@as(u32, @intFromEnum(set)) << 16) | opcode; } + pub fn getInstSpec(self: Parser, opcode: Opcode) ?spec.Instruction { + const index = self.opcode_table.get(mapSetAndOpcode(.core, @intFromEnum(opcode))) orelse return null; + return InstructionSet.core.instructions()[index]; + } + pub fn parse(self: *Parser, module: []const u32) ParseError!BinaryModule { if (module[0] != spec.magic_number) { return error.InvalidMagic; @@ -195,13 +219,12 @@ pub const Parser = struct { // We can't really efficiently use non-exhaustive enums here, because we would // need to manually write out all valid cases. Since we have this map anyway, just // use that. - const opcode_num: u16 = @truncate(binary.instructions[offset]); - const index = self.opcode_table.get(mapSetAndOpcode(.core, opcode_num)) orelse { - log.err("invalid opcode for core set: {}", .{opcode_num}); + const opcode: Opcode = @enumFromInt(@as(u16, @truncate(binary.instructions[offset]))); + const inst_spec = self.getInstSpec(opcode) orelse { + log.err("invalid opcode for core set: {}", .{@intFromEnum(opcode)}); return error.InvalidOpcode; }; - const opcode: Opcode = @enumFromInt(opcode_num); const operands = binary.instructions[offset..][1..len]; switch (opcode) { .OpExtInstImport => { @@ -226,11 +249,10 @@ pub const Parser = struct { // OpSwitch takes a value as argument, not an OpType... hence we need to populate arith_type_width // with ALL operations that return an int or float. - const proper_operands = InstructionSet.core.instructions()[index].operands; - - if (proper_operands.len >= 2 and - proper_operands[0].kind == .IdResultType and - proper_operands[1].kind == .IdResult) + const spec_operands = inst_spec.operands; + if (spec_operands.len >= 2 and + spec_operands[0].kind == .IdResultType and + spec_operands[1].kind == .IdResult) { if (operands.len < 2) return error.InvalidOperands; if (binary.arith_type_width.get(@enumFromInt(operands[0]))) |width| { @@ -283,6 +305,7 @@ pub const Parser = struct { if (offset + 1 >= inst.operands.len) return error.InvalidPhysicalFormat; const set_id: ResultId = @enumFromInt(inst.operands[offset]); + try offsets.append(@intCast(offset)); const set = binary.ext_inst_map.get(set_id) orelse { log.err("invalid instruction set {}", .{@intFromEnum(set_id)}); return error.InvalidId; @@ -375,8 +398,7 @@ pub const Parser = struct { } }, .id => { - const this_offset = std.math.cast(u16, offset) orelse return error.InvalidPhysicalFormat; - try offsets.append(this_offset); + try offsets.append(@intCast(offset)); offset += 1; }, else => switch (kind) { @@ -419,20 +441,16 @@ pub const Parser = struct { 33...64 => 2, else => unreachable, }; - const this_offset = std.math.cast(u16, offset) orelse return error.InvalidPhysicalFormat; - try offsets.append(this_offset); + try offsets.append(@intCast(offset)); offset += 1; }, .PairIdRefLiteralInteger => { - const this_offset = std.math.cast(u16, offset) orelse return error.InvalidPhysicalFormat; - try offsets.append(this_offset); + try offsets.append(@intCast(offset)); offset += 2; }, .PairIdRefIdRef => { - const a = std.math.cast(u16, offset) orelse return error.InvalidPhysicalFormat; - const b = std.math.cast(u16, offset + 1) orelse return error.InvalidPhysicalFormat; - try offsets.append(a); - try offsets.append(b); + try offsets.append(@intCast(offset)); + try offsets.append(@intCast(offset + 1)); offset += 2; }, else => unreachable, diff --git a/src/link/SpirV/lower_invocation_globals.zig b/src/link/SpirV/lower_invocation_globals.zig index 6457f521d2c9db6216c243dc70beafd7887e512f..e6de48ff93e08dfe833319b33d10960a9382f83c 100644 --- a/src/link/SpirV/lower_invocation_globals.zig +++ b/src/link/SpirV/lower_invocation_globals.zig @@ -345,23 +345,16 @@ const ModuleBuilder = struct { function_types: std.ArrayHashMapUnmanaged(FunctionType, ResultId, FunctionType.Context, true) = .{}, /// Maps functions to new information required for creating the module function_new_info: std.AutoArrayHashMapUnmanaged(ResultId, FunctionNewInfo) = .{}, + /// Offset of the functions section in the new binary. + new_functions_section: ?usize, fn init(arena: Allocator, binary: BinaryModule, info: ModuleInfo) !ModuleBuilder { - var section = Section{}; - - try section.instructions.appendSlice(arena, &.{ - spec.magic_number, - @bitCast(binary.version), - spec.zig_generator_id, - 0, // Filled in in finalize() - 0, // Schema (reserved) - }); - var self = ModuleBuilder{ .arena = arena, - .section = section, + .section = .{}, .id_bound = binary.id_bound, .entry_point_new_id_base = undefined, + .new_functions_section = null, }; self.entry_point_new_id_base = @intFromEnum(self.allocIds(@intCast(info.entry_points.count()))); return self; @@ -376,9 +369,12 @@ const ModuleBuilder = struct { return @enumFromInt(self.id_bound); } - fn finalize(self: *ModuleBuilder, a: Allocator) ![]Word { - self.section.instructions.items[3] = self.id_bound; - return try a.dupe(Word, self.section.instructions.items); + fn finalize(self: *ModuleBuilder, a: Allocator, binary: *BinaryModule) !void { + binary.id_bound = self.id_bound; + binary.instructions = try a.dupe(Word, self.section.instructions.items); + // Nothing is removed in this pass so we don't need to change any of the maps, + // just make sure the section is updated. + binary.sections.functions = self.new_functions_section orelse binary.instructions.len; } /// Process everything from `binary` up to the first function and emit it into the builder. @@ -386,16 +382,6 @@ const ModuleBuilder = struct { var it = binary.iterateInstructions(); while (it.next()) |inst| { switch (inst.opcode) { - // TODO: We should remove this instruction using something that eliminates unreferenced instructions. - // For now, this is the only place where the .zig instruction set is being referenced, so its safe - // to remove it here. - .OpExtInstImport => { - const set_id: ResultId = @enumFromInt(inst.operands[0]); - const set = binary.ext_inst_map.get(set_id).?; - if (set == .zig) { - continue; - } - }, .OpExtInst => { const set_id: ResultId = @enumFromInt(inst.operands[2]); const set_inst = inst.operands[3]; @@ -499,6 +485,7 @@ const ModuleBuilder = struct { var maybe_current_function: ?ResultId = null; var it = binary.iterateInstructionsFrom(binary.sections.functions); + self.new_functions_section = self.section.instructions.items.len; while (it.next()) |inst| { result_id_offsets.items.len = 0; try parser.parseInstructionResultIds(binary, inst, &result_id_offsets); @@ -695,20 +682,19 @@ const ModuleBuilder = struct { } }; -pub fn run(parser: *BinaryModule.Parser, binary: BinaryModule) ![]Word { +pub fn run(parser: *BinaryModule.Parser, binary: *BinaryModule) !void { var arena = std.heap.ArenaAllocator.init(parser.a); defer arena.deinit(); const a = arena.allocator(); - var info = try ModuleInfo.parse(a, parser, binary); + var info = try ModuleInfo.parse(a, parser, binary.*); try info.resolve(a); - var builder = try ModuleBuilder.init(a, binary, info); + var builder = try ModuleBuilder.init(a, binary.*, info); try builder.deriveNewFnInfo(info); - try builder.processPreamble(binary, info); + try builder.processPreamble(binary.*, info); try builder.emitFunctionTypes(info); - try builder.rewriteFunctions(parser, binary, info); + try builder.rewriteFunctions(parser, binary.*, info); try builder.emitNewEntryPoints(info); - - return builder.finalize(parser.a); + try builder.finalize(parser.a, binary); } diff --git a/src/link/SpirV/prune_unused.zig b/src/link/SpirV/prune_unused.zig new file mode 100644 index 0000000000000000000000000000000000000000..a6d0ceaea7ccd04662a3369d1a0cdb2d62ea398e --- /dev/null +++ b/src/link/SpirV/prune_unused.zig @@ -0,0 +1,354 @@ +//! This pass is used to simple pruning of unused things: +//! - Instructions at global scope +//! - Functions +//! Debug info and nonsemantic instructions are not handled; +//! this pass is mainly intended for cleaning up left over +//! stuff from codegen and other passes that is generated +//! but not actually used. + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const log = std.log.scoped(.spirv_link); + +const BinaryModule = @import("BinaryModule.zig"); +const Section = @import("../../codegen/spirv/Section.zig"); +const spec = @import("../../codegen/spirv/spec.zig"); +const Opcode = spec.Opcode; +const ResultId = spec.IdResult; +const Word = spec.Word; + +/// Return whether a particular opcode's instruction can be pruned. +/// These are idempotent instructions at globals scope and instructions +/// within functions that do not have any side effects. +/// The opcodes that return true here do not necessarily need to +/// have an .IdResult. If they don't, then they are regarded +/// as 'decoration'-style instructions that don't keep their +/// operands alive, but will be emitted if they are. +fn canPrune(op: Opcode) bool { + // This list should be as worked out as possible, but just + // getting common instructions is a good effort/effect ratio. + // When adding items to this list, also check whether the + // instruction requires any special control flow rules (like + // with labels and control flow and stuff) and whether the + // instruction has any non-trivial side effects (like OpLoad + // with the Volatile memory semantics). + return switch (op.class()) { + .TypeDeclaration, + .Conversion, + .Arithmetic, + .RelationalAndLogical, + .Bit, + => true, + else => switch (op) { + .OpFunction, + .OpUndef, + .OpString, + .OpName, + .OpMemberName, + // Prune OpConstant* instructions but + // retain OpSpecConstant declaration instructions + .OpConstantTrue, + .OpConstantFalse, + .OpConstant, + .OpConstantComposite, + .OpConstantSampler, + .OpConstantNull, + .OpSpecConstantOp, + // Prune ext inst import instructions, but not + // ext inst instructions themselves, because + // we don't know if they might have side effects. + .OpExtInstImport, + => true, + else => false, + }, + }; +} + +const ModuleInfo = struct { + const Fn = struct { + /// The index of the first callee in `callee_store`. + first_callee: usize, + }; + + /// Maps function result-id -> Fn information structure. + functions: std.AutoArrayHashMapUnmanaged(ResultId, Fn), + /// For each function, a list of function result-ids that it calls. + callee_store: []const ResultId, + /// For each instruction, the offset at which it appears in the source module. + result_id_to_code_offset: std.AutoArrayHashMapUnmanaged(ResultId, usize), + + /// Fetch the list of callees per function. Guaranteed to contain only unique IDs. + fn callees(self: ModuleInfo, fn_id: ResultId) []const ResultId { + const fn_index = self.functions.getIndex(fn_id).?; + const values = self.functions.values(); + const first_callee = values[fn_index].first_callee; + if (fn_index == values.len - 1) { + return self.callee_store[first_callee..]; + } else { + const next_first_callee = values[fn_index + 1].first_callee; + return self.callee_store[first_callee..next_first_callee]; + } + } + + /// Extract the information required to run this pass from the binary. + // TODO: Should the contents of this function be merged with that of lower_invocation_globals.zig? + // Many of the contents are the same... + fn parse( + arena: Allocator, + parser: *BinaryModule.Parser, + binary: BinaryModule, + ) !ModuleInfo { + var functions = std.AutoArrayHashMap(ResultId, Fn).init(arena); + var calls = std.AutoArrayHashMap(ResultId, void).init(arena); + var callee_store = std.ArrayList(ResultId).init(arena); + var result_id_to_code_offset = std.AutoArrayHashMap(ResultId, usize).init(arena); + var maybe_current_function: ?ResultId = null; + var it = binary.iterateInstructions(); + while (it.next()) |inst| { + const inst_spec = parser.getInstSpec(inst.opcode).?; + + // Result-id can only be the first or second operand + const maybe_result_id: ?ResultId = for (0..2) |i| { + if (inst_spec.operands.len > i and inst_spec.operands[i].kind == .IdResult) { + break @enumFromInt(inst.operands[i]); + } + } else null; + + // Only add result-ids of functions and anything outside a function. + // Result-ids declared inside functions cannot be reached outside anyway, + // and we don't care about the internals of functions anyway. + // Note that in the case of OpFunction, `maybe_current_function` is + // also `null`, because it is set below. + if (maybe_result_id) |result_id| { + try result_id_to_code_offset.put(result_id, inst.offset); + } + + switch (inst.opcode) { + .OpFunction => { + if (maybe_current_function) |current_function| { + log.err("OpFunction {} does not have an OpFunctionEnd", .{current_function}); + return error.InvalidPhysicalFormat; + } + + maybe_current_function = @enumFromInt(inst.operands[1]); + }, + .OpFunctionCall => { + const callee: ResultId = @enumFromInt(inst.operands[2]); + try calls.put(callee, {}); + }, + .OpFunctionEnd => { + const current_function = maybe_current_function orelse { + log.err("encountered OpFunctionEnd without corresponding OpFunction", .{}); + return error.InvalidPhysicalFormat; + }; + const entry = try functions.getOrPut(current_function); + if (entry.found_existing) { + log.err("Function {} has duplicate definition", .{current_function}); + return error.DuplicateId; + } + + const first_callee = callee_store.items.len; + try callee_store.appendSlice(calls.keys()); + + entry.value_ptr.* = .{ + .first_callee = first_callee, + }; + maybe_current_function = null; + calls.clearRetainingCapacity(); + }, + else => {}, + } + } + + if (maybe_current_function) |current_function| { + log.err("OpFunction {} does not have an OpFunctionEnd", .{current_function}); + return error.InvalidPhysicalFormat; + } + + return ModuleInfo{ + .functions = functions.unmanaged, + .callee_store = callee_store.items, + .result_id_to_code_offset = result_id_to_code_offset.unmanaged, + }; + } +}; + +const AliveMarker = struct { + parser: *BinaryModule.Parser, + binary: BinaryModule, + info: ModuleInfo, + result_id_offsets: std.ArrayList(u16), + alive: std.DynamicBitSetUnmanaged, + + fn markAlive(self: *AliveMarker, result_id: ResultId) BinaryModule.ParseError!void { + const index = self.info.result_id_to_code_offset.getIndex(result_id) orelse { + log.err("undefined result-id {}", .{result_id}); + return error.InvalidId; + }; + + if (self.alive.isSet(index)) { + return; + } + self.alive.set(index); + + const offset = self.info.result_id_to_code_offset.values()[index]; + const inst = self.binary.instructionAt(offset); + + if (inst.opcode == .OpFunction) { + try self.markFunctionAlive(inst); + } else { + try self.markInstructionAlive(inst); + } + } + + fn markFunctionAlive( + self: *AliveMarker, + func_inst: BinaryModule.Instruction, + ) !void { + // Go through the instruction and mark the + // operands of each instruction alive. + var it = self.binary.iterateInstructionsFrom(func_inst.offset); + try self.markInstructionAlive(it.next().?); + while (it.next()) |inst| { + if (inst.opcode == .OpFunctionEnd) { + break; + } + + if (!canPrune(inst.opcode)) { + try self.markInstructionAlive(inst); + } + } + } + + fn markInstructionAlive( + self: *AliveMarker, + inst: BinaryModule.Instruction, + ) !void { + const start_offset = self.result_id_offsets.items.len; + try self.parser.parseInstructionResultIds(self.binary, inst, &self.result_id_offsets); + const end_offset = self.result_id_offsets.items.len; + + // Recursive calls to markInstructionAlive() might change the pointer in self.result_id_offsets, + // so we need to iterate it manually. + var i = start_offset; + while (i < end_offset) : (i += 1) { + const offset = self.result_id_offsets.items[i]; + try self.markAlive(@enumFromInt(inst.operands[offset])); + } + } +}; + +fn removeIdsFromMap(a: Allocator, map: anytype, info: ModuleInfo, alive_marker: AliveMarker) !void { + var to_remove = std.ArrayList(ResultId).init(a); + var it = map.iterator(); + while (it.next()) |entry| { + const id = entry.key_ptr.*; + const index = info.result_id_to_code_offset.getIndex(id).?; + if (!alive_marker.alive.isSet(index)) { + try to_remove.append(id); + } + } + + for (to_remove.items) |id| { + assert(map.remove(id)); + } +} + +pub fn run(parser: *BinaryModule.Parser, binary: *BinaryModule) !void { + var arena = std.heap.ArenaAllocator.init(parser.a); + defer arena.deinit(); + const a = arena.allocator(); + + const info = try ModuleInfo.parse(a, parser, binary.*); + + var alive_marker = AliveMarker{ + .parser = parser, + .binary = binary.*, + .info = info, + .result_id_offsets = std.ArrayList(u16).init(a), + .alive = try std.DynamicBitSetUnmanaged.initEmpty(a, info.result_id_to_code_offset.count()), + }; + + // Mark initial stuff as slive + { + var it = binary.iterateInstructions(); + while (it.next()) |inst| { + if (inst.opcode == .OpFunction) { + // No need to process further. + break; + } else if (!canPrune(inst.opcode)) { + try alive_marker.markInstructionAlive(inst); + } + } + } + + var section = Section{}; + + var new_functions_section: ?usize = null; + var it = binary.iterateInstructions(); + skip: while (it.next()) |inst| { + const inst_spec = parser.getInstSpec(inst.opcode).?; + + reemit: { + if (!canPrune(inst.opcode)) { + break :reemit; + } + + // Result-id can only be the first or second operand + const result_id: ResultId = for (0..2) |i| { + if (inst_spec.operands.len > i and inst_spec.operands[i].kind == .IdResult) { + break @enumFromInt(inst.operands[i]); + } + } else { + // Instruction can be pruned but doesn't have a result id. + // Check all operands to see if they are alive, and emit it only if so. + alive_marker.result_id_offsets.items.len = 0; + try parser.parseInstructionResultIds(binary.*, inst, &alive_marker.result_id_offsets); + for (alive_marker.result_id_offsets.items) |offset| { + const id: ResultId = @enumFromInt(inst.operands[offset]); + const index = info.result_id_to_code_offset.getIndex(id).?; + + if (!alive_marker.alive.isSet(index)) { + continue :skip; + } + } + + break :reemit; + }; + + const index = info.result_id_to_code_offset.getIndex(result_id).?; + if (alive_marker.alive.isSet(index)) { + break :reemit; + } + + if (inst.opcode != .OpFunction) { + // Instruction can be pruned and its not alive, so skip it. + continue :skip; + } + + // We're at the start of a function that can be pruned, so skip everything until + // we encounter an OpFunctionEnd. + while (it.next()) |body_inst| { + if (body_inst.opcode == .OpFunctionEnd) + break; + } + + continue :skip; + } + + if (inst.opcode == .OpFunction and new_functions_section == null) { + new_functions_section = section.instructions.items.len; + } + + try section.emitRawInstruction(a, inst.opcode, inst.operands); + } + + // This pass might have pruned ext inst imports or arith types, update + // those maps to main consistency. + try removeIdsFromMap(a, &binary.ext_inst_map, info, alive_marker); + try removeIdsFromMap(a, &binary.arith_type_width, info, alive_marker); + + binary.instructions = try parser.a.dupe(Word, section.toWords()); + binary.sections.functions = new_functions_section orelse binary.instructions.len; +} diff --git a/test/behavior/align.zig b/test/behavior/align.zig index e00fbe67443f0bf13281cc15d06f69653beaead3..32dedc93fa13d8039ec9a81fe8f802f7b61f8016 100644 --- a/test/behavior/align.zig +++ b/test/behavior/align.zig @@ -18,6 +18,7 @@ test "global variable alignment" { test "large alignment of local constant" { if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; + if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest; // flaky const x: f32 align(128) = 12.34; try std.testing.expect(@intFromPtr(&x) % 128 == 0);