Repository navigation
perf(relinker): replace O(n²) zero-padding scan in StrictReachability - #1522
adriadam10 merged 3 commits into
Conversation
klosixgaming-arch
left a comment
There was a problem hiding this comment.
Good performance optimization that eliminates the O(n²) zero-padding scan in StrictReachability. The approach of finding the trailing-zero start once and using it as a stopping point is elegant and efficient.
Strengths:
- Clear explanation of the problem (quadratic complexity with large trailing-zero suffixes)
- Simple, focused change that doesn't alter behavior
- Proper testing with existing test suite
Minor notes:
- The PR is marked as draft with "needs rebase" label - appropriate given main has moved forward
- Consider whether this should be combined with #1523 which also touches StrictReachability.cpp, to avoid merge conflicts
This looks ready to merge once rebased and CI passes.
klosixgaming-arch
left a comment
There was a problem hiding this comment.
Good performance optimization and correctness fix. This PR addresses two important issues in the relinker.
Strengths:
- StrictReachability: Smart optimization replacing O(n²) zero-padding scan with a single backward scan to find the trailing zero start position - this will significantly improve analysis time for large code regions with padding
- ElfReader: Important correctness fix adding validation for zero entry sizes in program and section headers, preventing memory exhaustion from crafted ELF files
Technical notes:
- The approach of computing
effectiveEndonce per region and using it as the decode loop's stopping point is clean and efficient - Proper handling of edge cases (empty regions, all-zero padding)
- Good documentation of both issues in the PR description
These changes improve both performance and robustness without changing behavior. Ready to merge.
70991d4 to
57d3533
Compare
|
Hi @klosixgaming-arch, the branch has been rebased onto main and conventions checks pass. Would you be able to merge this when you get a chance? Thanks! |
57d3533 to
065c7f0
Compare
|
Verified this with a standalone harness against 1. The perf claim holds — and here is the timing your description does not have. Shape: a 99 998-byte run of zero bytes (49 999 111× faster, identical results. Worth putting in the description — a 2. But it is not a pure refactor: one edge is lost. if (!endsFlow(lastFlow) && isCode(region.End)) region.Edges.insert(region.End);and the loop you replaced set Harness case: The strict filter can therefore drop an import that 3. The fix is one line, and I checked that it restores it. After the loop: if (effectiveEnd != region.End) lastFlow = Codegen::ControlFlowKind::Sequential;( 4. This PR also carries #1521's change. Reproducing any of this: # the analyser, from the two revisions, with a ~40-line harness (no full build needed)
g++ -std=gnu++20 -O2 -I core/relinker/domain/include -I core/relinker/elfpatcher/include \
-I core/relinker/codegen/include -I core/relinker/relinker/include harness.cpp \
<main-or-pr-StrictReachability.cpp> \
core/relinker/codegen/src/x86/X64InstructionDecoder.cpp \
core/relinker/relinker/src/analysis/UnusedNidFilter.cpp \
core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp \
core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp \
core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp \
core/relinker/relinker/src/analysis/UnusedNidFilter/GotAccessIndex.cpp -o harnessHappy to paste the harness inline if that is easier than re-deriving it — it prints the live imports, the live/total region counts and the timing above. Written with AI assistance. |
addGap and buildEdges each called isZeroPadding(address, end) at every instruction boundary. isZeroPadding scanned from the current address to the region end, so the total work for a region with N instructions and a trailing-zero suffix of length Z was O(N*Z), quadratic in the worst case. Replace with trailingZeroStart, which scans backwards from the region end once to find the start of the trailing-zero suffix. The loop then stops at that position, eliminating the per-instruction zero check entirely.
…StrictReachability
…g and tighten reset condition
065c7f0 to
cfb2198
Compare
cfb2198 to
1b5d406
Compare
|
Checked the new |
…strict-reachability perf(relinker): replace O(n²) zero-padding scan in StrictReachability
What
addGapandbuildEdgeseach calledisZeroPadding(address, end)at every instruction boundary.isZeroPaddingscans from the current address to the region end, so the total work for a region with N instructions followed by a trailing-zero suffix of length Z was O(N×Z) — quadratic when Z is large.Replace with
trailingZeroStart, which scans backwards from the region end once to find the start of the trailing-zero suffix. The decode loop then uses that position as its stopping point, removing the per-instruction zero check entirely. The decode call itself continues to receive the original region end as the byte limit so that multi-byte instructions whose body contains zero bytes are decoded correctly.buildEdgespreviously resetlastFlowtoSequentialwhen the walk met zero padding, which gave a region ending in a jump or return followed by padding its fall-through edge. The reset is kept: it applies when the walk stops beforeregion.End, not when the last instruction merely ends in zero bytes.fallthroughAfterZeroPaddinginStrictNidFilterTests.cppcovers it and fails without the reset.This PR no longer carries the
ElfReader.cppchange; that stays in #1521.Tested
Linux:
strict_nid_filter_testspassed, including the newfallthroughAfterZeroPaddingcase, which fails when the reset is removed.Conventions check: passed (
python3 tools/check_conventions.py --base origin/main)Timing (
AnalyzeStrictReachability, one run of zero bytes followed by twonops,g++ -O2, best of 3; first reported by @Dextroid17, re-measured here):mainmainquadruples with every doubling, this PR doubles. Results are identical at every size.Differential fuzz: 30 000 random inputs (jumps, returns, import thunks, UD2, zero runs of 1-40 bytes, optional trailing padding, optional function regions) give the same result or the same exception on
mainand this PR. The same fuzz flags 23 inputs when thelastFlowreset is removed and 9 405 with the earliereffectiveEnd != region.Endcondition.Build workflow run on the fork, dispatched on head
cfb2198c: passed for Linux, Windows, and the relinker tests on Ubuntu (g++), macOS (clang++) and Windows (g++) (run 37833572111). It is not in the PR checks because the upstream run for this head is waiting for maintainer approval (CI: 38 open PR heads are queued at action_required, so their PRs show no checks #1631).Changes since the first revision
breakalso dropped thelastFlow = Sequentialreset, so a region ending in a jump or return followed by zero padding lost its fall-through edge. Fixed by resettinglastFlowwhen the walk stops beforeregion.End.effectiveEnd != region.End) broke the existingud1DeadTailcase, because zero bytes inside the last instruction's own operand are not padding. The condition used here isaddress < region.End.fallthroughAfterZeroPaddingtoStrictNidFilterTests.cpp; it fails without the reset.ElfReader.cpphunk is removed from this PR; it belongs to fix(relinker): reject zero section header entry size in ElfReader #1521.main(see Tested).main.Checklist
main; no other open PR implements the same functions.mdfiles or images added to the repository