commit 6402e119e8d21403cbafc4a6c8ca353b894592ba (tree)
parent 7ca061f3d68ff936dd77a58402c817b0aa1044e6
Author: Matthew Lugg <mlugg@mlugg.co.uk>
Date: Fri, 13 Feb 2026 09:31:36 +0000
Zcu: prioritize analyzing function bodies
...so that they can be sent to the codegen backend and linker ASAP.
Diffstat:
| M | src/Zcu.zig | | | 88 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------- |
1 file changed, 63 insertions(+), 25 deletions(-)
diff --git a/src/Zcu.zig b/src/Zcu.zig
@@ -275,10 +275,16 @@ potentially_outdated: std.AutoArrayHashMapUnmanaged(AnalUnit, u32) = .empty,
/// Value is the number of PO dependencies of this AnalUnit.
/// Once this value drops to 0, the AnalUnit is a candidate for re-analysis.
outdated: std.AutoArrayHashMapUnmanaged(AnalUnit, u32) = .empty,
-/// This contains all `AnalUnit`s in `outdated` whose PO dependency count is 0.
+/// This is the set of all `AnalUnit`s in `outdated` whose PO dependency count is 0.
/// Such `AnalUnit`s are ready for immediate re-analysis.
/// See `findOutdatedToAnalyze` for details.
-outdated_ready: std.AutoArrayHashMapUnmanaged(AnalUnit, void) = .empty,
+outdated_ready: struct {
+ /// These are separate from other units because it allows `findOutdatedToAnalyze` to prioritize
+ /// functions, which is useful because it means they will be sent to codegen more quickly.
+ funcs: std.AutoArrayHashMapUnmanaged(InternPool.Index, void),
+ /// Does not contain `.func` units.
+ other: std.AutoArrayHashMapUnmanaged(AnalUnit, void),
+} = .{ .funcs = .empty, .other = .empty },
/// This contains a list of AnalUnit whose analysis or codegen failed, but the
/// failure was something like running out of disk space, and trying again may
/// succeed. On the next update, we will flush this list, marking all members of
@@ -2835,7 +2841,8 @@ pub fn deinit(zcu: *Zcu) void {
zcu.potentially_outdated.deinit(gpa);
zcu.outdated.deinit(gpa);
- zcu.outdated_ready.deinit(gpa);
+ zcu.outdated_ready.funcs.deinit(gpa);
+ zcu.outdated_ready.other.deinit(gpa);
zcu.retryable_failures.deinit(gpa);
zcu.test_functions.deinit(gpa);
@@ -3065,6 +3072,7 @@ pub fn markDependeeOutdated(
marked_po: enum { not_marked_po, marked_po },
dependee: InternPool.Dependee,
) !void {
+ const gpa = zcu.comp.gpa;
deps_log.debug("outdated dependee: {f}", .{zcu.fmtDependee(dependee)});
var it = zcu.intern_pool.dependencyIterator(dependee);
if (std.debug.runtime_safety) zcu.outdated_lock.lockUncancelable(zcu.comp.io);
@@ -3078,7 +3086,10 @@ pub fn markDependeeOutdated(
deps_log.debug("outdated {f} => already outdated {f} po_deps={}", .{ zcu.fmtDependee(dependee), zcu.fmtAnalUnit(depender), po_dep_count.* });
if (po_dep_count.* == 0) {
deps_log.debug("outdated ready: {f}", .{zcu.fmtAnalUnit(depender)});
- try zcu.outdated_ready.put(zcu.gpa, depender, {});
+ switch (depender.unwrap()) {
+ .func => |func| try zcu.outdated_ready.funcs.put(gpa, func, {}),
+ else => try zcu.outdated_ready.other.put(gpa, depender, {}),
+ }
}
},
}
@@ -3094,14 +3105,17 @@ pub fn markDependeeOutdated(
},
};
try zcu.outdated.putNoClobber(
- zcu.gpa,
+ gpa,
depender,
new_po_dep_count,
);
deps_log.debug("outdated {f} => new outdated {f} po_deps={}", .{ zcu.fmtDependee(dependee), zcu.fmtAnalUnit(depender), new_po_dep_count });
if (new_po_dep_count == 0) {
deps_log.debug("outdated ready: {f}", .{zcu.fmtAnalUnit(depender)});
- try zcu.outdated_ready.put(zcu.gpa, depender, {});
+ switch (depender.unwrap()) {
+ .func => |func| try zcu.outdated_ready.funcs.put(gpa, func, {}),
+ else => try zcu.outdated_ready.other.put(gpa, depender, {}),
+ }
}
// If this is a Decl and was not previously PO, we must recursively
// mark dependencies on its tyval as PO.
@@ -3119,6 +3133,7 @@ pub fn markPoDependeeUpToDate(zcu: *Zcu, dependee: InternPool.Dependee) !void {
}
/// Assumes that `zcu.outdated_lock` is already held exclusively.
fn markPoDependeeUpToDateInner(zcu: *Zcu, dependee: InternPool.Dependee) !void {
+ const gpa = zcu.comp.gpa;
deps_log.debug("up-to-date dependee: {f}", .{zcu.fmtDependee(dependee)});
var it = zcu.intern_pool.dependencyIterator(dependee);
while (it.next()) |depender| {
@@ -3129,7 +3144,10 @@ fn markPoDependeeUpToDateInner(zcu: *Zcu, dependee: InternPool.Dependee) !void {
deps_log.debug("up-to-date {f} => {f} po_deps={}", .{ zcu.fmtDependee(dependee), zcu.fmtAnalUnit(depender), po_dep_count.* });
if (po_dep_count.* == 0) {
deps_log.debug("outdated ready: {f}", .{zcu.fmtAnalUnit(depender)});
- try zcu.outdated_ready.put(zcu.gpa, depender, {});
+ switch (depender.unwrap()) {
+ .func => |func| try zcu.outdated_ready.funcs.put(gpa, func, {}),
+ else => try zcu.outdated_ready.other.put(gpa, depender, {}),
+ }
}
continue;
}
@@ -3167,7 +3185,8 @@ fn markPoDependeeUpToDateInner(zcu: *Zcu, dependee: InternPool.Dependee) !void {
/// in turn be PO, due to a dependency on the original AnalUnit's tyval or IES.
///
/// Assumes that `zcu.outdated_lock` is already held exclusively.
-fn markTransitiveDependersPotentiallyOutdated(zcu: *Zcu, maybe_outdated: AnalUnit) !void {
+fn markTransitiveDependersPotentiallyOutdated(zcu: *Zcu, maybe_outdated: AnalUnit) Allocator.Error!void {
+ const gpa = zcu.comp.gpa;
const ip = &zcu.intern_pool;
const dependee: InternPool.Dependee = switch (maybe_outdated.unwrap()) {
.@"comptime" => return, // analysis of a comptime decl can't outdate any dependencies
@@ -3181,10 +3200,12 @@ fn markTransitiveDependersPotentiallyOutdated(zcu: *Zcu, maybe_outdated: AnalUni
var it = ip.dependencyIterator(dependee);
while (it.next()) |po| {
if (zcu.outdated.getPtr(po)) |po_dep_count| {
- // This dependency is already outdated, but it now has one more PO
- // dependency.
+ // This dependency is already outdated, but it now has one more PO dependency.
if (po_dep_count.* == 0) {
- _ = zcu.outdated_ready.swapRemove(po);
+ switch (po.unwrap()) {
+ .func => |func| _ = zcu.outdated_ready.funcs.swapRemove(func),
+ else => _ = zcu.outdated_ready.other.swapRemove(po),
+ }
}
po_dep_count.* += 1;
deps_log.debug("po {f} => {f} [outdated] po_deps={}", .{ zcu.fmtDependee(dependee), zcu.fmtAnalUnit(po), po_dep_count.* });
@@ -3196,7 +3217,7 @@ fn markTransitiveDependersPotentiallyOutdated(zcu: *Zcu, maybe_outdated: AnalUni
deps_log.debug("po {f} => {f} po_deps={}", .{ zcu.fmtDependee(dependee), zcu.fmtAnalUnit(po), n.* });
continue;
}
- try zcu.potentially_outdated.putNoClobber(zcu.gpa, po, 1);
+ try zcu.potentially_outdated.putNoClobber(gpa, po, 1);
deps_log.debug("po {f} => {f} po_deps=1", .{ zcu.fmtDependee(dependee), zcu.fmtAnalUnit(po) });
// This AnalUnit was not already PO, so we must recursively mark its dependers as also PO.
try zcu.markTransitiveDependersPotentiallyOutdated(po);
@@ -3208,10 +3229,20 @@ fn markTransitiveDependersPotentiallyOutdated(zcu: *Zcu, maybe_outdated: AnalUni
/// recursive analysis (all of its previously-marked dependencies are already up-to-date), because
/// recursive analysis can cause over-analysis on incremental updates.
pub fn findOutdatedToAnalyze(zcu: *Zcu) Allocator.Error!?AnalUnit {
- // MLUGG TODO: priorize `func` units, just like we used to do in the Compilation job queue.
+ // We prioritize functions, because the sooner they get analyzed, the sooner they can be send to
+ // the codegen backend and linker, which are usually running in parallel (so this can increase
+ // parallelism).
+ // TODO: perhaps we should also experiment with *avoiding* functions if the codegen/link queue
+ // is backed up (for instance due to a very large function). That could help minimize blocking
+ // on the main thread in `CodegenTaskPool.start` waiting for the linker to catch up.
+ if (zcu.outdated_ready.funcs.count() > 0) {
+ const unit: AnalUnit = .wrap(.{ .func = zcu.outdated_ready.funcs.keys()[0] });
+ log.debug("findOutdatedToAnalyze: {f}", .{zcu.fmtAnalUnit(unit)});
+ return unit;
+ }
- if (zcu.outdated_ready.count() > 0) {
- const unit = zcu.outdated_ready.keys()[0];
+ if (zcu.outdated_ready.other.count() > 0) {
+ const unit = zcu.outdated_ready.other.keys()[0];
log.debug("findOutdatedToAnalyze: {f}", .{zcu.fmtAnalUnit(unit)});
return unit;
}
@@ -3502,13 +3533,12 @@ pub fn ensureFuncBodyAnalysisQueued(zcu: *Zcu, func: InternPool.Index) !void {
assert(func == ip.unwrapCoercedFunc(func)); // analyze the body of the original function, not a coerced one
if (ip.setWantRuntimeFnAnalysis(io, func)) {
// This is the first reference to this function, so we must ensure it will be analyzed.
- const unit: AnalUnit = .wrap(.{ .func = func });
if (std.debug.runtime_safety) zcu.outdated_lock.lockUncancelable(zcu.comp.io);
defer if (std.debug.runtime_safety) zcu.outdated_lock.unlock(zcu.comp.io);
try zcu.outdated.ensureUnusedCapacity(gpa, 1);
- try zcu.outdated_ready.ensureUnusedCapacity(gpa, 1);
- zcu.outdated.putAssumeCapacityNoClobber(unit, 0);
- zcu.outdated_ready.putAssumeCapacityNoClobber(unit, {});
+ try zcu.outdated_ready.funcs.ensureUnusedCapacity(gpa, 1);
+ zcu.outdated.putAssumeCapacityNoClobber(.wrap(.{ .func = func }), 0);
+ zcu.outdated_ready.funcs.putAssumeCapacityNoClobber(func, {});
}
}
@@ -3522,11 +3552,11 @@ pub fn ensureNavValAnalysisQueued(zcu: *Zcu, nav: InternPool.Nav.Index) !void {
if (std.debug.runtime_safety) zcu.outdated_lock.lockUncancelable(zcu.comp.io);
defer if (std.debug.runtime_safety) zcu.outdated_lock.unlock(zcu.comp.io);
try zcu.outdated.ensureUnusedCapacity(gpa, 2);
- try zcu.outdated_ready.ensureUnusedCapacity(gpa, 2);
+ try zcu.outdated_ready.other.ensureUnusedCapacity(gpa, 2);
zcu.outdated.putAssumeCapacityNoClobber(.wrap(.{ .nav_val = nav }), 0);
zcu.outdated.putAssumeCapacityNoClobber(.wrap(.{ .nav_ty = nav }), 0);
- zcu.outdated_ready.putAssumeCapacityNoClobber(.wrap(.{ .nav_val = nav }), {});
- zcu.outdated_ready.putAssumeCapacityNoClobber(.wrap(.{ .nav_ty = nav }), {});
+ zcu.outdated_ready.other.putAssumeCapacityNoClobber(.wrap(.{ .nav_val = nav }), {});
+ zcu.outdated_ready.other.putAssumeCapacityNoClobber(.wrap(.{ .nav_ty = nav }), {});
}
}
@@ -3540,9 +3570,9 @@ pub fn queueComptimeUnitAnalysis(zcu: *Zcu, cu: InternPool.ComptimeUnit.Id) Allo
if (std.debug.runtime_safety) zcu.outdated_lock.lockUncancelable(io);
defer if (std.debug.runtime_safety) zcu.outdated_lock.unlock(io);
try zcu.outdated.ensureUnusedCapacity(gpa, 1);
- try zcu.outdated_ready.ensureUnusedCapacity(gpa, 1);
+ try zcu.outdated_ready.other.ensureUnusedCapacity(gpa, 1);
zcu.outdated.putAssumeCapacityNoClobber(unit, 0);
- zcu.outdated_ready.putAssumeCapacityNoClobber(unit, {});
+ zcu.outdated_ready.other.putAssumeCapacityNoClobber(unit, {});
}
/// If `unit` was marked as outdated or porentially outdated, clears that status and returns `true`.
@@ -3552,7 +3582,15 @@ pub fn clearOutdatedState(zcu: *Zcu, unit: AnalUnit) bool {
if (std.debug.runtime_safety) zcu.outdated_lock.lockUncancelable(io);
defer if (std.debug.runtime_safety) zcu.outdated_lock.unlock(io);
if (zcu.outdated.fetchSwapRemove(unit)) |kv| {
- if (kv.value == 0) assert(zcu.outdated_ready.swapRemove(unit));
+ const was_ready = switch (unit.unwrap()) {
+ .func => |func| zcu.outdated_ready.funcs.swapRemove(func),
+ else => zcu.outdated_ready.other.swapRemove(unit),
+ };
+ if (kv.value == 0) {
+ assert(was_ready);
+ } else {
+ assert(!was_ready);
+ }
return true;
} else if (zcu.potentially_outdated.swapRemove(unit)) {
return true;