stage2: free Sema's arena after generating machine code

Previously, linker backends or machine code backends were able to hold
on to references to inside Sema's temporary arena. However there can
be large objects stored there that we want to free after machine code is
generated.

The primary change in this commit is to use a temporary arena for Sema
of function bodies that gets freed after machine code backend finishes
handling `updateFunc` (at the same time that Air and Liveness get freed).

The other changes in this commit are fixing issues that fell out from
the primary change.

 * The C linker backend is rewritten to handle updateDecl and updateFunc
   separately. Also, all Decl updates get access to typedefs and
   fwd_decls, not only functions.
 * The C linker backend is updated to the new API that does not depend
   on allocateDeclIndexes and does not have to handle garbage collected
   decls.
 * The C linker backend uses an arena for Type/Value objects that
   `typedefs` references. These can be garbage collected every so often
   after flush(), however that garbage collection code is not
   implemented at this time. It will be pretty simple, just allocate a
   new arena, copy all the Type objects to it, update the keys of the
   hash map, free the old arena.
 * Sema: fix a handful of instances of not copying Type/Value objects
   from the temporary arena into the appropriate Decl arena.
 * Type: fix some function types not reporting hasCodeGenBits()
   correctly.
This commit is contained in:
Andrew Kelley 2021-09-21 15:08:32 -07:00
parent affd8f8b59
commit 5913140b6b
7 changed files with 762 additions and 651 deletions

View File

@ -2145,7 +2145,11 @@ pub fn performAllTheWork(self: *Compilation) error{ TimerUnsupported, OutOfMemor
const module = self.bin_file.options.module.?;
const decl = func.owner_decl;
var air = module.analyzeFnBody(decl, func) catch |err| switch (err) {
var tmp_arena = std.heap.ArenaAllocator.init(gpa);
defer tmp_arena.deinit();
const sema_arena = &tmp_arena.allocator;
var air = module.analyzeFnBody(decl, func, sema_arena) catch |err| switch (err) {
error.AnalysisFail => {
assert(func.state != .in_progress);
continue;
@ -2207,16 +2211,20 @@ pub fn performAllTheWork(self: *Compilation) error{ TimerUnsupported, OutOfMemor
const decl_emit_h = decl.getEmitH(module);
const fwd_decl = &decl_emit_h.fwd_decl;
fwd_decl.shrinkRetainingCapacity(0);
var typedefs_arena = std.heap.ArenaAllocator.init(gpa);
defer typedefs_arena.deinit();
var dg: c_codegen.DeclGen = .{
.gpa = gpa,
.module = module,
.error_msg = null,
.decl = decl,
.fwd_decl = fwd_decl.toManaged(gpa),
// we don't want to emit optionals and error unions to headers since they have no ABI
.typedefs = undefined,
.typedefs = c_codegen.TypedefMap.init(gpa),
.typedefs_arena = &typedefs_arena.allocator,
};
defer dg.fwd_decl.deinit();
defer dg.typedefs.deinit();
c_codegen.genHeader(&dg) catch |err| switch (err) {
error.AnalysisFail => {

View File

@ -610,7 +610,7 @@ pub const Decl = struct {
/// If the Decl has a value and it is a function, return it,
/// otherwise null.
pub fn getFunction(decl: *Decl) ?*Fn {
pub fn getFunction(decl: *const Decl) ?*Fn {
if (!decl.owns_tv) return null;
const func = (decl.val.castTag(.function) orelse return null).data;
assert(func.owner_decl == decl);
@ -3789,7 +3789,7 @@ pub fn clearDecl(
.elf => .{ .elf = link.File.Elf.TextBlock.empty },
.macho => .{ .macho = link.File.MachO.TextBlock.empty },
.plan9 => .{ .plan9 = link.File.Plan9.DeclBlock.empty },
.c => .{ .c = link.File.C.DeclBlock.empty },
.c => .{ .c = {} },
.wasm => .{ .wasm = link.File.Wasm.DeclBlock.empty },
.spirv => .{ .spirv = {} },
};
@ -3798,7 +3798,7 @@ pub fn clearDecl(
.elf => .{ .elf = link.File.Elf.SrcFn.empty },
.macho => .{ .macho = link.File.MachO.SrcFn.empty },
.plan9 => .{ .plan9 = {} },
.c => .{ .c = link.File.C.FnBlock.empty },
.c => .{ .c = {} },
.wasm => .{ .wasm = link.File.Wasm.FnData.empty },
.spirv => .{ .spirv = .{} },
};
@ -3828,10 +3828,13 @@ pub fn deleteUnusedDecl(mod: *Module, decl: *Decl) void {
// about the Decl in the first place.
// Until then, we did call `allocateDeclIndexes` on this anonymous Decl and so we
// must call `freeDecl` in the linker backend now.
if (decl.has_tv) {
if (decl.ty.hasCodeGenBits()) {
mod.comp.bin_file.freeDecl(decl);
}
switch (mod.comp.bin_file.tag) {
.c => {}, // this linker backend has already migrated to the new API
else => if (decl.has_tv) {
if (decl.ty.hasCodeGenBits()) {
mod.comp.bin_file.freeDecl(decl);
}
},
}
const dependants = decl.dependants.keys();
@ -3893,22 +3896,16 @@ fn deleteDeclExports(mod: *Module, decl: *Decl) void {
mod.gpa.free(kv.value);
}
pub fn analyzeFnBody(mod: *Module, decl: *Decl, func: *Fn) SemaError!Air {
pub fn analyzeFnBody(mod: *Module, decl: *Decl, func: *Fn, arena: *Allocator) SemaError!Air {
const tracy = trace(@src());
defer tracy.end();
const gpa = mod.gpa;
// Use the Decl's arena for function memory.
var arena = decl.value_arena.?.promote(gpa);
defer decl.value_arena.?.* = arena.state;
const fn_ty = decl.ty;
var sema: Sema = .{
.mod = mod,
.gpa = gpa,
.arena = &arena.allocator,
.arena = arena,
.code = decl.namespace.file_scope.zir,
.owner_decl = decl,
.namespace = decl.namespace,
@ -3942,6 +3939,7 @@ pub fn analyzeFnBody(mod: *Module, decl: *Decl, func: *Fn) SemaError!Air {
// This could be a generic function instantiation, however, in which case we need to
// map the comptime parameters to constant values and only emit arg AIR instructions
// for the runtime ones.
const fn_ty = decl.ty;
const runtime_params_len = @intCast(u32, fn_ty.fnParamLen());
try inner_block.instructions.ensureTotalCapacity(gpa, runtime_params_len);
try sema.air_instructions.ensureUnusedCapacity(gpa, fn_info.total_params_len * 2); // * 2 for the `addType`
@ -4072,7 +4070,7 @@ pub fn allocateNewDecl(mod: *Module, namespace: *Scope.Namespace, src_node: Ast.
.elf => .{ .elf = link.File.Elf.TextBlock.empty },
.macho => .{ .macho = link.File.MachO.TextBlock.empty },
.plan9 => .{ .plan9 = link.File.Plan9.DeclBlock.empty },
.c => .{ .c = link.File.C.DeclBlock.empty },
.c => .{ .c = {} },
.wasm => .{ .wasm = link.File.Wasm.DeclBlock.empty },
.spirv => .{ .spirv = {} },
},
@ -4081,7 +4079,7 @@ pub fn allocateNewDecl(mod: *Module, namespace: *Scope.Namespace, src_node: Ast.
.elf => .{ .elf = link.File.Elf.SrcFn.empty },
.macho => .{ .macho = link.File.MachO.SrcFn.empty },
.plan9 => .{ .plan9 = {} },
.c => .{ .c = link.File.C.FnBlock.empty },
.c => .{ .c = {} },
.wasm => .{ .wasm = link.File.Wasm.FnData.empty },
.spirv => .{ .spirv = .{} },
},

View File

@ -2999,6 +2999,8 @@ fn analyzeCall(
// TODO: check whether any external comptime memory was mutated by the
// comptime function call. If so, then do not memoize the call here.
// TODO: re-evaluate whether memoized_calls needs its own arena. I think
// it should be fine to use the Decl arena for the function.
{
var arena_allocator = std.heap.ArenaAllocator.init(gpa);
errdefer arena_allocator.deinit();
@ -3009,7 +3011,7 @@ fn analyzeCall(
}
try mod.memoized_calls.put(gpa, memoized_call_key, .{
.val = result_val,
.val = try result_val.copy(arena),
.arena = arena_allocator.state,
});
delete_memoized_call_key = false;
@ -5876,10 +5878,7 @@ fn zirArrayCat(sema: *Sema, block: *Scope.Block, inst: Zir.Inst.Index) CompileEr
else
try Type.Tag.array.create(anon_decl.arena(), .{ .len = final_len, .elem_type = lhs_info.elem_type });
const val = try Value.Tag.array.create(anon_decl.arena(), buf);
return sema.analyzeDeclRef(try anon_decl.finish(
ty,
val,
));
return sema.analyzeDeclRef(try anon_decl.finish(ty, val));
}
return sema.mod.fail(&block.base, lhs_src, "TODO array_cat more types of Values", .{});
} else {
@ -5941,10 +5940,7 @@ fn zirArrayMul(sema: *Sema, block: *Scope.Block, inst: Zir.Inst.Index) CompileEr
}
}
const val = try Value.Tag.array.create(anon_decl.arena(), buf);
return sema.analyzeDeclRef(try anon_decl.finish(
final_ty,
val,
));
return sema.analyzeDeclRef(try anon_decl.finish(final_ty, val));
}
return sema.mod.fail(&block.base, lhs_src, "TODO array_mul more types of Values", .{});
}
@ -9979,7 +9975,7 @@ fn analyzeRef(
var anon_decl = try block.startAnonDecl();
defer anon_decl.deinit();
return sema.analyzeDeclRef(try anon_decl.finish(
operand_ty,
try operand_ty.copy(anon_decl.arena()),
try val.copy(anon_decl.arena()),
));
}

File diff suppressed because it is too large Load Diff

View File

@ -149,7 +149,7 @@ pub const File = struct {
coff: Coff.TextBlock,
macho: MachO.TextBlock,
plan9: Plan9.DeclBlock,
c: C.DeclBlock,
c: void,
wasm: Wasm.DeclBlock,
spirv: void,
};
@ -159,7 +159,7 @@ pub const File = struct {
coff: Coff.SrcFn,
macho: MachO.SrcFn,
plan9: void,
c: C.FnBlock,
c: void,
wasm: Wasm.FnData,
spirv: SpirV.FnData,
};
@ -372,16 +372,18 @@ pub const File = struct {
/// Must be called before any call to updateDecl or updateDeclExports for
/// any given Decl.
/// TODO we're transitioning to deleting this function and instead having
/// each linker backend notice the first time updateDecl or updateFunc is called, or
/// a callee referenced from AIR.
pub fn allocateDeclIndexes(base: *File, decl: *Module.Decl) !void {
log.debug("allocateDeclIndexes {*} ({s})", .{ decl, decl.name });
switch (base.tag) {
.coff => return @fieldParentPtr(Coff, "base", base).allocateDeclIndexes(decl),
.elf => return @fieldParentPtr(Elf, "base", base).allocateDeclIndexes(decl),
.macho => return @fieldParentPtr(MachO, "base", base).allocateDeclIndexes(decl),
.c => return @fieldParentPtr(C, "base", base).allocateDeclIndexes(decl),
.wasm => return @fieldParentPtr(Wasm, "base", base).allocateDeclIndexes(decl),
.plan9 => return @fieldParentPtr(Plan9, "base", base).allocateDeclIndexes(decl),
.spirv => {},
.c, .spirv => {},
}
}

View File

@ -21,30 +21,34 @@ base: link.File,
/// This linker backend does not try to incrementally link output C source code.
/// Instead, it tracks all declarations in this table, and iterates over it
/// in the flush function, stitching pre-rendered pieces of C code together.
decl_table: std.AutoArrayHashMapUnmanaged(*Module.Decl, void) = .{},
decl_table: std.AutoArrayHashMapUnmanaged(*const Module.Decl, DeclBlock) = .{},
/// Stores Type/Value data for `typedefs` to reference.
/// Accumulates allocations and then there is a periodic garbage collection after flush().
arena: std.heap.ArenaAllocator,
/// Per-declaration data. For functions this is the body, and
/// the forward declaration is stored in the FnBlock.
pub const DeclBlock = struct {
code: std.ArrayListUnmanaged(u8),
const DeclBlock = struct {
code: std.ArrayListUnmanaged(u8) = .{},
fwd_decl: std.ArrayListUnmanaged(u8) = .{},
/// Each Decl stores a mapping of Zig Types to corresponding C types, for every
/// Zig Type used by the Decl. In flush(), we iterate over each Decl
/// and emit the typedef code for all types, making sure to not emit the same thing twice.
/// Any arena memory the Type points to lives in the `arena` field of `C`.
typedefs: codegen.TypedefMap.Unmanaged = .{},
pub const empty: DeclBlock = .{
.code = .{},
};
fn deinit(db: *DeclBlock, gpa: *Allocator) void {
db.code.deinit(gpa);
db.fwd_decl.deinit(gpa);
for (db.typedefs.values()) |typedef| {
gpa.free(typedef.rendered);
}
db.typedefs.deinit(gpa);
db.* = undefined;
}
};
/// Per-function data.
pub const FnBlock = struct {
fwd_decl: std.ArrayListUnmanaged(u8),
typedefs: codegen.TypedefMap.Unmanaged,
pub const empty: FnBlock = .{
.fwd_decl = .{},
.typedefs = .{},
};
};
pub fn openPath(allocator: *Allocator, sub_path: []const u8, options: link.Options) !*C {
pub fn openPath(gpa: *Allocator, sub_path: []const u8, options: link.Options) !*C {
assert(options.object_format == .c);
if (options.use_llvm) return error.LLVMHasNoCBackend;
@ -57,15 +61,16 @@ pub fn openPath(allocator: *Allocator, sub_path: []const u8, options: link.Optio
});
errdefer file.close();
var c_file = try allocator.create(C);
errdefer allocator.destroy(c_file);
var c_file = try gpa.create(C);
errdefer gpa.destroy(c_file);
c_file.* = C{
.arena = std.heap.ArenaAllocator.init(gpa),
.base = .{
.tag = .c,
.options = options,
.file = file,
.allocator = allocator,
.allocator = gpa,
},
};
@ -73,38 +78,105 @@ pub fn openPath(allocator: *Allocator, sub_path: []const u8, options: link.Optio
}
pub fn deinit(self: *C) void {
for (self.decl_table.keys()) |key| {
deinitDecl(self.base.allocator, key);
}
self.decl_table.deinit(self.base.allocator);
}
const gpa = self.base.allocator;
pub fn allocateDeclIndexes(self: *C, decl: *Module.Decl) !void {
_ = self;
_ = decl;
for (self.decl_table.values()) |*db| {
db.deinit(gpa);
}
self.decl_table.deinit(gpa);
self.arena.deinit();
}
pub fn freeDecl(self: *C, decl: *Module.Decl) void {
_ = self.decl_table.swapRemove(decl);
deinitDecl(self.base.allocator, decl);
}
fn deinitDecl(gpa: *Allocator, decl: *Module.Decl) void {
decl.link.c.code.deinit(gpa);
decl.fn_link.c.fwd_decl.deinit(gpa);
for (decl.fn_link.c.typedefs.values()) |value| {
gpa.free(value.rendered);
const gpa = self.base.allocator;
if (self.decl_table.fetchSwapRemove(decl)) |*kv| {
kv.value.deinit(gpa);
}
decl.fn_link.c.typedefs.deinit(gpa);
}
pub fn finishUpdateDecl(self: *C, module: *Module, decl: *Module.Decl, air: Air, liveness: Liveness) !void {
// Keep track of all decls so we can iterate over them on flush().
_ = try self.decl_table.getOrPut(self.base.allocator, decl);
pub fn updateFunc(self: *C, module: *Module, func: *Module.Fn, air: Air, liveness: Liveness) !void {
const tracy = trace(@src());
defer tracy.end();
const fwd_decl = &decl.fn_link.c.fwd_decl;
const typedefs = &decl.fn_link.c.typedefs;
const code = &decl.link.c.code;
const decl = func.owner_decl;
const gop = try self.decl_table.getOrPut(self.base.allocator, decl);
if (!gop.found_existing) {
gop.value_ptr.* = .{};
}
const fwd_decl = &gop.value_ptr.fwd_decl;
const typedefs = &gop.value_ptr.typedefs;
const code = &gop.value_ptr.code;
fwd_decl.shrinkRetainingCapacity(0);
{
for (typedefs.values()) |value| {
module.gpa.free(value.rendered);
}
}
typedefs.clearRetainingCapacity();
code.shrinkRetainingCapacity(0);
var function: codegen.Function = .{
.value_map = codegen.CValueMap.init(module.gpa),
.air = air,
.liveness = liveness,
.func = func,
.object = .{
.dg = .{
.gpa = module.gpa,
.module = module,
.error_msg = null,
.decl = decl,
.fwd_decl = fwd_decl.toManaged(module.gpa),
.typedefs = typedefs.promote(module.gpa),
.typedefs_arena = &self.arena.allocator,
},
.code = code.toManaged(module.gpa),
.indent_writer = undefined, // set later so we can get a pointer to object.code
},
};
function.object.indent_writer = .{ .underlying_writer = function.object.code.writer() };
defer {
function.value_map.deinit();
function.blocks.deinit(module.gpa);
function.object.code.deinit();
function.object.dg.fwd_decl.deinit();
for (function.object.dg.typedefs.values()) |value| {
module.gpa.free(value.rendered);
}
function.object.dg.typedefs.deinit();
}
codegen.genFunc(&function) catch |err| switch (err) {
error.AnalysisFail => {
try module.failed_decls.put(module.gpa, decl, function.object.dg.error_msg.?);
return;
},
else => |e| return e,
};
fwd_decl.* = function.object.dg.fwd_decl.moveToUnmanaged();
typedefs.* = function.object.dg.typedefs.unmanaged;
function.object.dg.typedefs.unmanaged = .{};
code.* = function.object.code.moveToUnmanaged();
// Free excess allocated memory for this Decl.
fwd_decl.shrinkAndFree(module.gpa, fwd_decl.items.len);
code.shrinkAndFree(module.gpa, code.items.len);
}
pub fn updateDecl(self: *C, module: *Module, decl: *Module.Decl) !void {
const tracy = trace(@src());
defer tracy.end();
const gop = try self.decl_table.getOrPut(self.base.allocator, decl);
if (!gop.found_existing) {
gop.value_ptr.* = .{};
}
const fwd_decl = &gop.value_ptr.fwd_decl;
const typedefs = &gop.value_ptr.typedefs;
const code = &gop.value_ptr.code;
fwd_decl.shrinkRetainingCapacity(0);
{
for (typedefs.values()) |value| {
@ -116,23 +188,19 @@ pub fn finishUpdateDecl(self: *C, module: *Module, decl: *Module.Decl, air: Air,
var object: codegen.Object = .{
.dg = .{
.gpa = module.gpa,
.module = module,
.error_msg = null,
.decl = decl,
.fwd_decl = fwd_decl.toManaged(module.gpa),
.typedefs = typedefs.promote(module.gpa),
.typedefs_arena = &self.arena.allocator,
},
.gpa = module.gpa,
.code = code.toManaged(module.gpa),
.value_map = codegen.CValueMap.init(module.gpa),
.indent_writer = undefined, // set later so we can get a pointer to object.code
.air = air,
.liveness = liveness,
};
object.indent_writer = .{ .underlying_writer = object.code.writer() };
defer {
object.value_map.deinit();
object.blocks.deinit(module.gpa);
object.code.deinit();
object.dg.fwd_decl.deinit();
for (object.dg.typedefs.values()) |value| {
@ -159,24 +227,12 @@ pub fn finishUpdateDecl(self: *C, module: *Module, decl: *Module.Decl, air: Air,
code.shrinkAndFree(module.gpa, code.items.len);
}
pub fn updateFunc(self: *C, module: *Module, func: *Module.Fn, air: Air, liveness: Liveness) !void {
const tracy = trace(@src());
defer tracy.end();
return self.finishUpdateDecl(module, func.owner_decl, air, liveness);
}
pub fn updateDecl(self: *C, module: *Module, decl: *Module.Decl) !void {
const tracy = trace(@src());
defer tracy.end();
return self.finishUpdateDecl(module, decl, undefined, undefined);
}
pub fn updateDeclLineNumber(self: *C, module: *Module, decl: *Module.Decl) !void {
// The C backend does not have the ability to fix line numbers without re-generating
// the entire Decl.
return self.updateDecl(module, decl);
_ = self;
_ = module;
_ = decl;
}
pub fn flush(self: *C, comp: *Compilation) !void {
@ -223,32 +279,42 @@ pub fn flushModule(self: *C, comp: *Compilation) !void {
var typedefs = std.HashMap(Type, void, Type.HashContext64, std.hash_map.default_max_load_percentage).init(comp.gpa);
defer typedefs.deinit();
// Typedefs, forward decls and non-functions first.
// Typedefs, forward decls, and non-functions first.
// TODO: performance investigation: would keeping a list of Decls that we should
// generate, rather than querying here, be faster?
for (self.decl_table.keys()) |decl| {
if (!decl.has_tv) continue;
const buf = buf: {
if (decl.val.castTag(.function)) |_| {
try typedefs.ensureUnusedCapacity(@intCast(u32, decl.fn_link.c.typedefs.count()));
var it = decl.fn_link.c.typedefs.iterator();
while (it.next()) |new| {
const gop = typedefs.getOrPutAssumeCapacity(new.key_ptr.*);
if (!gop.found_existing) {
try err_typedef_writer.writeAll(new.value_ptr.rendered);
}
const decl_keys = self.decl_table.keys();
const decl_values = self.decl_table.values();
for (decl_keys) |decl, i| {
if (!decl.has_tv) continue; // TODO do we really need this branch?
const decl_block = &decl_values[i];
if (decl_block.fwd_decl.items.len != 0) {
try typedefs.ensureUnusedCapacity(@intCast(u32, decl_block.typedefs.count()));
var it = decl_block.typedefs.iterator();
while (it.next()) |new| {
const gop = typedefs.getOrPutAssumeCapacity(new.key_ptr.*);
if (!gop.found_existing) {
try err_typedef_writer.writeAll(new.value_ptr.rendered);
}
fn_count += 1;
break :buf decl.fn_link.c.fwd_decl.items;
} else {
break :buf decl.link.c.code.items;
}
};
all_buffers.appendAssumeCapacity(.{
.iov_base = buf.ptr,
.iov_len = buf.len,
});
file_size += buf.len;
const buf = decl_block.fwd_decl.items;
all_buffers.appendAssumeCapacity(.{
.iov_base = buf.ptr,
.iov_len = buf.len,
});
file_size += buf.len;
}
if (decl.getFunction() != null) {
fn_count += 1;
} else if (decl_block.code.items.len != 0) {
const buf = decl_block.code.items;
all_buffers.appendAssumeCapacity(.{
.iov_base = buf.ptr,
.iov_len = buf.len,
});
file_size += buf.len;
}
}
err_typedef_item.* = .{
@ -259,15 +325,17 @@ pub fn flushModule(self: *C, comp: *Compilation) !void {
// Now the function bodies.
try all_buffers.ensureUnusedCapacity(fn_count);
for (self.decl_table.keys()) |decl| {
if (!decl.has_tv) continue;
if (decl.val.castTag(.function)) |_| {
const buf = decl.link.c.code.items;
all_buffers.appendAssumeCapacity(.{
.iov_base = buf.ptr,
.iov_len = buf.len,
});
file_size += buf.len;
for (decl_keys) |decl, i| {
if (decl.getFunction() != null) {
const decl_block = &decl_values[i];
const buf = decl_block.code.items;
if (buf.len != 0) {
all_buffers.appendAssumeCapacity(.{
.iov_base = buf.ptr,
.iov_len = buf.len,
});
file_size += buf.len;
}
}
}

View File

@ -1366,10 +1366,6 @@ pub const Type = extern union {
.f128,
.bool,
.anyerror,
.fn_noreturn_no_args,
.fn_void_no_args,
.fn_naked_noreturn_no_args,
.fn_ccc_void_no_args,
.single_const_pointer_to_comptime_int,
.const_slice_u8,
.array_u8_sentinel_0,
@ -1397,6 +1393,12 @@ pub const Type = extern union {
.function => !self.castTag(.function).?.data.is_generic,
.fn_noreturn_no_args,
.fn_void_no_args,
.fn_naked_noreturn_no_args,
.fn_ccc_void_no_args,
=> true,
.@"struct" => {
// TODO introduce lazy value mechanism
const struct_obj = self.castTag(.@"struct").?.data;