authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-09-27 18:20:00-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-09-28 16:28:41-07:00
log8f2f12f94080ede762df4a5018249160eedc7f09
tree40df4f059ae5c271102dcb2dc73be3be318e32d6
parent077994abb66be93f38142ee55243fee8cdc3a5da

Compilation: introduce saveState()

This commit introduces `--debug-incremental` so that we can start playing around with incremental compilation while it is still being developed, and before it is enabled by default. Currently it saves InternPool data, and has TODO comments for the remaining things. Deserialization is not implemented yet, which will require some post-processing such as to build a string map out of null-terminated string table bytes. The saved compiler state is stored in a file called <root-name>.zcs alongside <root-name>.o, <root-name>.pdb, <root-name>.exe, etc. In case of using the zig build system, these files are all in a zig-cache directory. For the self-hosted compiler, here is one data point on the performance penalty of saving this data: ``` Benchmark 1 (3 runs): zig build-exe ... measurement mean ± σ min … max outliers delta wall_time 51.1s ± 354ms 50.7s … 51.4s 0 ( 0%) 0% peak_rss 3.91GB ± 354KB 3.91GB … 3.91GB 0 ( 0%) 0% cpu_cycles 212G ± 3.17G 210G … 216G 0 ( 0%) 0% instructions 274G ± 57.5M 274G … 275G 0 ( 0%) 0% cache_references 13.1G ± 97.6M 13.0G … 13.2G 0 ( 0%) 0% cache_misses 1.12G ± 24.6M 1.10G … 1.15G 0 ( 0%) 0% branch_misses 1.53G ± 1.46M 1.53G … 1.53G 0 ( 0%) 0% Benchmark 2 (3 runs): zig build-exe ... --debug-incremental measurement mean ± σ min … max outliers delta wall_time 51.8s ± 271ms 51.5s … 52.1s 0 ( 0%) + 1.3% ± 1.4% peak_rss 3.91GB ± 317KB 3.91GB … 3.91GB 0 ( 0%) - 0.0% ± 0.0% cpu_cycles 213G ± 398M 212G … 213G 0 ( 0%) + 0.3% ± 2.4% instructions 275G ± 79.1M 275G … 275G 0 ( 0%) + 0.1% ± 0.1% cache_references 13.1G ± 26.9M 13.0G … 13.1G 0 ( 0%) - 0.1% ± 1.2% cache_misses 1.12G ± 5.66M 1.11G … 1.12G 0 ( 0%) - 0.6% ± 3.6% branch_misses 1.53G ± 1.75M 1.53G … 1.54G 0 ( 0%) + 0.2% ± 0.2% ``` At the end of each compilation with `--debug-incremental`, we end up with a 43 MiB `zig.zcs` file that contains all of the InternPool data serialized. Of course, it will necessarily be more expensive to save the state than to not save the state. However, this data point shows just how cheap the save state operation is, with all of the groundwork laid for using a serialization-friendly in-memory data layout.

2 files changed, 84 insertions(+), 1 deletions(-)

src/Compilation.zig+66
......@@ -2702,6 +2702,72 @@ pub fn makeBinFileWritable(self: *Compilation) !void {
27022702 return self.bin_file.makeWritable();
27032703}
27042704
2705const Header = extern struct {
2706 intern_pool: extern struct {
2707 items_len: u32,
2708 extra_len: u32,
2709 limbs_len: u32,
2710 string_bytes_len: u32,
2711 },
2712};
2713
2714/// Note that all state that is included in the cache hash namespace is *not*
2715/// saved, such as the target and most CLI flags. A cache hit will only occur
2716/// when subsequent compiler invocations use the same set of flags.
2717pub fn saveState(comp: *Compilation) !void {
2718 var bufs_list: [6]std.os.iovec_const = undefined;
2719 var bufs_len: usize = 0;
2720
2721 const emit = comp.bin_file.options.emit orelse return;
2722
2723 if (comp.bin_file.options.module) |mod| {
2724 const ip = &mod.intern_pool;
2725 const header: Header = .{
2726 .intern_pool = .{
2727 .items_len = @intCast(ip.items.len),
2728 .extra_len = @intCast(ip.extra.items.len),
2729 .limbs_len = @intCast(ip.limbs.items.len),
2730 .string_bytes_len = @intCast(ip.string_bytes.items.len),
2731 },
2732 };
2733 addBuf(&bufs_list, &bufs_len, mem.asBytes(&header));
2734 addBuf(&bufs_list, &bufs_len, mem.sliceAsBytes(ip.limbs.items));
2735 addBuf(&bufs_list, &bufs_len, mem.sliceAsBytes(ip.extra.items));
2736 addBuf(&bufs_list, &bufs_len, mem.sliceAsBytes(ip.items.items(.data)));
2737 addBuf(&bufs_list, &bufs_len, mem.sliceAsBytes(ip.items.items(.tag)));
2738 addBuf(&bufs_list, &bufs_len, ip.string_bytes.items);
2739
2740 // TODO: compilation errors
2741 // TODO: files
2742 // TODO: namespaces
2743 // TODO: decls
2744 // TODO: linker state
2745 }
2746 var basename_buf: [255]u8 = undefined;
2747 const basename = std.fmt.bufPrint(&basename_buf, "{s}.zcs", .{
2748 comp.bin_file.options.root_name,
2749 }) catch o: {
2750 basename_buf[basename_buf.len - 4 ..].* = ".zcs".*;
2751 break :o &basename_buf;
2752 };
2753
2754 // Using an atomic file prevents a crash or power failure from corrupting
2755 // the previous incremental compilation state.
2756 var af = try emit.directory.handle.atomicFile(basename, .{});
2757 defer af.deinit();
2758 try af.file.pwritevAll(bufs_list[0..bufs_len], 0);
2759 try af.finish();
2760}
2761
2762fn addBuf(bufs_list: []std.os.iovec_const, bufs_len: *usize, buf: []const u8) void {
2763 const i = bufs_len.*;
2764 bufs_len.* = i + 1;
2765 bufs_list[i] = .{
2766 .iov_base = buf.ptr,
2767 .iov_len = buf.len,
2768 };
2769}
2770
27052771/// This function is temporally single-threaded.
27062772pub fn totalErrorCount(self: *Compilation) u32 {
27072773 var total: usize = self.failed_c_objects.count() +
src/main.zig+18-1
......@@ -594,6 +594,7 @@ const usage_build_generic =
594594 \\ --debug-log [scope] Enable printing debug/info log messages for scope
595595 \\ --debug-compile-errors Crash with helpful diagnostics at the first compile error
596596 \\ --debug-link-snapshot Enable dumping of the linker's state in JSON format
597 \\ --debug-incremental Enable experimental feature: incremental compilation
597598 \\
598599;
599600
......@@ -904,6 +905,7 @@ fn buildOutputType(
904905 var minor_subsystem_version: ?u32 = null;
905906 var wasi_exec_model: ?std.builtin.WasiExecModel = null;
906907 var enable_link_snapshots: bool = false;
908 var debug_incremental: bool = false;
907909 var install_name: ?[]const u8 = null;
908910 var hash_style: link.HashStyle = .both;
909911 var entitlements: ?[]const u8 = null;
......@@ -1272,6 +1274,8 @@ fn buildOutputType(
12721274 } else {
12731275 enable_link_snapshots = true;
12741276 }
1277 } else if (mem.eql(u8, arg, "--debug-incremental")) {
1278 debug_incremental = true;
12751279 } else if (mem.eql(u8, arg, "--entitlements")) {
12761280 entitlements = args_iter.nextOrFatal();
12771281 } else if (mem.eql(u8, arg, "-fcompiler-rt")) {
......@@ -3591,11 +3595,16 @@ fn buildOutputType(
35913595 }
35923596
35933597 updateModule(comp) catch |err| switch (err) {
3594 error.SemanticAnalyzeFail => if (listen == .none) process.exit(1),
3598 error.SemanticAnalyzeFail => {
3599 assert(listen == .none);
3600 saveState(comp, debug_incremental);
3601 process.exit(1);
3602 },
35953603 else => |e| return e,
35963604 };
35973605 if (build_options.only_c) return cleanExit();
35983606 try comp.makeBinFileExecutable();
3607 saveState(comp, debug_incremental);
35993608
36003609 if (test_exec_args.items.len == 0 and object_format == .c) default_exec_args: {
36013610 // Default to using `zig run` to execute the produced .c code from `zig test`.
......@@ -3658,6 +3667,14 @@ fn buildOutputType(
36583667 return cleanExit();
36593668}
36603669
3670fn saveState(comp: *Compilation, debug_incremental: bool) void {
3671 if (debug_incremental) {
3672 comp.saveState() catch |err| {
3673 warn("unable to save incremental compilation state: {s}", .{@errorName(err)});
3674 };
3675 }
3676}
3677
36613678fn serve(
36623679 comp: *Compilation,
36633680 in: fs.File,