Skip to content

Reduce key path COW copies in Graph forEach and compactMapValues#1725

Merged
harlanhaskins merged 4 commits into
swiftlang:mainfrom
inju2403:avoid-graph-keypath-cow-copies
May 26, 2026
Merged

Reduce key path COW copies in Graph forEach and compactMapValues#1725
harlanhaskins merged 4 commits into
swiftlang:mainfrom
inju2403:avoid-graph-keypath-cow-copies

Conversation

@inju2403

@inju2403 inju2403 commented May 22, 2026

Copy link
Copy Markdown
Contributor

This avoids an accidentally-superlinear COW copy of the key path during Graph.forEach and Graph.compactMapValues traversal.

Motivation:

While reading @harlanhaskins' #1626 (which fixes a quadratic COW issue on Graph.insertValue), I noticed that _forEach and
_compactMapValues carry a related — though less dramatic — version of the same pattern. Each recursive call builds the descendant's key path with var childKeyPath = keyPath; childKeyPath.append(key), and because
childKeyPath is COW-shared with keyPath, the append always triggers a copy of the whole key-path array. For a graph with N nodes at average depth d, this turns traversal into O(N · d) instead of the O(N) it should
be.

In isolated benchmarks of the key-path copy pattern at depths 3–6 with branching factor 3–5, the new inout-based version is consistently 3–5× faster, with the speedup growing with depth — exactly as expected from the O(N ·
d) → O(N)
change:

Config Nodes Old New Speedup
depth 3, branch 3 40 0.006 ms 0.002 ms 3.51×
depth 4, branch 4 341 0.059 ms 0.012 ms 4.84×
depth 4, branch 5 781 0.138 ms 0.029 ms 4.84×
depth 5, branch 4 1,365 0.242 ms 0.046 ms 5.21×
depth 6, branch 4 5,461 0.830 ms 0.159 ms 5.23×

Modifications:

The four _forEach / _compactMapValues overloads (sync + async for each) now thread a single inout [K] through the recursion. After invoking the body, the child's key is appended once, recursion descends, and the key is
popped on the way back up — mutating the key path in place rather than copying it on every descent. The public forEach(_:) and compactMapValues(_:) wrappers create the initial var keyPath: [K] = [] and pass it through; the
public API surface is unchanged.

All six current forEach callsites (two in Runner.Plan, four in AdvancedConsoleOutputRecorder) consume the key path inline — none of them escape it — so the existing semantics are preserved. The existing GraphTests for
forEach, forEach (async), and compactMapValues pass unchanged.

Checklist:

  • Code and documentation should follow the style of the Style Guide.
  • If public symbols are renamed or modified, DocC references should be updated.

Motivation:

`_forEach` and `_compactMapValues` previously built each descendant's key path with `var childKeyPath = keyPath; childKeyPath.append(key)`. Because `childKeyPath` is COW-shared with `keyPath`, the subsequent `append` always triggers a copy of the whole key-path array. For a graph with `N` nodes at average depth `d`, this turns traversal into O(N * d) instead of the O(N) it should be.

The fix threads a single `inout [K]` through the recursion and uses append/removeLast around the recursive call, so the key path is mutated in place. The public `forEach` and `compactMapValues` APIs are unchanged.

Across isolated benchmarks at depths 3-6 with branching factor 3-5, traversal is 3-5x faster, with the speedup growing with depth - exactly as expected from the O(N * d) -> O(N) change.

Adds a perf test gated by `performanceTestsEnabled` that verifies forEach traversal scales linearly with node count, mirroring the pattern in swiftlang#1626 (the symmetric fix on the insertion side).
Comment thread Tests/TestingTests/Support/GraphTests.swift Outdated
@harlanhaskins

Copy link
Copy Markdown
Contributor

The algorithmic improvements look sound here, and assuming it passes the existing test suite I think it’s a good change. Thanks for looking into this!

The SD-based scaling check (mirrored from swiftlang#1626) doesn't yet behave reliably across CI invocations, per @harlanhaskins' review.
Comment thread Sources/Testing/Support/Graph.swift Outdated
@grynspan

Copy link
Copy Markdown
Contributor

Makes sense to me. Perf test aside, do you have any metrics showing an improvement?

Update the docstrings for the four `_forEach` / `_compactMapValues` overloads to describe the reused inout key path and the COW copy it avoids, per @harlanhaskins' review.
@inju2403

Copy link
Copy Markdown
Contributor Author

Thanks, @grynspan! There's a benchmark table in the PR description with isolated measurements (3-5× speedup, scaling with depth) — let me copy it here for visibility:

Config Nodes Old New Speedup
depth 3, branch 3 40 0.006 ms 0.002 ms 3.51×
depth 4, branch 4 341 0.059 ms 0.012 ms 4.84×
depth 4, branch 5 781 0.138 ms 0.029 ms 4.84×
depth 5, branch 4 1,365 0.242 ms 0.046 ms 5.21×
depth 6, branch 4 5,461 0.830 ms 0.159 ms 5.23×

These are from an isolated micro-benchmark of just the keyPath copy pattern (release build, best of 20 runs, warmup ×5). The speedup grows roughly with depth — consistent with the O(N·d) → O(N) reduction. Happy to put together
end-to-end numbers against a real test plan if that's more useful.

@grynspan

Copy link
Copy Markdown
Contributor

Thanks, sorry! Looks like it wasn't rendering in the GitHub app.

Comment thread Sources/Testing/Support/Graph.swift Outdated
Drop the inout-reuse rationale from the four `_forEach` / `_compactMapValues` overloads and keep just a one-line description of what the keyPath is.
@harlanhaskins

harlanhaskins commented May 26, 2026

Copy link
Copy Markdown
Contributor

@inju2403 Thanks! Feel free to merge. It should be set as the default, but if not, make sure it's set to "Squash and merge"

@grynspan

Copy link
Copy Markdown
Contributor

I think you need to do that @harlanhaskins.

@harlanhaskins harlanhaskins merged commit bd820f8 into swiftlang:main May 26, 2026
35 checks passed
@grynspan grynspan added bug 🪲 Something isn't working performance 🏎️ Performance issues labels May 26, 2026
@grynspan grynspan added this to the Swift 6.4.0 (main) milestone May 26, 2026
@harlanhaskins

Copy link
Copy Markdown
Contributor

Ah, sorry. I figured with approval they'd be able to. But at any rate, thanks @inju2403!

@grynspan

Copy link
Copy Markdown
Contributor

@inju2403 Was this PR AI-assisted like #1773? (Not a criticism or a blocker, just want to label it correctly if so.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug 🪲 Something isn't working performance 🏎️ Performance issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants