authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-11-12 15:45:10-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-11-12 23:21:21-05:00
log91b897ef581aec65dd7a080754ab09d2e176ed8f
tree6c6a7a2a3776d579619c6ed1cda49f5392f63640
parent53500a57684665e08a2e18e7a736aacfa6548062

rework memory management of Module.Namespace hash maps

The motivating problem here was a memory leak in the hash maps of Module.Namespace. The commit deletes more of the legacy incremental compilation implementation. It had things like use of orderedRemove and trying to do too much OOP-style creation and deletion of objects. Instead, this commit iterates over all the namespaces on Module deinit and calls deinit on the hash map fields. This logic is much simpler to reason about. Similarly, change global inline assembly to an array hash map since iterating over the values is a primary use of it, and clean up the remaining values on Module deinit, solving another memory leak. After this there are no more memory leaks remaining when using the x86 backend in a libc-less compiler.

5 files changed, 25 insertions(+), 194 deletions(-)

src/Compilation.zig-22
......@@ -2522,18 +2522,6 @@ pub fn update(comp: *Compilation, main_progress_node: *std.Progress.Node) !void
25222522 try module.populateTestFunctions(main_progress_node);
25232523 }
25242524
2525 // Process the deletion set. We use a while loop here because the
2526 // deletion set may grow as we call `clearDecl` within this loop,
2527 // and more unreferenced Decls are revealed.
2528 while (module.deletion_set.count() != 0) {
2529 const decl_index = module.deletion_set.keys()[0];
2530 const decl = module.declPtr(decl_index);
2531 assert(decl.deletion_flag);
2532 assert(decl.zir_decl_index != .none);
2533
2534 try module.clearDecl(decl_index, null);
2535 }
2536
25372525 try module.processExports();
25382526 }
25392527
......@@ -3686,16 +3674,6 @@ pub fn performAllTheWork(
36863674 try reportMultiModuleErrors(mod);
36873675 }
36883676
3689 {
3690 const outdated_and_deleted_decls_frame = tracy.namedFrame("outdated_and_deleted_decls");
3691 defer outdated_and_deleted_decls_frame.end();
3692
3693 // Iterate over all the files and look for outdated and deleted declarations.
3694 if (comp.bin_file.options.module) |mod| {
3695 try mod.processOutdatedAndDeletedDecls();
3696 }
3697 }
3698
36993677 if (comp.bin_file.options.module) |mod| {
37003678 mod.sema_prog_node = main_progress_node.start("Semantic Analysis", 0);
37013679 mod.sema_prog_node.activate();
src/InternPool.zig+5-2
......@@ -6151,7 +6151,6 @@ fn finishFuncInstance(
61516151 .@"linksection" = section,
61526152 .@"addrspace" = fn_owner_decl.@"addrspace",
61536153 .analysis = .complete,
6154 .deletion_flag = false,
61556154 .zir_decl_index = fn_owner_decl.zir_decl_index,
61566155 .src_scope = fn_owner_decl.src_scope,
61576156 .generation = generation,
......@@ -7617,7 +7616,11 @@ pub fn createNamespace(
76177616}
76187617
76197618pub fn destroyNamespace(ip: *InternPool, gpa: Allocator, index: Module.Namespace.Index) void {
7620 ip.namespacePtr(index).* = undefined;
7619 ip.namespacePtr(index).* = .{
7620 .parent = undefined,
7621 .file_scope = undefined,
7622 .ty = undefined,
7623 };
76217624 ip.namespaces_free_list.append(gpa, index) catch {
76227625 // In order to keep `destroyNamespace` a non-fallible function, we ignore memory
76237626 // allocation failures here, instead leaking the Namespace until garbage collection.
src/Module.zig+14-166
......@@ -132,10 +132,6 @@ failed_exports: std.AutoArrayHashMapUnmanaged(*Export, *ErrorMsg) = .{},
132132/// are stored here.
133133cimport_errors: std.AutoArrayHashMapUnmanaged(Decl.Index, std.zig.ErrorBundle) = .{},
134134
135/// Candidates for deletion. After a semantic analysis update completes, this list
136/// contains Decls that need to be deleted if they end up having no references to them.
137deletion_set: std.AutoArrayHashMapUnmanaged(Decl.Index, void) = .{},
138
139135/// Key is the error name, index is the error tag value. Index 0 has a length-0 string.
140136global_error_set: GlobalErrorSet = .{},
141137
......@@ -165,7 +161,7 @@ emit_h: ?*GlobalEmitH,
165161
166162test_functions: std.AutoArrayHashMapUnmanaged(Decl.Index, void) = .{},
167163
168global_assembly: std.AutoHashMapUnmanaged(Decl.Index, []u8) = .{},
164global_assembly: std.AutoArrayHashMapUnmanaged(Decl.Index, []u8) = .{},
169165
170166reference_table: std.AutoHashMapUnmanaged(Decl.Index, struct {
171167 referencer: Decl.Index,
......@@ -438,9 +434,6 @@ pub const Decl = struct {
438434 /// with it. That means when `Decl` is destroyed, the cleanup code should additionally
439435 /// check if the value owns a `Namespace`, and destroy that too.
440436 owns_tv: bool,
441 /// This flag is set when this Decl is added to `Module.deletion_set`, and cleared
442 /// when removed.
443 deletion_flag: bool,
444437 /// Whether the corresponding AST decl has a `pub` keyword.
445438 is_pub: bool,
446439 /// Whether the corresponding AST decl has a `export` keyword.
......@@ -873,47 +866,6 @@ pub const Namespace = struct {
873866 }
874867 };
875868
876 pub fn deinit(ns: *Namespace, mod: *Module) void {
877 ns.destroyDecls(mod);
878 ns.* = undefined;
879 }
880
881 pub fn destroyDecls(ns: *Namespace, mod: *Module) void {
882 const gpa = mod.gpa;
883
884 var decls = ns.decls;
885 ns.decls = .{};
886
887 for (decls.keys()) |decl_index| {
888 mod.destroyDecl(decl_index);
889 }
890 decls.deinit(gpa);
891
892 ns.usingnamespace_set.deinit(gpa);
893 }
894
895 pub fn deleteAllDecls(
896 ns: *Namespace,
897 mod: *Module,
898 outdated_decls: ?*std.AutoArrayHashMap(Decl.Index, void),
899 ) !void {
900 const gpa = mod.gpa;
901
902 var decls = ns.decls;
903 ns.decls = .{};
904
905 // TODO rework this code to not panic on OOM.
906 // (might want to coordinate with the clearDecl function)
907
908 for (decls.keys()) |child_decl| {
909 mod.clearDecl(child_decl, outdated_decls) catch @panic("out of memory");
910 mod.destroyDecl(child_decl);
911 }
912 decls.deinit(gpa);
913
914 ns.usingnamespace_set.deinit(gpa);
915 }
916
917869 // This renders e.g. "std.fs.Dir.OpenOptions"
918870 pub fn renderFullyQualifiedName(
919871 ns: Namespace,
......@@ -2527,7 +2479,6 @@ pub fn deinit(mod: *Module) void {
25272479 mod.embed_table.deinit(gpa);
25282480 }
25292481
2530 mod.deletion_set.deinit(gpa);
25312482 mod.compile_log_text.deinit(gpa);
25322483
25332484 mod.zig_cache_artifact_directory.handle.close();
......@@ -2590,9 +2541,21 @@ pub fn deinit(mod: *Module) void {
25902541
25912542 mod.test_functions.deinit(gpa);
25922543
2544 for (mod.global_assembly.values()) |s| {
2545 gpa.free(s);
2546 }
25932547 mod.global_assembly.deinit(gpa);
2548
25942549 mod.reference_table.deinit(gpa);
25952550
2551 {
2552 var it = mod.intern_pool.allocated_namespaces.iterator(0);
2553 while (it.next()) |namespace| {
2554 namespace.decls.deinit(gpa);
2555 namespace.usingnamespace_set.deinit(gpa);
2556 }
2557 }
2558
25962559 mod.intern_pool.deinit(gpa);
25972560 mod.tmp_hack_arena.deinit();
25982561
......@@ -2606,20 +2569,10 @@ pub fn destroyDecl(mod: *Module, decl_index: Decl.Index) void {
26062569 const ip = &mod.intern_pool;
26072570
26082571 {
2609 const decl = mod.declPtr(decl_index);
26102572 _ = mod.test_functions.swapRemove(decl_index);
2611 if (decl.deletion_flag) {
2612 assert(mod.deletion_set.swapRemove(decl_index));
2613 }
2614 if (mod.global_assembly.fetchRemove(decl_index)) |kv| {
2573 if (mod.global_assembly.fetchSwapRemove(decl_index)) |kv| {
26152574 gpa.free(kv.value);
26162575 }
2617 if (decl.has_tv) {
2618 if (decl.getOwnedInnerNamespaceIndex(mod).unwrap()) |i| {
2619 mod.namespacePtr(i).destroyDecls(mod);
2620 mod.destroyNamespace(i);
2621 }
2622 }
26232576 }
26242577
26252578 ip.destroyDecl(gpa, decl_index);
......@@ -4422,56 +4375,6 @@ fn scanDecl(iter: *ScanDeclIter, decl_sub_index: usize, flags: u4) Allocator.Err
44224375 }
44234376}
44244377
4425/// Make it as if the semantic analysis for this Decl never happened.
4426pub fn clearDecl(
4427 mod: *Module,
4428 decl_index: Decl.Index,
4429 outdated_decls: ?*std.AutoArrayHashMap(Decl.Index, void),
4430) Allocator.Error!void {
4431 const tracy = trace(@src());
4432 defer tracy.end();
4433
4434 const decl = mod.declPtr(decl_index);
4435
4436 const gpa = mod.gpa;
4437
4438 if (outdated_decls) |map| {
4439 _ = map.swapRemove(decl_index);
4440 }
4441
4442 if (mod.failed_decls.fetchSwapRemove(decl_index)) |kv| {
4443 kv.value.destroy(gpa);
4444 }
4445 if (mod.cimport_errors.fetchSwapRemove(decl_index)) |kv| {
4446 var errors = kv.value;
4447 errors.deinit(gpa);
4448 }
4449 if (mod.emit_h) |emit_h| {
4450 if (emit_h.failed_decls.fetchSwapRemove(decl_index)) |kv| {
4451 kv.value.destroy(gpa);
4452 }
4453 assert(emit_h.decl_table.swapRemove(decl_index));
4454 }
4455 _ = mod.compile_log_decls.swapRemove(decl_index);
4456 try mod.deleteDeclExports(decl_index);
4457
4458 if (decl.has_tv) {
4459 if (decl.ty.isFnOrHasRuntimeBits(mod)) {
4460 mod.comp.bin_file.freeDecl(decl_index);
4461 }
4462 if (decl.getOwnedInnerNamespace(mod)) |namespace| {
4463 try namespace.deleteAllDecls(mod, outdated_decls);
4464 }
4465 }
4466
4467 if (decl.deletion_flag) {
4468 decl.deletion_flag = false;
4469 assert(mod.deletion_set.swapRemove(decl_index));
4470 }
4471
4472 decl.analysis = .unreferenced;
4473}
4474
44754378/// This function is exclusively called for anonymous decls.
44764379/// All resources referenced by anonymous decls are owned by InternPool
44774380/// so there is no cleanup to do here.
......@@ -4488,14 +4391,6 @@ pub fn deleteUnusedDecl(mod: *Module, decl_index: Decl.Index) void {
44884391 }
44894392}
44904393
4491/// We don't perform a deletion here, because this Decl or another one
4492/// may end up referencing it before the update is complete.
4493fn markDeclForDeletion(mod: *Module, decl_index: Decl.Index) !void {
4494 const decl = mod.declPtr(decl_index);
4495 decl.deletion_flag = true;
4496 try mod.deletion_set.put(mod.gpa, decl_index, {});
4497}
4498
44994394/// Cancel the creation of an anon decl and delete any references to it.
45004395/// If other decls depend on this decl, they must be aborted first.
45014396pub fn abortAnonDecl(mod: *Module, decl_index: Decl.Index) void {
......@@ -4868,7 +4763,6 @@ pub fn allocateNewDecl(
48684763 .@"linksection" = .none,
48694764 .@"addrspace" = .generic,
48704765 .analysis = .unreferenced,
4871 .deletion_flag = false,
48724766 .zir_decl_index = .none,
48734767 .src_scope = src_scope,
48744768 .generation = 0,
......@@ -5366,52 +5260,6 @@ pub fn optionsSrc(mod: *Module, decl: *Decl, base_src: LazySrcLoc, wanted: []con
53665260 return base_src;
53675261}
53685262
5369/// Called from `performAllTheWork`, after all AstGen workers have finished,
5370/// and before the main semantic analysis loop begins.
5371pub fn processOutdatedAndDeletedDecls(mod: *Module) !void {
5372 // Ultimately, the goal is to queue up `analyze_decl` tasks in the work queue
5373 // for the outdated decls, but we cannot queue up the tasks until after
5374 // we find out which ones have been deleted, otherwise there would be
5375 // deleted Decl pointers in the work queue.
5376 var outdated_decls = std.AutoArrayHashMap(Decl.Index, void).init(mod.gpa);
5377 defer outdated_decls.deinit();
5378 for (mod.import_table.values()) |file| {
5379 try outdated_decls.ensureUnusedCapacity(file.outdated_decls.items.len);
5380 for (file.outdated_decls.items) |decl_index| {
5381 outdated_decls.putAssumeCapacity(decl_index, {});
5382 }
5383 file.outdated_decls.clearRetainingCapacity();
5384
5385 // Handle explicitly deleted decls from the source code. This is one of two
5386 // places that Decl deletions happen. The other is in `Compilation`, after
5387 // `performAllTheWork`, where we iterate over `Module.deletion_set` and
5388 // delete Decls which are no longer referenced.
5389 // If a Decl is explicitly deleted from source, and also no longer referenced,
5390 // it may be both in this `deleted_decls` set, as well as in the
5391 // `Module.deletion_set`. To avoid deleting it twice, we remove it from the
5392 // deletion set at this time.
5393 for (file.deleted_decls.items) |decl_index| {
5394 const decl = mod.declPtr(decl_index);
5395
5396 // Remove from the namespace it resides in, preserving declaration order.
5397 assert(decl.zir_decl_index != .none);
5398 _ = mod.namespacePtr(decl.src_namespace).decls.orderedRemoveAdapted(
5399 decl.name,
5400 DeclAdapter{ .mod = mod },
5401 );
5402
5403 try mod.clearDecl(decl_index, &outdated_decls);
5404 mod.destroyDecl(decl_index);
5405 }
5406 file.deleted_decls.clearRetainingCapacity();
5407 }
5408 // Finally we can queue up re-analysis tasks after we have processed
5409 // the deleted decls.
5410 for (outdated_decls.keys()) |key| {
5411 try mod.markOutdatedDecl(key);
5412 }
5413}
5414
54155263/// Called from `Compilation.update`, after everything is done, just before
54165264/// reporting compile errors. In this function we emit exported symbol collision
54175265/// errors and communicate exported symbols to the linker backend.
src/codegen/c.zig+3-2
......@@ -2506,8 +2506,9 @@ pub fn genTypeDecl(
25062506}
25072507
25082508pub fn genGlobalAsm(mod: *Module, writer: anytype) !void {
2509 var it = mod.global_assembly.valueIterator();
2510 while (it.next()) |asm_source| try writer.print("__asm({s});\n", .{fmtStringLiteral(asm_source.*, null)});
2509 for (mod.global_assembly.values()) |asm_source| {
2510 try writer.print("__asm({s});\n", .{fmtStringLiteral(asm_source, null)});
2511 }
25112512}
25122513
25132514pub fn genErrDecls(o: *Object) !void {
src/codegen/llvm.zig+3-2
......@@ -1121,8 +1121,9 @@ pub const Object = struct {
11211121 const mod = object.module;
11221122
11231123 const writer = object.builder.setModuleAsm();
1124 var it = mod.global_assembly.valueIterator();
1125 while (it.next()) |assembly| try writer.print("{s}\n", .{assembly.*});
1124 for (mod.global_assembly.values()) |assembly| {
1125 try writer.print("{s}\n", .{assembly});
1126 }
11261127 try object.builder.finishModuleAsm();
11271128 }
11281129