authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2020-06-29 21:58:34-04:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2020-07-05 23:20:08+00:00
log8fb392dbb443688cdf62e965935e17a4ae4a4267
tree1f4c24d9f1b71a20e43984cd9a70c823f66ed716
parentabcd4ea5d88541a21062e63fe427b2d2ea16a041

stage2: implement liveness analysis


4 files changed, 207 insertions(+), 11 deletions(-)

src-self-hosted/Module.zig+6
...@@ -18,6 +18,7 @@ const Inst = ir.Inst;...@@ -18,6 +18,7 @@ const Inst = ir.Inst;
18const Body = ir.Body;18const Body = ir.Body;
19const ast = std.zig.ast;19const ast = std.zig.ast;
20const trace = @import("tracy.zig").trace;20const trace = @import("tracy.zig").trace;
21const liveness = @import("liveness.zig");
2122
22/// General-purpose allocator.23/// General-purpose allocator.
23allocator: *Allocator,24allocator: *Allocator,
...@@ -986,6 +987,11 @@ pub fn performAllTheWork(self: *Module) error{OutOfMemory}!void {...@@ -986,6 +987,11 @@ pub fn performAllTheWork(self: *Module) error{OutOfMemory}!void {
986 .sema_failure, .dependency_failure => continue,987 .sema_failure, .dependency_failure => continue,
987 .success => {},988 .success => {},
988 }989 }
990 // Here we tack on additional allocations to the Decl's arena. The allocations are
991 // lifetime annotations in the ZIR.
992 var decl_arena = decl.typed_value.most_recent.arena.?.promote(self.allocator);
993 defer decl.typed_value.most_recent.arena.?.* = decl_arena.state;
994 try liveness.analyze(self.allocator, &decl_arena.allocator, payload.func.analysis.success);
989 }995 }
990996
991 assert(decl.typed_value.most_recent.typed_value.ty.hasCodeGenBits());997 assert(decl.typed_value.most_recent.typed_value.ty.hasCodeGenBits());
src-self-hosted/codegen.zig+50-10
...@@ -104,11 +104,18 @@ pub fn generateSymbol(...@@ -104,11 +104,18 @@ pub fn generateSymbol(
104 .bin_file = bin_file,104 .bin_file = bin_file,
105 .mod_fn = module_fn,105 .mod_fn = module_fn,
106 .code = code,106 .code = code,
107 .inst_table = std.AutoHashMap(*ir.Inst, Function.MCValue).init(bin_file.allocator),
108 .err_msg = null,107 .err_msg = null,
109 .args = mc_args.items,108 .args = mc_args.items,
109 .branch_stack = .{},
110 };110 };
111 defer function.inst_table.deinit();111 defer {
112 assert(function.branch_stack.items.len == 1);
113 function.branch_stack.items[0].inst_table.deinit();
114 function.branch_stack.deinit(bin_file.allocator);
115 }
116 try function.branch_stack.append(bin_file.allocator, .{
117 .inst_table = std.AutoHashMap(*ir.Inst, Function.MCValue).init(bin_file.allocator),
118 });
112119
113 function.gen() catch |err| switch (err) {120 function.gen() catch |err| switch (err) {
114 error.CodegenFail => return Result{ .fail = function.err_msg.? },121 error.CodegenFail => return Result{ .fail = function.err_msg.? },
...@@ -215,13 +222,29 @@ const Function = struct {...@@ -215,13 +222,29 @@ const Function = struct {
215 target: *const std.Target,222 target: *const std.Target,
216 mod_fn: *const Module.Fn,223 mod_fn: *const Module.Fn,
217 code: *std.ArrayList(u8),224 code: *std.ArrayList(u8),
218 inst_table: std.AutoHashMap(*ir.Inst, MCValue),
219 err_msg: ?*ErrorMsg,225 err_msg: ?*ErrorMsg,
220 args: []MCValue,226 args: []MCValue,
221227
228 /// Whenever there is a runtime branch, we push a Branch onto this stack,
229 /// and pop it off when the runtime branch joins. This provides an "overlay"
230 /// of the table of mappings from instructions to `MCValue` from within the branch.
231 /// This way we can modify the `MCValue` for an instruction in different ways
232 /// within different branches. Special consideration is needed when a branch
233 /// joins with its parent, to make sure all instructions have the same MCValue
234 /// across each runtime branch upon joining.
235 branch_stack: std.ArrayListUnmanaged(Branch),
236
237 const Branch = struct {
238 inst_table: std.AutoHashMap(*ir.Inst, MCValue),
239 };
240
222 const MCValue = union(enum) {241 const MCValue = union(enum) {
242 /// No runtime bits. `void` types, empty structs, u0, enums with 1 tag, etc.
223 none,243 none,
244 /// Control flow will not allow this value to be observed.
224 unreach,245 unreach,
246 /// No more references to this value remain.
247 dead,
225 /// A pointer-sized integer that fits in a register.248 /// A pointer-sized integer that fits in a register.
226 immediate: u64,249 immediate: u64,
227 /// The constant was emitted into the code, at this offset.250 /// The constant was emitted into the code, at this offset.
...@@ -292,9 +315,10 @@ const Function = struct {...@@ -292,9 +315,10 @@ const Function = struct {
292 }315 }
293316
294 fn genArch(self: *Function, comptime arch: std.Target.Cpu.Arch) !void {317 fn genArch(self: *Function, comptime arch: std.Target.Cpu.Arch) !void {
318 const inst_table = &self.branch_stack.items[0].inst_table;
295 for (self.mod_fn.analysis.success.instructions) |inst| {319 for (self.mod_fn.analysis.success.instructions) |inst| {
296 const new_inst = try self.genFuncInst(inst, arch);320 const new_inst = try self.genFuncInst(inst, arch);
297 try self.inst_table.putNoClobber(inst, new_inst);321 try inst_table.putNoClobber(inst, new_inst);
298 }322 }
299 }323 }
300324
...@@ -525,7 +549,9 @@ const Function = struct {...@@ -525,7 +549,9 @@ const Function = struct {
525 fn genSetReg(self: *Function, src: usize, comptime arch: Target.Cpu.Arch, reg: Reg(arch), mcv: MCValue) error{ CodegenFail, OutOfMemory }!void {549 fn genSetReg(self: *Function, src: usize, comptime arch: Target.Cpu.Arch, reg: Reg(arch), mcv: MCValue) error{ CodegenFail, OutOfMemory }!void {
526 switch (arch) {550 switch (arch) {
527 .x86_64 => switch (mcv) {551 .x86_64 => switch (mcv) {
528 .none, .unreach => unreachable,552 .dead => unreachable,
553 .none => unreachable,
554 .unreach => unreachable,
529 .immediate => |x| {555 .immediate => |x| {
530 if (reg.size() != 64) {556 if (reg.size() != 64) {
531 return self.fail(src, "TODO decide whether to implement non-64-bit loads", .{});557 return self.fail(src, "TODO decide whether to implement non-64-bit loads", .{});
...@@ -708,12 +734,26 @@ const Function = struct {...@@ -708,12 +734,26 @@ const Function = struct {
708 if (self.inst_table.get(inst)) |mcv| {734 if (self.inst_table.get(inst)) |mcv| {
709 return mcv;735 return mcv;
710 }736 }
737 // Constants have static lifetimes, so they are always memoized in the outer most table.
711 if (inst.cast(ir.Inst.Constant)) |const_inst| {738 if (inst.cast(ir.Inst.Constant)) |const_inst| {
712 const mcvalue = try self.genTypedValue(inst.src, .{ .ty = inst.ty, .val = const_inst.val });739 const branch = &self.branch_stack.items[0];
713 try self.inst_table.putNoClobber(inst, mcvalue);740 const gop = try branch.inst_table.getOrPut(inst);
714 return mcvalue;741 if (!gop.found_existing) {
715 } else {742 const mcv = try self.genTypedValue(inst.src, .{ .ty = inst.ty, .val = const_inst.val });
716 return self.inst_table.get(inst).?;743 try branch.inst_table.putNoClobber(inst, mcv);
744 gop.kv.value = mcv;
745 return mcv;
746 }
747 return gop.kv.value;
748 }
749
750 // Treat each stack item as a "layer" on top of the previous one.
751 var i: usize = self.branch_stack.items.len;
752 while (true) {
753 i -= 1;
754 if (self.branch_stack.items[i].inst_table.getValue(inst)) |mcv| {
755 return mcv;
756 }
717 }757 }
718 }758 }
719759
src-self-hosted/ir.zig+17-1
...@@ -10,6 +10,16 @@ const Module = @import("Module.zig");...@@ -10,6 +10,16 @@ const Module = @import("Module.zig");
10/// a memory location for the value to survive after a const instruction.10/// a memory location for the value to survive after a const instruction.
11pub const Inst = struct {11pub const Inst = struct {
12 tag: Tag,12 tag: Tag,
13 /// Each bit represents the index of an `Inst` parameter in the `args` field.
14 /// If a bit is set, it marks the end of the lifetime of the corresponding
15 /// instruction parameter. For example, 0b00000101 means that the first and
16 /// third `Inst` parameters' lifetimes end after this instruction, and will
17 /// not have any more following references.
18 /// The most significant bit being set means that the instruction itself is
19 /// never referenced, in other words its lifetime ends as soon as it finishes.
20 /// If the byte is `0xff`, it means this is a special case and this data is
21 /// encoded elsewhere.
22 deaths: u8 = 0xff,
13 ty: Type,23 ty: Type,
14 /// Byte offset into the source.24 /// Byte offset into the source.
15 src: usize,25 src: usize,
...@@ -165,6 +175,12 @@ pub const Inst = struct {...@@ -165,6 +175,12 @@ pub const Inst = struct {
165 true_body: Body,175 true_body: Body,
166 false_body: Body,176 false_body: Body,
167 },177 },
178 /// Set of instructions whose lifetimes end at the start of one of the branches.
179 /// The `true` branch is first: `deaths[0..true_death_count]`.
180 /// The `false` branch is next: `(deaths + true_death_count)[..false_death_count]`.
181 deaths: [*]*Inst = undefined,
182 true_death_count: u32 = 0,
183 false_death_count: u32 = 0,
168 };184 };
169185
170 pub const Constant = struct {186 pub const Constant = struct {
...@@ -224,4 +240,4 @@ pub const Inst = struct {...@@ -224,4 +240,4 @@ pub const Inst = struct {
224240
225pub const Body = struct {241pub const Body = struct {
226 instructions: []*Inst,242 instructions: []*Inst,
227};243};
\ No newline at end of file
src-self-hosted/liveness.zig created+134
...@@ -0,0 +1,134 @@
1const std = @import("std");
2const ir = @import("ir.zig");
3const trace = @import("tracy.zig").trace;
4
5/// Perform Liveness Analysis over the `Body`. Each `Inst` will have its `deaths` field populated.
6pub fn analyze(
7 /// Used for temporary storage during the analysis.
8 gpa: *std.mem.Allocator,
9 /// Used to tack on extra allocations in the same lifetime as the existing instructions.
10 arena: *std.mem.Allocator,
11 body: ir.Body,
12) error{OutOfMemory}!void {
13 const tracy = trace(@src());
14 defer tracy.end();
15
16 var table = std.AutoHashMap(*ir.Inst, void).init(gpa);
17 defer table.deinit();
18 try table.ensureCapacity(body.instructions.len);
19 try analyzeWithTable(arena, &table, body);
20}
21
22fn analyzeWithTable(arena: *std.mem.Allocator, table: *std.AutoHashMap(*ir.Inst, void), body: ir.Body) error{OutOfMemory}!void {
23 var i: usize = body.instructions.len;
24
25 while (i != 0) {
26 i -= 1;
27 const base = body.instructions[i];
28
29 // Obtain the corresponding instruction type based on the tag type.
30 inline for (std.meta.declarations(ir.Inst)) |decl| {
31 switch (decl.data) {
32 .Type => |T| {
33 if (@hasDecl(T, "base_tag")) {
34 if (T.base_tag == base.tag) {
35 return analyzeInst(arena, table, T, @fieldParentPtr(T, "base", base));
36 }
37 }
38 },
39 else => continue,
40 }
41 }
42 unreachable;
43 }
44}
45
46fn analyzeInst(arena: *std.mem.Allocator, table: *std.AutoHashMap(*ir.Inst, void), comptime T: type, inst: *T) error{OutOfMemory}!void {
47 inst.base.deaths = 0;
48
49 switch (T) {
50 ir.Inst.Constant => return,
51 ir.Inst.Block => {
52 try analyzeWithTable(arena, table, inst.args.body);
53 // We let this continue so that it can possibly mark the block as
54 // unreferenced below.
55 },
56 ir.Inst.CondBr => {
57 var true_table = std.AutoHashMap(*ir.Inst, void).init(table.allocator);
58 defer true_table.deinit();
59 try true_table.ensureCapacity(inst.args.true_body.instructions.len);
60 try analyzeWithTable(arena, &true_table, inst.args.true_body);
61
62 var false_table = std.AutoHashMap(*ir.Inst, void).init(table.allocator);
63 defer false_table.deinit();
64 try false_table.ensureCapacity(inst.args.false_body.instructions.len);
65 try analyzeWithTable(arena, &false_table, inst.args.false_body);
66
67 // Each death that occurs inside one branch, but not the other, needs
68 // to be added as a death immediately upon entering the other branch.
69 // During the iteration of the table, we additionally propagate the
70 // deaths to the parent table.
71 var true_entry_deaths = std.ArrayList(*ir.Inst).init(table.allocator);
72 defer true_entry_deaths.deinit();
73 var false_entry_deaths = std.ArrayList(*ir.Inst).init(table.allocator);
74 defer false_entry_deaths.deinit();
75 {
76 var it = false_table.iterator();
77 while (it.next()) |entry| {
78 const false_death = entry.key;
79 if (!true_table.contains(false_death)) {
80 try true_entry_deaths.append(false_death);
81 // Here we are only adding to the parent table if the following iteration
82 // would miss it.
83 try table.putNoClobber(false_death, {});
84 }
85 }
86 }
87 {
88 var it = true_table.iterator();
89 while (it.next()) |entry| {
90 const true_death = entry.key;
91 try table.putNoClobber(true_death, {});
92 if (!false_table.contains(true_death)) {
93 try false_entry_deaths.append(true_death);
94 }
95 }
96 }
97 inst.true_death_count = std.math.cast(@TypeOf(inst.true_death_count), true_entry_deaths.items.len) catch return error.OutOfMemory;
98 inst.false_death_count = std.math.cast(@TypeOf(inst.false_death_count), false_entry_deaths.items.len) catch return error.OutOfMemory;
99 const allocated_slice = try arena.alloc(*ir.Inst, true_entry_deaths.items.len + false_entry_deaths.items.len);
100 inst.deaths = allocated_slice.ptr;
101
102 // Continue on with the instruction analysis. The following code will find the condition
103 // instruction, and the deaths flag for the CondBr instruction will indicate whether the
104 // condition's lifetime ends immediately before entering any branch.
105 },
106 else => {},
107 }
108
109 if (!table.contains(&inst.base)) {
110 // No tombstone for this instruction means it is never referenced,
111 // and its birth marks its own death. Very metal 🤘
112 inst.base.deaths |= 1 << 7;
113 }
114
115 const Args = ir.Inst.Args(T);
116 if (Args == void) {
117 return;
118 }
119
120 comptime var arg_index: usize = 0;
121 inline for (std.meta.fields(Args)) |field| {
122 if (field.field_type == *ir.Inst) {
123 if (arg_index >= 6) {
124 @compileError("out of bits to mark deaths of operands");
125 }
126 const prev = try table.put(@field(inst.args, field.name), {});
127 if (prev == null) {
128 // Death.
129 inst.base.deaths |= 1 << arg_index;
130 }
131 arg_index += 1;
132 }
133 }
134}
\ No newline at end of file