Skip to content

Commit a5a1b42

Browse files
honkstar1claude
andcommitted
linker: parallelize the ICF worklist reverse-index build
The last serial cost of the worklist tail: two full scans over every active cand's reloc slice plus a per-target last_ref/cursor dedup, ~425 ms wall at editor scale. Replaced with: parallel count/fill of the filtered (target, referrer) edge list over reloc-weighted cand ranges (duplicates kept), one parallel stable radix sort by target, then a serial compaction pass that drops adjacent duplicate pairs and emits the CSR sequentially. Identical index bit-for-bit: - per-target referrer order: workers own contiguous cand ranges concatenated in ascending-ci order and the LSD radix sort is stable, so each target's referrers stay ascending -- the old iteration order; - dedup: duplicate (C,T) pairs can only come from one cand's reloc list (C is unique per cand), so they are adjacent after the stable sort; dropping equal neighbors removes exactly the pairs last_ref/cursor dropped (verified: kept=14510682 on the FN editor Engine.dll link in both versions). Measured on the FN editor Engine.dll link (/RAD_LOG:TIMERS): revidx 425.1 -> 140.5 ms; whole worklist tail 2507.9 ms (pre-series) -> 160.7 ms. Output byte-identical (control-band only vs base back-to-back); ICF_WORKLIST_SELFCHECK partition check passes at editor scale (0 mismatches). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent dc8fade commit a5a1b42

1 file changed

Lines changed: 135 additions & 50 deletions

File tree

src/linker/lnk.c

Lines changed: 135 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4172,69 +4172,154 @@ lnk_icf_small_sort(U64 *keys, U32 *vals, U64 n)
41724172
// provably-redundant re-key requests (the worklist dirties CLASSES, set semantics), so it visits the
41734173
// same dirty sets -> identical partition -> byte-identical output.
41744174
//
4175-
// Duplicate (C,T) edges (multiple relocs from C to the same T) are deduped: cands are iterated in
4176-
// ascending index and each cand's relocs are drained before the next, so per-target appends arrive in
4177-
// non-decreasing C -- a duplicate is exactly "tail of T's bucket == C" (last_ref[] in the count pass,
4178-
// bucket-tail check in the fill pass; identical predicates under identical iteration order).
4179-
// Filtering/dedup only removes provably-redundant re-key requests (the worklist dirties CLASSES, set
4180-
// semantics), so it visits the same dirty sets -> identical partition -> byte-identical output.
4181-
internal void
4182-
lnk_icf_build_reverse_index(Arena *arena, U64 cand_count, LNK_ICFCand *cands, U8 *rt_iscand, U64 *rt_target,
4183-
U32 *active, U64 active_count,
4184-
U32 **out_rev_off, U32 **out_rev_adj)
4175+
// PARALLEL BUILD: the old serial builder (two full scans over every active cand's relocs + a per-target
4176+
// last_ref/cursor dedup) was the dominant serial cost of the whole worklist tail (~0.4-0.9s wall at
4177+
// editor scale). Instead: parallel count/fill of the filtered (target, referrer) edge list over
4178+
// reloc-weighted cand ranges (dups kept), one parallel stable radix sort by target, then a serial
4179+
// compaction pass. Identical result:
4180+
// - per-target referrer order: edges are emitted in ascending referrer ci (workers own contiguous
4181+
// cand ranges, concatenated in order), and the radix sort is stable -> per-target ascending ci,
4182+
// exactly the old iteration order;
4183+
// - dedup: duplicate (C,T) pairs only arise within one cand's reloc list (C is unique per cand), so
4184+
// they are adjacent after the stable sort; dropping equal-neighbor pairs removes exactly the pairs
4185+
// last_ref/cursor dropped.
4186+
typedef struct LNK_ICFRevIdxTask
41854187
{
4186-
Temp scratch = scratch_begin(&arena, 1);
4187-
U8 *is_active = push_array(scratch.arena, U8, cand_count ? cand_count : 1);
4188-
for EachIndex(i, active_count) { is_active[active[i]] = 1; }
4189-
U32 *last_ref = push_array(scratch.arena, U32, cand_count ? cand_count : 1); // ci+1 of last referrer counted per target; 0 = none
4188+
LNK_ICFCand *cands;
4189+
U8 *rt_iscand;
4190+
U64 *rt_target;
4191+
U8 *is_active;
4192+
U32 *active;
4193+
Rng1U64 *act_ranges; // [W+1] even split over active_count (is_active scatter)
4194+
Rng1U64 *cand_ranges; // [W+1] reloc-weighted split over cand_count (count/fill)
4195+
U64 *counts; // [W] per-worker filtered edge counts
4196+
U64 *offsets; // [W] exclusive prefix of counts
4197+
U64 *ek; // [edge] key: target cand idx
4198+
U32 *ev; // [edge] val: referrer cand idx
4199+
} LNK_ICFRevIdxTask;
41904200

4191-
U32 *rev_off = push_array(arena, U32, cand_count + 1);
4192-
U64 raw_iscand = 0; // instrumentation: unfiltered edge count (old rev_adj size); debug-log only, so
4193-
// it is counted in a separate debug-gated pass -- the hot count pass skips
4194-
// inactive referrer rows at the CAND level (no per-reloc rt_iscand loads at all
4195-
// for the ~vast majority of cands that are already inactive at handoff).
4196-
if (lnk_get_log_status(LNK_Log_Debug)) {
4197-
for EachIndex(ci, cand_count) {
4198-
LNK_ICFCand *c = &cands[ci];
4199-
for EachIndex(j, c->reloc_count) { raw_iscand += rt_iscand[(U64)c->reloc_first + j]; }
4201+
internal
4202+
THREAD_POOL_TASK_FUNC(lnk_icf_revidx_active_task)
4203+
{
4204+
LNK_ICFRevIdxTask *t = raw_task;
4205+
Rng1U64 r = t->act_ranges[task_id];
4206+
for EachInRange(i, r) { t->is_active[t->active[i]] = 1; }
4207+
}
4208+
4209+
internal
4210+
THREAD_POOL_TASK_FUNC(lnk_icf_revidx_count_task)
4211+
{
4212+
LNK_ICFRevIdxTask *t = raw_task;
4213+
Rng1U64 r = t->cand_ranges[task_id];
4214+
U64 n = 0;
4215+
for EachInRange(ci, r) {
4216+
if (!t->is_active[ci]) { continue; }
4217+
LNK_ICFCand *c = &t->cands[ci];
4218+
for EachIndex(j, c->reloc_count) {
4219+
U64 idx = (U64)c->reloc_first + j;
4220+
if (!t->rt_iscand[idx]) { continue; }
4221+
if (!t->is_active[t->rt_target[idx]]) { continue; }
4222+
n += 1;
42004223
}
42014224
}
4202-
for EachIndex(ci, cand_count) {
4203-
if (!is_active[ci]) { continue; }
4204-
LNK_ICFCand *c = &cands[ci];
4225+
t->counts[task_id] = n;
4226+
}
4227+
4228+
internal
4229+
THREAD_POOL_TASK_FUNC(lnk_icf_revidx_fill_task)
4230+
{
4231+
LNK_ICFRevIdxTask *t = raw_task;
4232+
Rng1U64 r = t->cand_ranges[task_id];
4233+
U64 out = t->offsets[task_id];
4234+
for EachInRange(ci, r) {
4235+
if (!t->is_active[ci]) { continue; }
4236+
LNK_ICFCand *c = &t->cands[ci];
42054237
for EachIndex(j, c->reloc_count) {
42064238
U64 idx = (U64)c->reloc_first + j;
4207-
if (!rt_iscand[idx]) { continue; }
4208-
U32 tg = (U32)rt_target[idx];
4209-
if (!is_active[tg]) { continue; }
4210-
if (last_ref[tg] == (U32)ci + 1) { continue; } // duplicate (C,T)
4211-
last_ref[tg] = (U32)ci + 1;
4212-
rev_off[tg + 1] += 1;
4239+
if (!t->rt_iscand[idx]) { continue; }
4240+
U64 tg = t->rt_target[idx];
4241+
if (!t->is_active[tg]) { continue; }
4242+
t->ek[out] = tg; t->ev[out] = (U32)ci; out += 1;
42134243
}
42144244
}
4245+
}
4246+
4247+
internal void
4248+
lnk_icf_build_reverse_index(TP_Context *tp, Arena *arena, U64 cand_count, LNK_ICFCand *cands, U8 *rt_iscand, U64 *rt_target,
4249+
U32 *active, U64 active_count,
4250+
U32 **out_rev_off, U32 **out_rev_adj)
4251+
{
4252+
Temp scratch = scratch_begin(&arena, 1);
4253+
U64 W = tp->worker_count ? tp->worker_count : 1;
4254+
U64 total_relocs = cand_count ? ((U64)cands[cand_count - 1].reloc_first + cands[cand_count - 1].reloc_count) : 0;
4255+
4256+
LNK_ICFRevIdxTask task = {0};
4257+
task.cands = cands;
4258+
task.rt_iscand = rt_iscand;
4259+
task.rt_target = rt_target;
4260+
task.active = active;
4261+
task.is_active = push_array(scratch.arena, U8, cand_count ? cand_count : 1);
4262+
4263+
// active-bit scatter (disjoint ranges; all stores are the constant 1)
4264+
task.act_ranges = tp_divide_work(scratch.arena, active_count, W);
4265+
tp_for_parallel(tp, 0, W, lnk_icf_revidx_active_task, &task);
4266+
4267+
// reloc-weighted cand ranges: split at the cand whose reloc_first crosses w/W of total_relocs
4268+
// (reloc_first is monotone). Splitting at cand boundaries keeps each cand's (potentially
4269+
// duplicated) targets inside ONE worker's contiguous, in-order output range.
4270+
task.cand_ranges = push_array_no_zero(scratch.arena, Rng1U64, W + 1);
4271+
{
4272+
U64 prev = 0;
4273+
for (U64 w = 1; w <= W; w += 1) {
4274+
U64 goal = (total_relocs * w) / W;
4275+
U64 lo = prev, hi = cand_count;
4276+
while (lo < hi) { U64 mid = (lo + hi) >> 1; if ((U64)cands[mid].reloc_first < goal) { lo = mid + 1; } else { hi = mid; } }
4277+
task.cand_ranges[w - 1] = rng_1u64(prev, lo);
4278+
prev = lo;
4279+
}
4280+
task.cand_ranges[W - 1].max = cand_count; // last range absorbs the tail
4281+
task.cand_ranges[W] = rng_1u64(cand_count, cand_count);
4282+
}
4283+
4284+
task.counts = push_array(scratch.arena, U64, W);
4285+
tp_for_parallel(tp, 0, W, lnk_icf_revidx_count_task, &task);
4286+
task.offsets = offsets_from_counts_array_u64(scratch.arena, task.counts, W);
4287+
U64 edge_raw = task.offsets[W - 1] + task.counts[W - 1];
4288+
4289+
task.ek = push_array_no_zero(scratch.arena, U64, edge_raw ? edge_raw : 1);
4290+
task.ev = push_array_no_zero(scratch.arena, U32, edge_raw ? edge_raw : 1);
4291+
tp_for_parallel(tp, 0, W, lnk_icf_revidx_fill_task, &task);
4292+
4293+
// stable parallel radix sort by target (ties keep input order = ascending referrer ci)
4294+
lnk_radix_sort_u64_pairs(tp, scratch.arena, edge_raw, task.ek, task.ev);
4295+
4296+
// serial compaction: drop adjacent duplicate (target, referrer) pairs, count per target, emit CSR.
4297+
// rev_adj is written strictly sequentially (edges are sorted by target).
4298+
U32 *rev_off = push_array(arena, U32, cand_count + 1);
4299+
U64 edge_count = 0;
4300+
for (U64 e = 0; e < edge_raw; e += 1) {
4301+
if (e > 0 && task.ek[e] == task.ek[e - 1] && task.ev[e] == task.ev[e - 1]) { continue; } // duplicate (C,T)
4302+
rev_off[task.ek[e] + 1] += 1;
4303+
edge_count += 1;
4304+
}
42154305
for EachIndex(t, cand_count) { rev_off[t + 1] += rev_off[t]; }
4216-
U64 edge_count = rev_off[cand_count];
42174306
U32 *rev_adj = push_array_no_zero(arena, U32, edge_count ? edge_count : 1);
4218-
U32 *cursor = push_array(scratch.arena, U32, cand_count ? cand_count : 1);
4219-
for EachIndex(ci, cand_count) {
4220-
if (!is_active[ci]) { continue; }
4221-
LNK_ICFCand *c = &cands[ci];
4222-
for EachIndex(j, c->reloc_count) {
4223-
U64 idx = (U64)c->reloc_first + j;
4224-
if (!rt_iscand[idx]) { continue; }
4225-
U32 tg = (U32)rt_target[idx];
4226-
if (!is_active[tg]) { continue; }
4227-
U32 n = cursor[tg];
4228-
if (n > 0 && rev_adj[rev_off[tg] + n - 1] == (U32)ci) { continue; } // duplicate (C,T)
4229-
rev_adj[rev_off[tg] + n] = (U32)ci;
4230-
cursor[tg] = n + 1;
4307+
{
4308+
U64 out = 0;
4309+
for (U64 e = 0; e < edge_raw; e += 1) {
4310+
if (e > 0 && task.ek[e] == task.ek[e - 1] && task.ev[e] == task.ev[e - 1]) { continue; }
4311+
rev_adj[out++] = task.ev[e];
42314312
}
42324313
}
42334314
scratch_end(scratch);
42344315
if (lnk_get_log_status(LNK_Log_Debug)) {
4235-
U64 total_relocs = cand_count ? ((U64)cands[cand_count - 1].reloc_first + cands[cand_count - 1].reloc_count) : 0;
4236-
lnk_log(LNK_Log_Debug, "/OPT:ICF worklist rev-index: relocs=%llu iscand=%llu kept=%llu (active-rows+dedup); rev_adj %llu -> %llu bytes",
4237-
total_relocs, raw_iscand, edge_count, raw_iscand * sizeof(U32), edge_count * sizeof(U32));
4316+
U64 raw_iscand = 0; // instrumentation: unfiltered edge count (old rev_adj size); debug-log only
4317+
for EachIndex(ci, cand_count) {
4318+
LNK_ICFCand *c = &cands[ci];
4319+
for EachIndex(j, c->reloc_count) { raw_iscand += rt_iscand[(U64)c->reloc_first + j]; }
4320+
}
4321+
lnk_log(LNK_Log_Debug, "/OPT:ICF worklist rev-index: relocs=%llu iscand=%llu filtered=%llu kept=%llu (active-rows+dedup); rev_adj %llu -> %llu bytes",
4322+
total_relocs, raw_iscand, edge_raw, edge_count, raw_iscand * sizeof(U32), edge_count * sizeof(U32));
42384323
}
42394324
*out_rev_off = rev_off;
42404325
*out_rev_adj = rev_adj;
@@ -4703,7 +4788,7 @@ lnk_opt_icf(TP_Context *tp, Arena *perm, LNK_SymbolTable *symtab, LNK_Config *co
47034788
U32 *active_prev = rs.active2;
47044789
U64 n_prev = rs.round_n;
47054790
U32 *rev_off = 0, *rev_adj = 0;
4706-
lnk_icf_build_reverse_index(arena, cand_count, cands, rt_iscand, rt_target, active_prev, n_prev, &rev_off, &rev_adj);
4791+
lnk_icf_build_reverse_index(tp, arena, cand_count, cands, rt_iscand, rt_target, active_prev, n_prev, &rev_off, &rev_adj);
47074792
U64 revidx_end_us = now_time_us();
47084793

47094794
// SEED: members of the classes that SPLIT in the region's final round. active_prev is grouped by

0 commit comments

Comments
 (0)