authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2021-07-10 16:24:35-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2021-07-20 12:18:14-07:00
log3c3abaf3907e344305620fb4565e7c1acb0a9c88
treed418d08645bf500bb30e3b3b388dbfa4a21e4aec
parent5d6f7b44c19b064a543b0c1eecb6ef5c671b612e

stage2: update liveness analysis to new AIR memory layout

It's pretty compact, with each AIR instruction only taking up 4 bits, plus a sparse table for special instructions such as conditional branch, switch branch, and function calls with more than 2 arguments.

5 files changed, 541 insertions(+), 378 deletions(-)

BRANCH_TODO-73
......@@ -57,79 +57,6 @@
5757 unreachable;
5858 }
5959
60 pub fn Type(tag: Tag) type {
61 return switch (tag) {
62 .alloc,
63 .retvoid,
64 .unreach,
65 .breakpoint,
66 => NoOp,
67
68 .ref,
69 .ret,
70 .bitcast,
71 .not,
72 .is_non_null,
73 .is_non_null_ptr,
74 .is_null,
75 .is_null_ptr,
76 .is_err,
77 .is_non_err,
78 .is_err_ptr,
79 .is_non_err_ptr,
80 .ptrtoint,
81 .floatcast,
82 .intcast,
83 .load,
84 .optional_payload,
85 .optional_payload_ptr,
86 .wrap_optional,
87 .unwrap_errunion_payload,
88 .unwrap_errunion_err,
89 .unwrap_errunion_payload_ptr,
90 .unwrap_errunion_err_ptr,
91 .wrap_errunion_payload,
92 .wrap_errunion_err,
93 => UnOp,
94
95 .add,
96 .addwrap,
97 .sub,
98 .subwrap,
99 .mul,
100 .mulwrap,
101 .div,
102 .cmp_lt,
103 .cmp_lte,
104 .cmp_eq,
105 .cmp_gte,
106 .cmp_gt,
107 .cmp_neq,
108 .store,
109 .bool_and,
110 .bool_or,
111 .bit_and,
112 .bit_or,
113 .xor,
114 => BinOp,
115
116 .arg => Arg,
117 .assembly => Assembly,
118 .block => Block,
119 .br => Br,
120 .br_block_flat => BrBlockFlat,
121 .br_void => BrVoid,
122 .call => Call,
123 .condbr => CondBr,
124 .constant => Constant,
125 .loop => Loop,
126 .varptr => VarPtr,
127 .struct_field_ptr => StructFieldPtr,
128 .switchbr => SwitchBr,
129 .dbg_stmt => DbgStmt,
130 };
131 }
132
13360 pub fn Args(comptime T: type) type {
13461 return std.meta.fieldInfo(T, .args).field_type;
13562 }
src/Air.zig+79-43
......@@ -10,10 +10,18 @@ const Air = @This();
1010
1111instructions: std.MultiArrayList(Inst).Slice,
1212/// The meaning of this data is determined by `Inst.Tag` value.
13/// The first few indexes are reserved. See `ExtraIndex` for the values.
1314extra: []u32,
1415values: []Value,
1516variables: []*Module.Var,
1617
18pub const ExtraIndex = enum(u32) {
19 /// Payload index of the main `Block` in the `extra` array.
20 main_block,
21
22 _,
23};
24
1725pub const Inst = struct {
1826 tag: Tag,
1927 data: Data,
......@@ -231,11 +239,25 @@ pub const Inst = struct {
231239 .neq => .cmp_neq,
232240 };
233241 }
242
243 pub fn toCmpOp(tag: Tag) ?std.math.CompareOperator {
244 return switch (tag) {
245 .cmp_lt => .lt,
246 .cmp_lte => .lte,
247 .cmp_eq => .eq,
248 .cmp_gte => .gte,
249 .cmp_gt => .gt,
250 .cmp_neq => .neq,
251 else => null,
252 };
253 }
234254 };
235255
236256 /// The position of an AIR instruction within the `Air` instructions array.
237257 pub const Index = u32;
238258
259 pub const Ref = @import("Zir.zig").Inst.Ref;
260
239261 /// All instructions have an 8-byte payload, which is contained within
240262 /// this union. `Tag` determines which union field is active, as well as
241263 /// how to interpret the data within.
......@@ -281,55 +303,69 @@ pub const Inst = struct {
281303 }
282304 }
283305 };
306};
284307
285 pub fn cmpOperator(base: *Inst) ?std.math.CompareOperator {
286 return switch (base.tag) {
287 .cmp_lt => .lt,
288 .cmp_lte => .lte,
289 .cmp_eq => .eq,
290 .cmp_gte => .gte,
291 .cmp_gt => .gt,
292 .cmp_neq => .neq,
293 else => null,
294 };
295 }
308/// Trailing is a list of instruction indexes for every `body_len`.
309pub const Block = struct {
310 body_len: u32,
311};
296312
297 /// Trailing is a list of instruction indexes for every `body_len`.
298 pub const Block = struct {
299 body_len: u32,
300 };
313/// Trailing is a list of `Ref` for every `args_len`.
314pub const Call = struct {
315 args_len: u32,
316};
301317
302 /// Trailing is a list of `Ref` for every `args_len`.
303 pub const Call = struct {
304 args_len: u32,
305 };
318/// This data is stored inside extra, with two sets of trailing `Ref`:
319/// * 0. the then body, according to `then_body_len`.
320/// * 1. the else body, according to `else_body_len`.
321pub const CondBr = struct {
322 then_body_len: u32,
323 else_body_len: u32,
324};
306325
307 /// This data is stored inside extra, with two sets of trailing `Ref`:
308 /// * 0. the then body, according to `then_body_len`.
309 /// * 1. the else body, according to `else_body_len`.
310 pub const CondBr = struct {
311 condition: Ref,
312 then_body_len: u32,
313 else_body_len: u32,
314 };
326/// Trailing:
327/// * 0. `Case` for each `cases_len`
328/// * 1. the else body, according to `else_body_len`.
329pub const SwitchBr = struct {
330 cases_len: u32,
331 else_body_len: u32,
315332
316333 /// Trailing:
317 /// * 0. `Case` for each `cases_len`
318 /// * 1. the else body, according to `else_body_len`.
319 pub const SwitchBr = struct {
320 cases_len: u32,
321 else_body_len: u32,
322
323 /// Trailing:
324 /// * instruction index for each `body_len`.
325 pub const Case = struct {
326 item: Ref,
327 body_len: u32,
328 };
334 /// * instruction index for each `body_len`.
335 pub const Case = struct {
336 item: Ref,
337 body_len: u32,
329338 };
339};
330340
331 pub const StructField = struct {
332 struct_ptr: Ref,
333 field_index: u32,
334 };
341pub const StructField = struct {
342 struct_ptr: Ref,
343 field_index: u32,
335344};
345
346pub fn getMainBody(air: Air) []const Air.Inst.Index {
347 const body_index = air.extra[@enumToInt(ExtraIndex.main_block)];
348 const body_len = air.extra[body_index];
349 return air.extra[body_index..][0..body_len];
350}
351
352/// Returns the requested data, as well as the new index which is at the start of the
353/// trailers for the object.
354pub fn extraData(air: Air, comptime T: type, index: usize) struct { data: T, end: usize } {
355 const fields = std.meta.fields(T);
356 var i: usize = index;
357 var result: T = undefined;
358 inline for (fields) |field| {
359 @field(result, field.name) = switch (field.field_type) {
360 u32 => air.extra[i],
361 Inst.Ref => @intToEnum(Inst.Ref, air.extra[i]),
362 i32 => @bitCast(i32, air.extra[i]),
363 else => @compileError("bad field type"),
364 };
365 i += 1;
366 }
367 return .{
368 .data = result,
369 .end = i,
370 };
371}
src/Liveness.zig created+457
......@@ -0,0 +1,457 @@
1//! For each AIR instruction, we want to know:
2//! * Is the instruction unreferenced (e.g. dies immediately)?
3//! * For each of its operands, does the operand die with this instruction (e.g. is
4//! this the last reference to it)?
5//! Some instructions are special, such as:
6//! * Conditional Branches
7//! * Switch Branches
8const Liveness = @This();
9const std = @import("std");
10const Air = @import("Air.zig");
11const trace = @import("tracy.zig").trace;
12const log = std.log.scoped(.liveness);
13const assert = std.debug.assert;
14const Allocator = std.mem.Allocator;
15
16/// This array is split into sets of 4 bits per AIR instruction.
17/// The MSB (0bX000) is whether the instruction is unreferenced.
18/// The LSB (0b000X) is the first operand, and so on, up to 3 operands. A set bit means the
19/// operand dies after this instruction.
20/// Instructions which need more data to track liveness have special handling via the
21/// `special` table.
22tomb_bits: []const usize,
23/// Sparse table of specially handled instructions. The value is an index into the `extra`
24/// array. The meaning of the data depends on the AIR tag.
25special: std.AutoHashMapUnmanaged(Air.Inst.Index, u32),
26/// Auxilliary data. The way this data is interpreted is determined contextually.
27extra: []const u32,
28
29/// Trailing is the set of instructions whose lifetimes end at the start of the then branch,
30/// followed by the set of instructions whose lifetimes end at the start of the else branch.
31pub const CondBr = struct {
32 then_death_count: u32,
33 else_death_count: u32,
34};
35
36/// Trailing is:
37/// * For each case in the same order as in the AIR:
38/// - case_death_count: u32
39/// - Air.Inst.Index for each `case_death_count`: set of instructions whose lifetimes
40/// end at the start of this case.
41/// * Air.Inst.Index for each `else_death_count`: set of instructions whose lifetimes
42/// end at the start of the else case.
43pub const SwitchBr = struct {
44 else_death_count: u32,
45};
46
47pub fn analyze(gpa: *Allocator, air: Air) Allocator.Error!Liveness {
48 const tracy = trace(@src());
49 defer tracy.end();
50
51 var a: Analysis = .{
52 .gpa = gpa,
53 .air = &air,
54 .table = .{},
55 .tomb_bits = try gpa.alloc(
56 usize,
57 (air.instructions.len * bpi + @bitSizeOf(usize) - 1) / @bitSizeOf(usize),
58 ),
59 .extra = .{},
60 .special = .{},
61 };
62 errdefer gpa.free(a.tomb_bits);
63 errdefer a.special.deinit(gpa);
64 defer a.extra.deinit(gpa);
65 defer a.table.deinit(gpa);
66
67 const main_body = air.getMainBody();
68 try a.table.ensureTotalCapacity(main_body.len);
69 try analyzeWithContext(&a, null, main_body);
70 return Liveness{
71 .tomb_bits = a.tomb_bits,
72 .special = a.special,
73 .extra = a.extra.toOwnedSlice(gpa),
74 };
75}
76
77pub fn deinit(l: *Liveness, gpa: *Allocator) void {
78 gpa.free(l.tomb_bits);
79 gpa.free(l.extra);
80 l.special.deinit(gpa);
81}
82
83/// How many tomb bits per AIR instruction.
84const bpi = 4;
85const Bpi = std.meta.Int(.unsigned, bpi);
86
87/// In-progress data; on successful analysis converted into `Liveness`.
88const Analysis = struct {
89 gpa: *Allocator,
90 air: *const Air,
91 table: std.AutoHashMapUnmanaged(Air.Inst.Index, void),
92 tomb_bits: []usize,
93 extra: std.ArrayListUnmanaged(u32),
94
95 fn storeTombBits(a: *Analysis, inst: Air.Inst.Index, tomb_bits: Bpi) void {
96 const usize_index = (inst * bpi) / @bitSizeOf(usize);
97 a.tomb_bits[usize_index] |= tomb_bits << (inst % (@bitSizeOf(usize) / bpi)) * bpi;
98 }
99
100 fn addExtra(a: *Analysis, extra: anytype) Allocator.Error!u32 {
101 const fields = std.meta.fields(@TypeOf(extra));
102 try a.extra.ensureUnusedCapacity(a.gpa, fields.len);
103 return addExtraAssumeCapacity(a, extra);
104 }
105
106 fn addExtraAssumeCapacity(a: *Analysis, extra: anytype) u32 {
107 const fields = std.meta.fields(@TypeOf(extra));
108 const result = @intCast(u32, a.extra.items.len);
109 inline for (fields) |field| {
110 a.extra.appendAssumeCapacity(switch (field.field_type) {
111 u32 => @field(extra, field.name),
112 else => @compileError("bad field type"),
113 });
114 }
115 return result;
116 }
117};
118
119fn analyzeWithContext(
120 a: *Analysis,
121 new_set: ?*std.AutoHashMapUnmanaged(Air.Inst.Index, void),
122 body: []const Air.Inst.Index,
123) Allocator.Error!void {
124 var i: usize = body.len;
125
126 if (new_set) |ns| {
127 // We are only interested in doing this for instructions which are born
128 // before a conditional branch, so after obtaining the new set for
129 // each branch we prune the instructions which were born within.
130 while (i != 0) {
131 i -= 1;
132 const inst = body[i];
133 _ = ns.remove(inst);
134 try analyzeInst(a, new_set, inst);
135 }
136 } else {
137 while (i != 0) {
138 i -= 1;
139 const inst = body[i];
140 try analyzeInst(a, new_set, inst);
141 }
142 }
143}
144
145fn analyzeInst(
146 a: *Analysis,
147 new_set: ?*std.AutoHashMap(Air.Inst.Index, void),
148 inst: Air.Inst.Index,
149) Allocator.Error!void {
150 const gpa = a.gpa;
151 const table = &a.table;
152 const inst_tags = a.air.instructions.items(.tag);
153
154 // No tombstone for this instruction means it is never referenced,
155 // and its birth marks its own death. Very metal 🤘
156 const main_tomb = !table.contains(inst);
157
158 switch (inst_tags[inst]) {
159 .add,
160 .addwrap,
161 .sub,
162 .subwrap,
163 .mul,
164 .mulwrap,
165 .div,
166 .bit_and,
167 .bit_or,
168 .xor,
169 .cmp_lt,
170 .cmp_lte,
171 .cmp_eq,
172 .cmp_gte,
173 .cmp_gt,
174 .cmp_neq,
175 .bool_and,
176 .bool_or,
177 .store,
178 => {
179 const o = inst_datas[inst].bin_op;
180 return trackOperands(a, new_set, inst, main_tomb, .{ o.lhs, o.rhs, .none });
181 },
182
183 .alloc,
184 .br,
185 .constant,
186 .breakpoint,
187 .dbg_stmt,
188 .varptr,
189 .unreach,
190 => return trackOperands(a, new_set, inst, main_tomb, .{ .none, .none, .none }),
191
192 .not,
193 .bitcast,
194 .load,
195 .ref,
196 .floatcast,
197 .intcast,
198 .optional_payload,
199 .optional_payload_ptr,
200 .wrap_optional,
201 .unwrap_errunion_payload,
202 .unwrap_errunion_err,
203 .unwrap_errunion_payload_ptr,
204 .unwrap_errunion_err_ptr,
205 .wrap_errunion_payload,
206 .wrap_errunion_err,
207 => {
208 const o = inst_datas[inst].ty_op;
209 return trackOperands(a, new_set, inst, main_tomb, .{ o.operand, .none, .none });
210 },
211
212 .is_null,
213 .is_non_null,
214 .is_null_ptr,
215 .is_non_null_ptr,
216 .is_err,
217 .is_non_err,
218 .is_err_ptr,
219 .is_non_err_ptr,
220 .ptrtoint,
221 .ret,
222 => {
223 const operand = inst_datas[inst].un_op;
224 return trackOperands(a, new_set, inst, main_tomb, .{ operand, .none, .none });
225 },
226
227 .call => {
228 const inst_data = inst_datas[inst].pl_op;
229 const callee = inst_data.operand;
230 const extra = a.air.extraData(Air.Call, inst_data.payload);
231 const args = a.air.extra[extra.end..][0..extra.data.args_len];
232 if (args.len <= bpi - 2) {
233 var buf: [bpi - 1]Air.Inst.Ref = undefined;
234 buf[0] = callee;
235 std.mem.copy(&buf, buf[1..], args);
236 return trackOperands(a, new_set, inst, main_tomb, buf);
237 }
238 @panic("TODO: liveness analysis for function with many args");
239 },
240 .struct_field_ptr => {
241 const extra = a.air.extraData(Air.StructField, inst_datas[inst].ty_pl.payload).data;
242 return trackOperands(a, new_set, inst, main_tomb, .{ extra.struct_ptr, .none, .none });
243 },
244 .block => {
245 const extra = a.air.extraData(Air.Block, inst_datas[inst].ty_pl.payload);
246 const body = a.air.extra[extra.end..][0..extra.data.body_len];
247 try analyzeWithContext(a, new_set, body);
248 // We let this continue so that it can possibly mark the block as
249 // unreferenced below.
250 return trackOperands(a, new_set, inst, main_tomb, .{ .none, .none, .none });
251 },
252 .loop => {
253 const extra = a.air.extraData(Air.Block, inst_datas[inst].ty_pl.payload);
254 const body = a.air.extra[extra.end..][0..extra.data.body_len];
255 try analyzeWithContext(a, new_set, body);
256 return; // Loop has no operands and it is always unreferenced.
257 },
258 .cond_br => {
259 // Each death that occurs inside one branch, but not the other, needs
260 // to be added as a death immediately upon entering the other branch.
261 const inst_data = inst_datas[inst].pl_op;
262 const condition = inst_data.operand;
263 const extra = a.air.extraData(Air.CondBr, inst_data.payload);
264 const then_body = a.air.extra[extra.end..][0..extra.data.then_body_len];
265 const else_body = a.air.extra[extra.end + then_body.len ..][0..extra.data.else_body_len];
266
267 var then_table = std.AutoHashMap(Air.Inst.Index, void).init(gpa);
268 defer then_table.deinit();
269 try analyzeWithContext(a, &then_table, then_body);
270
271 // Reset the table back to its state from before the branch.
272 {
273 var it = then_table.keyIterator();
274 while (it.next()) |key| {
275 assert(table.remove(key.*));
276 }
277 }
278
279 var else_table = std.AutoHashMap(Air.Inst.Index, void).init(gpa);
280 defer else_table.deinit();
281 try analyzeWithContext(a, &else_table, else_body);
282
283 var then_entry_deaths = std.ArrayList(Air.Inst.Index).init(gpa);
284 defer then_entry_deaths.deinit();
285 var else_entry_deaths = std.ArrayList(Air.Inst.Index).init(gpa);
286 defer else_entry_deaths.deinit();
287
288 {
289 var it = else_table.keyIterator();
290 while (it.next()) |key| {
291 const else_death = key.*;
292 if (!then_table.contains(else_death)) {
293 try then_entry_deaths.append(else_death);
294 }
295 }
296 }
297 // This loop is the same, except it's for the then branch, and it additionally
298 // has to put its items back into the table to undo the reset.
299 {
300 var it = then_table.keyIterator();
301 while (it.next()) |key| {
302 const then_death = key.*;
303 if (!else_table.contains(then_death)) {
304 try else_entry_deaths.append(then_death);
305 }
306 try table.put(gpa, then_death, {});
307 }
308 }
309 // Now we have to correctly populate new_set.
310 if (new_set) |ns| {
311 try ns.ensureCapacity(@intCast(u32, ns.count() + then_table.count() + else_table.count()));
312 var it = then_table.keyIterator();
313 while (it.next()) |key| {
314 _ = ns.putAssumeCapacity(key.*, {});
315 }
316 it = else_table.keyIterator();
317 while (it.next()) |key| {
318 _ = ns.putAssumeCapacity(key.*, {});
319 }
320 }
321 const then_death_count = @intCast(u32, then_entry_deaths.items.len);
322 const else_death_count = @intCast(u32, else_entry_deaths.items.len);
323
324 try a.extra.ensureUnusedCapacity(std.meta.fields(@TypeOf(CondBr)).len +
325 then_death_count + else_death_count);
326 const extra_index = a.addExtraAssumeCapacity(CondBr{
327 .then_death_count = then_death_count,
328 .else_death_count = else_death_count,
329 });
330 a.extra.appendSliceAssumeCapacity(then_entry_deaths.items);
331 a.extra.appendSliceAssumeCapacity(else_entry_deaths.items);
332 try a.special.put(inst, extra_index);
333
334 // Continue on with the instruction analysis. The following code will find the condition
335 // instruction, and the deaths flag for the CondBr instruction will indicate whether the
336 // condition's lifetime ends immediately before entering any branch.
337 return trackOperands(a, new_set, inst, main_tomb, .{ condition, .none, .none });
338 },
339 .switch_br => {
340 const inst_data = inst_datas[inst].pl_op;
341 const condition = inst_data.operand;
342 const switch_br = a.air.extraData(Air.SwitchBr, inst_data.payload);
343
344 const Table = std.AutoHashMapUnmanaged(Air.Inst.Index, void);
345 const case_tables = try gpa.alloc(Table, switch_br.data.cases_len + 1); // +1 for else
346 defer gpa.free(case_tables);
347
348 std.mem.set(Table, case_tables, .{});
349 defer for (case_tables) |*ct| ct.deinit(gpa);
350
351 var air_extra_index: usize = switch_br.end;
352 for (case_tables[0..switch_br.data.cases_len]) |*case_table| {
353 const case = a.air.extraData(Air.SwitchBr.Case, air_extra_index);
354 const case_body = a.air.extra[case.end..][0..case.data.body_len];
355 air_extra_index = case.end + case_body.len;
356 try analyzeWithContext(a, case_table, case_body);
357
358 // Reset the table back to its state from before the case.
359 var it = case_table.keyIterator();
360 while (it.next()) |key| {
361 assert(table.remove(key.*));
362 }
363 }
364 { // else
365 const else_table = &case_tables[case_tables.len - 1];
366 const else_body = a.air.extra[air_extra_index..][0..switch_br.data.else_body_len];
367 try analyzeWithContext(a, else_table, else_body);
368
369 // Reset the table back to its state from before the case.
370 var it = else_table.keyIterator();
371 while (it.next()) |key| {
372 assert(table.remove(key.*));
373 }
374 }
375
376 const List = std.ArrayListUnmanaged(Air.Inst.Index);
377 const case_deaths = try gpa.alloc(List, case_tables.len); // includes else
378 defer gpa.free(case_deaths);
379
380 std.mem.set(List, case_deaths, .{});
381 defer for (case_deaths) |*cd| cd.deinit(gpa);
382
383 var total_deaths: u32 = 0;
384 for (case_tables) |*ct, i| {
385 total_deaths += ct.count();
386 var it = ct.keyIterator();
387 while (it.next()) |key| {
388 const case_death = key.*;
389 for (case_tables) |*ct_inner, j| {
390 if (i == j) continue;
391 if (!ct_inner.contains(case_death)) {
392 // instruction is not referenced in this case
393 try case_deaths[j].append(gpa, case_death);
394 }
395 }
396 // undo resetting the table
397 try table.put(gpa, case_death, {});
398 }
399 }
400
401 // Now we have to correctly populate new_set.
402 if (new_set) |ns| {
403 try ns.ensureUnusedCapacity(gpa, total_deaths);
404 for (case_tables) |*ct| {
405 var it = ct.keyIterator();
406 while (it.next()) |key| {
407 _ = ns.putAssumeCapacity(key.*, {});
408 }
409 }
410 }
411
412 const else_death_count = @intCast(u32, case_deaths[case_deaths.len - 1].items.len);
413 const extra_index = try a.addExtra(SwitchBr{
414 .else_death_count = else_death_count,
415 });
416 for (case_deaths[0 .. case_deaths.len - 1]) |*cd| {
417 const case_death_count = @intCast(u32, cd.items.len);
418 try a.extra.ensureUnusedCapacity(1 + case_death_count + else_death_count);
419 a.extra.appendAssumeCapacity(case_death_count);
420 a.extra.appendSliceAssumeCapacity(cd.items);
421 }
422 a.extra.appendSliceAssumeCapacity(case_deaths[case_deaths.len - 1].items);
423 try a.special.put(inst, extra_index);
424
425 return trackOperands(a, new_set, inst, main_tomb, .{ condition, .none, .none });
426 },
427 }
428}
429
430fn trackOperands(
431 a: *Analysis,
432 new_set: ?*std.AutoHashMap(Air.Inst.Index, void),
433 inst: Air.Inst.Index,
434 main_tomb: bool,
435 operands: [bpi - 1]Air.Inst.Ref,
436) Allocator.Error!void {
437 const table = &a.table;
438 const gpa = a.gpa;
439
440 var tomb_bits: Bpi = @boolToInt(main_tomb);
441 var i = operands.len;
442
443 while (i > 0) {
444 i -= 1;
445 tomb_bits <<= 1;
446 const op_int = @enumToInt(operands[i]);
447 if (op_int < Air.Inst.Ref.typed_value_map.len) continue;
448 const operand: Air.Inst.Index = op_int - Air.Inst.Ref.typed_value_map.len;
449 const prev = try table.fetchPut(gpa, operand, {});
450 if (prev == null) {
451 // Death.
452 tomb_bits |= 1;
453 if (new_set) |ns| try ns.putNoClobber(operand, {});
454 }
455 }
456 a.storeTombBits(inst, tomb_bits);
457}
src/codegen.zig+5-8
......@@ -297,7 +297,8 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
297297 /// across each runtime branch upon joining.
298298 branch_stack: *std.ArrayList(Branch),
299299
300 blocks: std.AutoHashMapUnmanaged(*ir.Inst.Block, BlockData) = .{},
300 // Key is the block instruction
301 blocks: std.AutoHashMapUnmanaged(Air.Inst.Index, BlockData) = .{},
301302
302303 register_manager: RegisterManager(Self, Register, &callee_preserved_regs) = .{},
303304 /// Maps offset to what is stored there.
......@@ -383,7 +384,7 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
383384 };
384385
385386 const Branch = struct {
386 inst_table: std.AutoArrayHashMapUnmanaged(*ir.Inst, MCValue) = .{},
387 inst_table: std.AutoArrayHashMapUnmanaged(Air.Inst.Index, MCValue) = .{},
387388
388389 fn deinit(self: *Branch, gpa: *Allocator) void {
389390 self.inst_table.deinit(gpa);
......@@ -392,7 +393,7 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
392393 };
393394
394395 const StackAllocation = struct {
395 inst: *ir.Inst,
396 inst: Air.Inst.Index,
396397 /// TODO do we need size? should be determined by inst.ty.abiSize()
397398 size: u32,
398399 };
......@@ -720,7 +721,7 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
720721 try self.dbgAdvancePCAndLine(self.end_di_line, self.end_di_column);
721722 }
722723
723 fn genBody(self: *Self, body: ir.Body) InnerError!void {
724 fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
724725 for (body.instructions) |inst| {
725726 try self.ensureProcessDeathCapacity(@popCount(@TypeOf(inst.deaths), inst.deaths));
726727
......@@ -2824,10 +2825,6 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
28242825 }
28252826
28262827 fn genDbgStmt(self: *Self, inst: *ir.Inst.DbgStmt) !MCValue {
2827 // TODO when reworking AIR memory layout, rework source locations here as
2828 // well to be more efficient, as well as support inlined function calls correctly.
2829 // For now we convert LazySrcLoc to absolute byte offset, to match what the
2830 // existing codegen code expects.
28312828 try self.dbgAdvancePCAndLine(inst.line, inst.column);
28322829 assert(inst.base.isUnused());
28332830 return MCValue.dead;
src/liveness.zig deleted-254
......@@ -1,254 +0,0 @@
1const std = @import("std");
2const Air = @import("Air.zig");
3const trace = @import("tracy.zig").trace;
4const log = std.log.scoped(.liveness);
5const assert = std.debug.assert;
6
7/// Perform Liveness Analysis over the `Body`. Each `Inst` will have its `deaths` field populated.
8pub fn analyze(
9 /// Used for temporary storage during the analysis.
10 gpa: *std.mem.Allocator,
11 /// Used to tack on extra allocations in the same lifetime as the existing instructions.
12 arena: *std.mem.Allocator,
13 body: ir.Body,
14) error{OutOfMemory}!void {
15 const tracy = trace(@src());
16 defer tracy.end();
17
18 var table = std.AutoHashMap(*ir.Inst, void).init(gpa);
19 defer table.deinit();
20 try table.ensureCapacity(@intCast(u32, body.instructions.len));
21 try analyzeWithTable(arena, &table, null, body);
22}
23
24fn analyzeWithTable(
25 arena: *std.mem.Allocator,
26 table: *std.AutoHashMap(*ir.Inst, void),
27 new_set: ?*std.AutoHashMap(*ir.Inst, void),
28 body: ir.Body,
29) error{OutOfMemory}!void {
30 var i: usize = body.instructions.len;
31
32 if (new_set) |ns| {
33 // We are only interested in doing this for instructions which are born
34 // before a conditional branch, so after obtaining the new set for
35 // each branch we prune the instructions which were born within.
36 while (i != 0) {
37 i -= 1;
38 const base = body.instructions[i];
39 _ = ns.remove(base);
40 try analyzeInst(arena, table, new_set, base);
41 }
42 } else {
43 while (i != 0) {
44 i -= 1;
45 const base = body.instructions[i];
46 try analyzeInst(arena, table, new_set, base);
47 }
48 }
49}
50
51fn analyzeInst(
52 arena: *std.mem.Allocator,
53 table: *std.AutoHashMap(*ir.Inst, void),
54 new_set: ?*std.AutoHashMap(*ir.Inst, void),
55 base: *ir.Inst,
56) error{OutOfMemory}!void {
57 if (table.contains(base)) {
58 base.deaths = 0;
59 } else {
60 // No tombstone for this instruction means it is never referenced,
61 // and its birth marks its own death. Very metal 🤘
62 base.deaths = 1 << ir.Inst.unreferenced_bit_index;
63 }
64
65 switch (base.tag) {
66 .constant => return,
67 .block => {
68 const inst = base.castTag(.block).?;
69 try analyzeWithTable(arena, table, new_set, inst.body);
70 // We let this continue so that it can possibly mark the block as
71 // unreferenced below.
72 },
73 .loop => {
74 const inst = base.castTag(.loop).?;
75 try analyzeWithTable(arena, table, new_set, inst.body);
76 return; // Loop has no operands and it is always unreferenced.
77 },
78 .condbr => {
79 const inst = base.castTag(.condbr).?;
80
81 // Each death that occurs inside one branch, but not the other, needs
82 // to be added as a death immediately upon entering the other branch.
83
84 var then_table = std.AutoHashMap(*ir.Inst, void).init(table.allocator);
85 defer then_table.deinit();
86 try analyzeWithTable(arena, table, &then_table, inst.then_body);
87
88 // Reset the table back to its state from before the branch.
89 {
90 var it = then_table.keyIterator();
91 while (it.next()) |key| {
92 assert(table.remove(key.*));
93 }
94 }
95
96 var else_table = std.AutoHashMap(*ir.Inst, void).init(table.allocator);
97 defer else_table.deinit();
98 try analyzeWithTable(arena, table, &else_table, inst.else_body);
99
100 var then_entry_deaths = std.ArrayList(*ir.Inst).init(table.allocator);
101 defer then_entry_deaths.deinit();
102 var else_entry_deaths = std.ArrayList(*ir.Inst).init(table.allocator);
103 defer else_entry_deaths.deinit();
104
105 {
106 var it = else_table.keyIterator();
107 while (it.next()) |key| {
108 const else_death = key.*;
109 if (!then_table.contains(else_death)) {
110 try then_entry_deaths.append(else_death);
111 }
112 }
113 }
114 // This loop is the same, except it's for the then branch, and it additionally
115 // has to put its items back into the table to undo the reset.
116 {
117 var it = then_table.keyIterator();
118 while (it.next()) |key| {
119 const then_death = key.*;
120 if (!else_table.contains(then_death)) {
121 try else_entry_deaths.append(then_death);
122 }
123 try table.put(then_death, {});
124 }
125 }
126 // Now we have to correctly populate new_set.
127 if (new_set) |ns| {
128 try ns.ensureCapacity(@intCast(u32, ns.count() + then_table.count() + else_table.count()));
129 var it = then_table.keyIterator();
130 while (it.next()) |key| {
131 _ = ns.putAssumeCapacity(key.*, {});
132 }
133 it = else_table.keyIterator();
134 while (it.next()) |key| {
135 _ = ns.putAssumeCapacity(key.*, {});
136 }
137 }
138 inst.then_death_count = std.math.cast(@TypeOf(inst.then_death_count), then_entry_deaths.items.len) catch return error.OutOfMemory;
139 inst.else_death_count = std.math.cast(@TypeOf(inst.else_death_count), else_entry_deaths.items.len) catch return error.OutOfMemory;
140 const allocated_slice = try arena.alloc(*ir.Inst, then_entry_deaths.items.len + else_entry_deaths.items.len);
141 inst.deaths = allocated_slice.ptr;
142 std.mem.copy(*ir.Inst, inst.thenDeaths(), then_entry_deaths.items);
143 std.mem.copy(*ir.Inst, inst.elseDeaths(), else_entry_deaths.items);
144
145 // Continue on with the instruction analysis. The following code will find the condition
146 // instruction, and the deaths flag for the CondBr instruction will indicate whether the
147 // condition's lifetime ends immediately before entering any branch.
148 },
149 .switchbr => {
150 const inst = base.castTag(.switchbr).?;
151
152 const Table = std.AutoHashMap(*ir.Inst, void);
153 const case_tables = try table.allocator.alloc(Table, inst.cases.len + 1); // +1 for else
154 defer table.allocator.free(case_tables);
155
156 std.mem.set(Table, case_tables, Table.init(table.allocator));
157 defer for (case_tables) |*ct| ct.deinit();
158
159 for (inst.cases) |case, i| {
160 try analyzeWithTable(arena, table, &case_tables[i], case.body);
161
162 // Reset the table back to its state from before the case.
163 var it = case_tables[i].keyIterator();
164 while (it.next()) |key| {
165 assert(table.remove(key.*));
166 }
167 }
168 { // else
169 try analyzeWithTable(arena, table, &case_tables[case_tables.len - 1], inst.else_body);
170
171 // Reset the table back to its state from before the case.
172 var it = case_tables[case_tables.len - 1].keyIterator();
173 while (it.next()) |key| {
174 assert(table.remove(key.*));
175 }
176 }
177
178 const List = std.ArrayList(*ir.Inst);
179 const case_deaths = try table.allocator.alloc(List, case_tables.len); // +1 for else
180 defer table.allocator.free(case_deaths);
181
182 std.mem.set(List, case_deaths, List.init(table.allocator));
183 defer for (case_deaths) |*cd| cd.deinit();
184
185 var total_deaths: u32 = 0;
186 for (case_tables) |*ct, i| {
187 total_deaths += ct.count();
188 var it = ct.keyIterator();
189 while (it.next()) |key| {
190 const case_death = key.*;
191 for (case_tables) |*ct_inner, j| {
192 if (i == j) continue;
193 if (!ct_inner.contains(case_death)) {
194 // instruction is not referenced in this case
195 try case_deaths[j].append(case_death);
196 }
197 }
198 // undo resetting the table
199 try table.put(case_death, {});
200 }
201 }
202
203 // Now we have to correctly populate new_set.
204 if (new_set) |ns| {
205 try ns.ensureCapacity(@intCast(u32, ns.count() + total_deaths));
206 for (case_tables) |*ct| {
207 var it = ct.keyIterator();
208 while (it.next()) |key| {
209 _ = ns.putAssumeCapacity(key.*, {});
210 }
211 }
212 }
213
214 total_deaths = 0;
215 for (case_deaths[0 .. case_deaths.len - 1]) |*ct, i| {
216 inst.cases[i].index = total_deaths;
217 const len = std.math.cast(@TypeOf(inst.else_deaths), ct.items.len) catch return error.OutOfMemory;
218 inst.cases[i].deaths = len;
219 total_deaths += len;
220 }
221 { // else
222 const else_deaths = std.math.cast(@TypeOf(inst.else_deaths), case_deaths[case_deaths.len - 1].items.len) catch return error.OutOfMemory;
223 inst.else_index = total_deaths;
224 inst.else_deaths = else_deaths;
225 total_deaths += else_deaths;
226 }
227
228 const allocated_slice = try arena.alloc(*ir.Inst, total_deaths);
229 inst.deaths = allocated_slice.ptr;
230 for (case_deaths[0 .. case_deaths.len - 1]) |*cd, i| {
231 std.mem.copy(*ir.Inst, inst.caseDeaths(i), cd.items);
232 }
233 std.mem.copy(*ir.Inst, inst.elseDeaths(), case_deaths[case_deaths.len - 1].items);
234 },
235 else => {},
236 }
237
238 const needed_bits = base.operandCount();
239 if (needed_bits <= ir.Inst.deaths_bits) {
240 var bit_i: ir.Inst.DeathsBitIndex = 0;
241 while (base.getOperand(bit_i)) |operand| : (bit_i += 1) {
242 const prev = try table.fetchPut(operand, {});
243 if (prev == null) {
244 // Death.
245 base.deaths |= @as(ir.Inst.DeathsInt, 1) << bit_i;
246 if (new_set) |ns| try ns.putNoClobber(operand, {});
247 }
248 }
249 } else {
250 @panic("Handle liveness analysis for instructions with many parameters");
251 }
252
253 log.debug("analyze {}: 0b{b}\n", .{ base.tag, base.deaths });
254}