authorgravatar for mail@isaacfreund.comIsaac Freund <mail@isaacfreund.com> 2020-08-18 18:30:48+02:00
committergravatar for mail@isaacfreund.comIsaac Freund <mail@isaacfreund.com> 2020-08-19 02:05:01+02:00
logfe3aa4ccd0bc157e1dd9d84b7c57b0cb2f0a77a9
tree9412eed471ec13ed1649ee3348f6d62e802202a9
parent741fb8d30675f85316f7df100801e7cf8c2b3194
signature Commit is signed but in an unrecognized format.

stage2/wasm: do incremental compilation in-memory

Before this commit the wasm backend worked similarly to elf. As functions were generated they were written directly to the output file and existing code was shifted around in the file as necessary. This approach had several disadvantages: - Large amounts of padding in the output were necessary to avoid expensive copying of data within the file. - Function/type/global/etc indexes were required to be known at the time of preforming codegen, which severely limited the flexibility of where code could be placed in the binary - Significant complexity to track the state of the output file through incremental updates This commit takes things in a different direction. Code is incrementally compiled into in-memory buffers and the entire binary is rewritten using these buffers on flush. This has several advantages: - Significantly smaller resulting binaries - More performant resulting binaries due to lack of indirection - Significantly simpler compiler code - Indexes no longer need to be known before codegen. We can track where Decls must be referenced by index insert the proper indexes while writing the code in the flush() function. This is not yet implemented but is planned for the next commit. The main disadvantage is of course increased memory usage in order to store these buffers of generated code.

1 files changed, 135 insertions(+), 361 deletions(-)

src-self-hosted/link/Wasm.zig+135-361
......@@ -32,19 +32,19 @@ const spec = struct {
3232pub const base_tag = link.File.Tag.wasm;
3333
3434pub const FnData = struct {
35 funcidx: u32,
35 /// Generated code for the type of the function
36 functype: std.ArrayListUnmanaged(u8) = .{},
37 /// Generated code for the body of the function
38 code: std.ArrayListUnmanaged(u8) = .{},
3639};
3740
3841base: link.File,
3942
40types: Types,
41funcs: Funcs,
42exports: Exports,
43
44/// Array over the section structs used in the various sections above to
45/// allow iteration when shifting sections to make space.
46/// TODO: this should eventually be size 11 when we use all the sections.
47sections: [4]*Section,
43/// List of all function Decls to be written to the output file. The index of
44/// each Decl in this list at the time of writing the binary is used as the
45/// function index.
46/// TODO: can/should we access some data structure in Module directly?
47funcs: std.ArrayListUnmanaged(*Module.Decl) = .{},
4848
4949pub fn openPath(allocator: *Allocator, dir: fs.Dir, sub_path: []const u8, options: link.Options) !*link.File {
5050 assert(options.object_format == .wasm);
......@@ -58,10 +58,6 @@ pub fn openPath(allocator: *Allocator, dir: fs.Dir, sub_path: []const u8, option
5858
5959 try file.writeAll(&(spec.magic ++ spec.version));
6060
61 // TODO: this should vary depending on the section and be less arbitrary
62 const size = 1024;
63 const offset = @sizeOf(@TypeOf(spec.magic ++ spec.version));
64
6561 wasm.* = .{
6662 .base = .{
6763 .tag = .wasm,
......@@ -69,52 +65,40 @@ pub fn openPath(allocator: *Allocator, dir: fs.Dir, sub_path: []const u8, option
6965 .file = file,
7066 .allocator = allocator,
7167 },
72
73 .types = try Types.init(file, offset, size),
74 .funcs = try Funcs.init(file, offset + size, size, offset + 3 * size, size),
75 .exports = try Exports.init(file, offset + 2 * size, size),
76
77 // These must be ordered as they will appear in the output file
78 .sections = [_]*Section{
79 &wasm.types.typesec.section,
80 &wasm.funcs.funcsec,
81 &wasm.exports.exportsec,
82 &wasm.funcs.codesec.section,
83 },
8468 };
8569
86 try file.setEndPos(offset + 4 * size);
87
8870 return &wasm.base;
8971}
9072
9173pub fn deinit(self: *Wasm) void {
92 self.types.deinit();
93 self.funcs.deinit();
74 for (self.funcs.items) |decl| {
75 decl.fn_link.wasm.?.functype.deinit(self.base.allocator);
76 decl.fn_link.wasm.?.code.deinit(self.base.allocator);
77 }
78 self.funcs.deinit(self.base.allocator);
9479}
9580
81// Generate code for the Decl, storing it in memory to be later written to
82// the file on flush().
9683pub fn updateDecl(self: *Wasm, module: *Module, decl: *Module.Decl) !void {
9784 if (decl.typed_value.most_recent.typed_value.ty.zigTypeTag() != .Fn)
9885 return error.TODOImplementNonFnDeclsForWasm;
9986
100 if (decl.fn_link.wasm) |fn_data| {
101 self.funcs.free(fn_data.funcidx);
102 }
103
104 var buf = std.ArrayList(u8).init(self.base.allocator);
105 defer buf.deinit();
106
107 try codegen.genFunctype(&buf, decl);
108 const typeidx = try self.types.new(buf.items);
109 buf.items.len = 0;
110
111 try codegen.genCode(&buf, decl);
112 const funcidx = try self.funcs.new(typeidx, buf.items);
113
114 decl.fn_link.wasm = .{ .funcidx = funcidx };
115
116 // TODO: we should be more smart and set this only when needed
117 self.exports.dirty = true;
87 if (decl.fn_link.wasm) |*fn_data| {
88 fn_data.functype.items.len = 0;
89 fn_data.code.items.len = 0;
90 } else {
91 decl.fn_link.wasm = .{};
92 try self.funcs.append(self.base.allocator, decl);
93 }
94 const fn_data = &decl.fn_link.wasm.?;
95
96 var managed_functype = fn_data.functype.toManaged(self.base.allocator);
97 var managed_code = fn_data.code.toManaged(self.base.allocator);
98 try codegen.genFunctype(&managed_functype, decl);
99 try codegen.genCode(&managed_code, decl);
100 fn_data.functype = managed_functype.toUnmanaged();
101 fn_data.code = managed_code.toUnmanaged();
118102}
119103
120104pub fn updateDeclExports(
......@@ -122,332 +106,122 @@ pub fn updateDeclExports(
122106 module: *Module,
123107 decl: *const Module.Decl,
124108 exports: []const *Module.Export,
125) !void {
126 self.exports.dirty = true;
127}
109) !void {}
128110
129111pub fn freeDecl(self: *Wasm, decl: *Module.Decl) void {
130112 // TODO: remove this assert when non-function Decls are implemented
131113 assert(decl.typed_value.most_recent.typed_value.ty.zigTypeTag() == .Fn);
132 if (decl.fn_link.wasm) |fn_data| {
133 self.funcs.free(fn_data.funcidx);
134 decl.fn_link.wasm = null;
135 }
114 _ = self.funcs.swapRemove(self.getFuncidx(decl).?);
115 decl.fn_link.wasm.?.functype.deinit(self.base.allocator);
116 decl.fn_link.wasm.?.code.deinit(self.base.allocator);
117 decl.fn_link.wasm = null;
136118}
137119
138120pub fn flush(self: *Wasm, module: *Module) !void {
139 if (self.exports.dirty) try self.exports.writeAll(module);
140}
141
142/// This struct describes the location of a named section + custom section
143/// padding in the output file. This is all the data we need to allow for
144/// shifting sections around when padding runs out.
145const Section = struct {
146 /// The size of a section header: 1 byte section id + 5 bytes
147 /// for the fixed-width ULEB128 encoded contents size.
148 const header_size = 1 + 5;
149 /// Offset of the section id byte from the start of the file.
150 offset: u64,
151 /// Size of the section, including the header and directly
152 /// following custom section used for padding if any.
153 size: u64,
154
155 /// Resize the usable part of the section, handling the following custom
156 /// section used for padding. If there is not enough padding left, shift
157 /// all following sections to make space. Takes the current and target
158 /// contents sizes of the section as arguments.
159 fn resize(self: *Section, file: fs.File, current: u32, target: u32) !void {
160 // Section header + target contents size + custom section header
161 // + custom section name + empty custom section > owned chunk of the file
162 if (header_size + target + header_size + 1 + 0 > self.size)
163 return error.TODOImplementSectionShifting;
164
165 const new_custom_start = self.offset + header_size + target;
166 const new_custom_contents_size = self.size - target - 2 * header_size;
167 assert(new_custom_contents_size >= 1);
168 // +1 for the name of the custom section, which we set to an empty string
169 var custom_header: [header_size + 1]u8 = undefined;
170 custom_header[0] = spec.custom_id;
171 leb.writeUnsignedFixed(5, custom_header[1..header_size], @intCast(u32, new_custom_contents_size));
172 custom_header[header_size] = 0;
173 try file.pwriteAll(&custom_header, new_custom_start);
174 }
175};
176
177/// This can be used to manage the contents of any section which uses a vector
178/// of contents. This interface maintains index stability while allowing for
179/// reuse of "dead" indexes.
180const VecSection = struct {
181 /// Represents a single entry in the vector (e.g. a type in the type section)
182 const Entry = struct {
183 /// Offset from the start of the section contents in bytes
184 offset: u32,
185 /// Size in bytes of the entry
186 size: u32,
187 };
188 section: Section,
189 /// Size in bytes of the contents of the section. Does not include
190 /// the "header" containing the section id and this value.
191 contents_size: u32,
192 /// List of all entries in the contents of the section.
193 entries: std.ArrayListUnmanaged(Entry) = std.ArrayListUnmanaged(Entry){},
194 /// List of indexes of unreferenced entries which may be
195 /// overwritten and reused.
196 dead_list: std.ArrayListUnmanaged(u32) = std.ArrayListUnmanaged(u32){},
197
198 /// Write the headers of the section and custom padding section
199 fn init(comptime section_id: u8, file: fs.File, offset: u64, initial_size: u64) !VecSection {
200 // section id, section size, empty vector, custom section id,
201 // custom section size, empty custom section name
202 var initial_data: [1 + 5 + 5 + 1 + 5 + 1]u8 = undefined;
203
204 assert(initial_size >= initial_data.len);
205
206 comptime var i = 0;
207 initial_data[i] = section_id;
208 i += 1;
209 leb.writeUnsignedFixed(5, initial_data[i..(i + 5)], 5);
210 i += 5;
211 leb.writeUnsignedFixed(5, initial_data[i..(i + 5)], 0);
212 i += 5;
213 initial_data[i] = spec.custom_id;
214 i += 1;
215 leb.writeUnsignedFixed(5, initial_data[i..(i + 5)], @intCast(u32, initial_size - @sizeOf(@TypeOf(initial_data))));
216 i += 5;
217 initial_data[i] = 0;
218
219 try file.pwriteAll(&initial_data, offset);
220
221 return VecSection{
222 .section = .{
223 .offset = offset,
224 .size = initial_size,
225 },
226 .contents_size = 5,
227 };
228 }
229
230 fn deinit(self: *VecSection, allocator: *Allocator) void {
231 self.entries.deinit(allocator);
232 self.dead_list.deinit(allocator);
233 }
234
235 /// Write a new entry into the file, returning the index used.
236 fn addEntry(self: *VecSection, file: fs.File, allocator: *Allocator, data: []const u8) !u32 {
237 // First look for a dead entry we can reuse
238 for (self.dead_list.items) |dead_idx, i| {
239 const dead_entry = &self.entries.items[dead_idx];
240 if (dead_entry.size == data.len) {
241 // Found a dead entry of the right length, overwrite it
242 try file.pwriteAll(data, self.section.offset + Section.header_size + dead_entry.offset);
243 _ = self.dead_list.swapRemove(i);
244 return dead_idx;
245 }
121 const file = self.base.file.?;
122 const header_size = 5 + 1;
123
124 // No need to rewrite the magic/version header
125 try file.setEndPos(@sizeOf(@TypeOf(spec.magic ++ spec.version)));
126 try file.seekTo(@sizeOf(@TypeOf(spec.magic ++ spec.version)));
127
128 // Type section
129 {
130 const header_offset = try reserveVecSectionHeader(file);
131 for (self.funcs.items) |decl| {
132 try file.writeAll(decl.fn_link.wasm.?.functype.items);
246133 }
247
248 // TODO: We can be more efficient if we special-case one or
249 // more consecutive dead entries at the end of the vector.
250
251 // We failed to find a dead entry to reuse, so write the new
252 // entry to the end of the section.
253 try self.section.resize(file, self.contents_size, self.contents_size + @intCast(u32, data.len));
254 try file.pwriteAll(data, self.section.offset + Section.header_size + self.contents_size);
255 try self.entries.append(allocator, .{
256 .offset = self.contents_size,
257 .size = @intCast(u32, data.len),
258 });
259 self.contents_size += @intCast(u32, data.len);
260 // Make sure the dead list always has enough space to store all free'd
261 // entries. This makes it so that delEntry() cannot fail.
262 // TODO: figure out a better way that doesn't waste as much memory
263 try self.dead_list.ensureCapacity(allocator, self.entries.items.len);
264
265 // Update the size in the section header and the item count of
266 // the contents vector.
267 var size_and_count: [10]u8 = undefined;
268 leb.writeUnsignedFixed(5, size_and_count[0..5], self.contents_size);
269 leb.writeUnsignedFixed(5, size_and_count[5..], @intCast(u32, self.entries.items.len));
270 try file.pwriteAll(&size_and_count, self.section.offset + 1);
271
272 return @intCast(u32, self.entries.items.len - 1);
273 }
274
275 /// Mark the type referenced by the given index as dead.
276 fn delEntry(self: *VecSection, index: u32) void {
277 self.dead_list.appendAssumeCapacity(index);
278 }
279};
280
281const Types = struct {
282 typesec: VecSection,
283
284 fn init(file: fs.File, offset: u64, initial_size: u64) !Types {
285 return Types{ .typesec = try VecSection.init(spec.types_id, file, offset, initial_size) };
286 }
287
288 fn deinit(self: *Types) void {
289 const wasm = @fieldParentPtr(Wasm, "types", self);
290 self.typesec.deinit(wasm.base.allocator);
291 }
292
293 fn new(self: *Types, data: []const u8) !u32 {
294 const wasm = @fieldParentPtr(Wasm, "types", self);
295 return self.typesec.addEntry(wasm.base.file.?, wasm.base.allocator, data);
296 }
297
298 fn free(self: *Types, typeidx: u32) void {
299 self.typesec.delEntry(typeidx);
300 }
301};
302
303const Funcs = struct {
304 /// This section needs special handling to keep the indexes matching with
305 /// the codesec, so we cant just use a VecSection.
306 funcsec: Section,
307 /// The typeidx stored for each function, indexed by funcidx.
308 func_types: std.ArrayListUnmanaged(u32) = std.ArrayListUnmanaged(u32){},
309 codesec: VecSection,
310
311 fn init(file: fs.File, funcs_offset: u64, funcs_size: u64, code_offset: u64, code_size: u64) !Funcs {
312 return Funcs{
313 .funcsec = (try VecSection.init(spec.funcs_id, file, funcs_offset, funcs_size)).section,
314 .codesec = try VecSection.init(spec.code_id, file, code_offset, code_size),
315 };
316 }
317
318 fn deinit(self: *Funcs) void {
319 const wasm = @fieldParentPtr(Wasm, "funcs", self);
320 self.func_types.deinit(wasm.base.allocator);
321 self.codesec.deinit(wasm.base.allocator);
322 }
323
324 /// Add a new function to the binary, first finding space for and writing
325 /// the code then writing the typeidx to the corresponding index in the
326 /// funcsec. Returns the function index used.
327 fn new(self: *Funcs, typeidx: u32, code: []const u8) !u32 {
328 const wasm = @fieldParentPtr(Wasm, "funcs", self);
329 const file = wasm.base.file.?;
330 const allocator = wasm.base.allocator;
331
332 assert(self.func_types.items.len == self.codesec.entries.items.len);
333
334 // TODO: consider nop-padding the code if there is a close but not perfect fit
335 const funcidx = try self.codesec.addEntry(file, allocator, code);
336
337 if (self.func_types.items.len < self.codesec.entries.items.len) {
338 // u32 vector length + funcs_count u32s in the vector
339 const current = 5 + @intCast(u32, self.func_types.items.len) * 5;
340 try self.funcsec.resize(file, current, current + 5);
341 try self.func_types.append(allocator, typeidx);
342
343 // Update the size in the section header and the item count of
344 // the contents vector.
345 const count = @intCast(u32, self.func_types.items.len);
346 var size_and_count: [10]u8 = undefined;
347 leb.writeUnsignedFixed(5, size_and_count[0..5], 5 + count * 5);
348 leb.writeUnsignedFixed(5, size_and_count[5..], count);
349 try file.pwriteAll(&size_and_count, self.funcsec.offset + 1);
350 } else {
351 // We are overwriting a dead function and may now free the type
352 wasm.types.free(self.func_types.items[funcidx]);
353 }
354
355 assert(self.func_types.items.len == self.codesec.entries.items.len);
356
357 var typeidx_leb: [5]u8 = undefined;
358 leb.writeUnsignedFixed(5, &typeidx_leb, typeidx);
359 try file.pwriteAll(&typeidx_leb, self.funcsec.offset + Section.header_size + 5 + funcidx * 5);
360
361 return funcidx;
362 }
363
364 fn free(self: *Funcs, funcidx: u32) void {
365 self.codesec.delEntry(funcidx);
366 }
367};
368
369/// Exports are tricky. We can't leave dead entries in the binary as they
370/// would obviously be visible from the execution environment. The simplest
371/// way to work around this is to re-emit the export section whenever
372/// something changes. This also makes it easier to ensure exported function
373/// and global indexes are updated as they change.
374const Exports = struct {
375 exportsec: Section,
376 /// Size in bytes of the contents of the section. Does not include
377 /// the "header" containing the section id and this value.
378 contents_size: u32,
379 /// If this is true, then exports will be rewritten on flush()
380 dirty: bool,
381
382 fn init(file: fs.File, offset: u64, initial_size: u64) !Exports {
383 return Exports{
384 .exportsec = (try VecSection.init(spec.exports_id, file, offset, initial_size)).section,
385 .contents_size = 5,
386 .dirty = false,
387 };
388 }
389
390 fn writeAll(self: *Exports, module: *Module) !void {
391 const wasm = @fieldParentPtr(Wasm, "exports", self);
392 const file = wasm.base.file.?;
393 var buf: [5]u8 = undefined;
394
395 // First ensure the section is the right size
396 var export_count: u32 = 0;
397 var new_contents_size: u32 = 5;
134 try writeVecSectionHeader(
135 file,
136 header_offset,
137 spec.types_id,
138 @intCast(u32, (try file.getPos()) - header_offset - header_size),
139 @intCast(u32, self.funcs.items.len),
140 );
141 }
142
143 // Function section
144 {
145 const header_offset = try reserveVecSectionHeader(file);
146 const writer = file.writer();
147 for (self.funcs.items) |_, typeidx| try leb.writeULEB128(writer, @intCast(u32, typeidx));
148 try writeVecSectionHeader(
149 file,
150 header_offset,
151 spec.funcs_id,
152 @intCast(u32, (try file.getPos()) - header_offset - header_size),
153 @intCast(u32, self.funcs.items.len),
154 );
155 }
156
157 // Export section
158 {
159 const header_offset = try reserveVecSectionHeader(file);
160 const writer = file.writer();
161 var count: u32 = 0;
398162 for (module.decl_exports.entries.items) |entry| {
399 for (entry.value) |e| {
400 export_count += 1;
401 new_contents_size += calcSize(e);
163 for (entry.value) |exprt| {
164 // Export name length + name
165 try leb.writeULEB128(writer, @intCast(u32, exprt.options.name.len));
166 try writer.writeAll(exprt.options.name);
167
168 switch (exprt.exported_decl.typed_value.most_recent.typed_value.ty.zigTypeTag()) {
169 .Fn => {
170 // Type of the export
171 try writer.writeByte(0x00);
172 // Exported function index
173 try leb.writeULEB128(writer, self.getFuncidx(exprt.exported_decl).?);
174 },
175 else => return error.TODOImplementNonFnDeclsForWasm,
176 }
177
178 count += 1;
402179 }
403180 }
404 if (new_contents_size != self.contents_size) {
405 try self.exportsec.resize(file, self.contents_size, new_contents_size);
406 leb.writeUnsignedFixed(5, &buf, new_contents_size);
407 try file.pwriteAll(&buf, self.exportsec.offset + 1);
408 }
409
410 try file.seekTo(self.exportsec.offset + Section.header_size);
411 const writer = file.writer();
412
413 // Length of the exports vec
414 leb.writeUnsignedFixed(5, &buf, export_count);
415 try writer.writeAll(&buf);
416
417 for (module.decl_exports.entries.items) |entry|
418 for (entry.value) |e| try writeExport(writer, e);
419
420 self.dirty = false;
421 }
422
423 /// Return the total number of bytes an export will take.
424 /// TODO: fixed-width LEB128 is currently used for simplicity, but should
425 /// be replaced with proper variable-length LEB128 as it is inefficient.
426 fn calcSize(e: *Module.Export) u32 {
427 // LEB128 name length + name bytes + export type + LEB128 index
428 return 5 + @intCast(u32, e.options.name.len) + 1 + 5;
181 try writeVecSectionHeader(
182 file,
183 header_offset,
184 spec.exports_id,
185 @intCast(u32, (try file.getPos()) - header_offset - header_size),
186 count,
187 );
188 }
189
190 // Code section
191 {
192 const header_offset = try reserveVecSectionHeader(file);
193 for (self.funcs.items) |decl| try file.writeAll(decl.fn_link.wasm.?.code.items);
194 try writeVecSectionHeader(
195 file,
196 header_offset,
197 spec.code_id,
198 @intCast(u32, (try file.getPos()) - header_offset - header_size),
199 @intCast(u32, self.funcs.items.len),
200 );
429201 }
202}
430203
431 /// Write the data for a single export to the given file at a given offset.
432 /// TODO: fixed-width LEB128 is currently used for simplicity, but should
433 /// be replaced with proper variable-length LEB128 as it is inefficient.
434 fn writeExport(writer: anytype, e: *Module.Export) !void {
435 var buf: [5]u8 = undefined;
204/// Get the current index of a given Decl in the function list
205/// TODO: we could maintain a hash map to potentially make this
206fn getFuncidx(self: Wasm, decl: *Module.Decl) ?u32 {
207 return for (self.funcs.items) |func, idx| {
208 if (func == decl) break @intCast(u32, idx);
209 } else null;
210}
436211
437 // Export name length + name
438 leb.writeUnsignedFixed(5, &buf, @intCast(u32, e.options.name.len));
439 try writer.writeAll(&buf);
440 try writer.writeAll(e.options.name);
212fn reserveVecSectionHeader(file: fs.File) !u64 {
213 // section id + fixed leb contents size + fixed leb vector length
214 const header_size = 1 + 5 + 5;
215 // TODO: this should be a single lseek(2) call, but fs.File does not
216 // currently provide a way to do this.
217 try file.seekBy(header_size);
218 return (try file.getPos()) - header_size;
219}
441220
442 switch (e.exported_decl.typed_value.most_recent.typed_value.ty.zigTypeTag()) {
443 .Fn => {
444 // Type of the export
445 try writer.writeByte(0x00);
446 // Exported function index
447 leb.writeUnsignedFixed(5, &buf, e.exported_decl.fn_link.wasm.?.funcidx);
448 try writer.writeAll(&buf);
449 },
450 else => return error.TODOImplementNonFnDeclsForWasm,
451 }
452 }
453};
221fn writeVecSectionHeader(file: fs.File, offset: u64, section: u8, size: u32, items: u32) !void {
222 var buf: [1 + 5 + 5]u8 = undefined;
223 buf[0] = section;
224 leb.writeUnsignedFixed(5, buf[1..6], size);
225 leb.writeUnsignedFixed(5, buf[6..], items);
226 try file.pwriteAll(&buf, offset);
227}