Remove redundant copies from the asset parse path - #252
Merged
briaguya0 merged 1 commit intoAug 13, 2026
Conversation
GetFileOffsetFromSegmentedAddr and GetFileOffsetFromCompressedSegmentedAddr each deep-copied the entire SegmentConfig — four unordered_maps, one of them keyed by every file — solely so operator[] could be used from a const method. Look the maps up in place instead. AddAsset bound GetRomData()'s std::vector<uint8_t>& to an `auto`, which deduces a value and so deep-copied the whole ROM once per asset. The variable was never read. -Wunused-variable stays quiet because std::vector has a non-trivial ctor/dtor. Extracting Banjo-Kazooie US v1.0: 28.3s -> 3.0s. Output archives verified identical by per-entry CRC and uncompressed size. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
|
Confirmed reduction of ~97% of extraction time on Windows release (from ~1:35 to ~3.5s) and for debug, a ~91% decrease (from ~4:30 to ~24s). |
Malkierian
approved these changes
Aug 12, 2026
KiritoDv
approved these changes
Aug 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two redundant copies in the asset parse path. Extracting Banjo-Kazooie US v1.0 goes from 28.3s to 3.0s.
SegmentConfigdeep copy —GetFileOffsetFromSegmentedAddrandGetFileOffsetFromCompressedSegmentedAddrboth didauto segments = this->gConfig.segment;, copying fourunordered_maps (one keyed by every file) on every address resolution. The copy only existed sooperator[]could be used from aconstmethod;find()on the member does the same job. 28.3s → 18.9s.Dead ROM copy —
AddAssetdidauto rom = this->GetRomData();.GetRomData()returnsstd::vector<uint8_t>&, butautodeduces a value, so this deep-copied the whole 16MB ROM once per asset.romwas never read.-Wunused-variablestays quiet becausestd::vectorhas a non-trivial ctor/dtor. 18.9s → 3.0s.No behaviour change: o2r archives compared by per-entry CRC and uncompressed size (byte comparison is useless — zip timestamps), 16,937 entries identical before and after.
Full benchmark + profiling writeup
Asset extraction benchmark (2026-08-12)
Environment: Linux x86-64, 32 cores, GCC, glibc malloc.
ROM: Banjo-Kazooie US v1.0 (
1fe1632098865f639e22c11b9a81ee8f29c75d7a).What's being measured
The target is the in-app extraction users hit on first launch:
GameExtractor::GenerateOTR(src/port/Extractor/GameExtractor.cpp:240), run on athread pool from
Engine.cpp:781. Its actual work isCompanion::Init(ExportType::Binary, ...)(
GameExtractor.cpp:366).We measure it via the standalone
torch o2r <rom>binary, which calls the sameCompanion::Init. On Linux the in-app path is only reachable through GUI popups(
ES_EXTRACT→PS_FILE_CHECK→PS_FIRST), so it isn't scriptable without codechanges. The proxy was matched to the app on both axes that could skew it:
add_subdirectory(Torch)(
CMakeLists.txt:509).Torch/CMakeLists.txt:243setsCMAKE_CXX_FLAGS_RELEASE "-O3"in its own directory scope, so a Release app build compiles torch at
-O3—identical to
build/torch-bench.BUILD_BK64=ONand every other game plusBUILD_NAUDIO=OFF(CMakeLists.txt:500-508);tools/bench-extract.shpasses thesame. Measured: no effect on runtime (28.28 s vs 28.37 s with everything on).
Remaining in-app-only cost, verified negligible:
GenerateOTRwalks the asset YAMLsto size the progress bar (
GameExtractor.cpp:246-313) — that's 3 files / 296 KBunder
assets/yaml/us/rev0.Timings
Release
-O3(== app Release), hyperfine, 5 runs each:SegmentConfigcopy)Peak RSS ~738 MB. 99% CPU on a 32-core box — extraction is single-threaded.
The one thread pool in
Torch/src/Companion.cpp:1735covers only the modding-exportpath, not the
Binary/o2r export the game uses. (BKAssetFactory.cpp:367does run athreaded pre-decompression pass, but it's a small slice of wall time.)
Fix 1 —
SegmentConfigdeep copy (done)Torch/src/Companion.cpp,GetFileOffsetFromCompressedSegmentedAddr(hot, 35%cumulative) and
GetFileOffsetFromSegmentedAddr(same pattern, cold) both did:SegmentConfig(Companion.h:57) holds fourunordered_maps, one a map-of-mapskeyed by every file. The copy existed only so
operator[]could be used from aconstmethod. Replaced withfind()on the member directly.28.28 s → 18.90 s. Bigger than the ~20% the profile predicted, because the copy
was also driving nearly all allocator traffic:
mallocfell 20.7% → 1.5% andfreedropped off the profile entirely.Verified behaviour-preserving: o2r archives can't be compared byte-wise (zip
timestamps), so compare per-entry CRCs and sizes from the zip directory — 16,937
entries, all identical.
Fix 2 — dead per-asset copy of the entire ROM (done)
Companion::AddAsset,Torch/src/Companion.cpp:2540:GetRomData()returnsstd::vector<uint8_t>&, butautodeduces a value, sothis deep-copied the whole 16 MB ROM.
romis never read anywhere inAddAsset(lines 2514-2578) — it was pure dead weight. GCC doesn't warn because
std::vectorhas a non-trivial ctor/dtor, so
-Wunused-variablestays quiet.18.90 s → 3.00 s. Deleting one line.
How it was found
perf couldn't attribute it (inline frames at
-O2were nonsense), but the shape ofthe profile gave it away: 14.3 s of
memmoveagainst only 1.5%malloc. Many smallvector copies would show proportional malloc traffic — so it had to be a few very
large copies. At ~10 GB/s, 14.3 s ≈ 140 GB moved; against ~16.9k assets that's about
one 16 MB ROM per asset. From there
grep GetRomData()found theauto-by-value bugdirectly, no heaptrack needed.
Ruled out along the way:
BaseFactory::parsetakes the buffer by reference(
BaseFactory.h:104), the call site passes the member (Companion.cpp:493), andDecompressor::AutoDecodereturns cached raw pointers (Decompressor.h:26).Profile after both fixes (
build/bench/report-*.txt)tdefl_compress(miniz)shared_ptrrefcount releasenode_data::getlinear scan__libc_malloc2mz_crc32__memmove_avx512_unaligned_ermsRoughly half the run is now genuine zip compression (
tdefl_*+mz_crc32), andmemmove has collapsed from 76% to 1%. This is a healthy profile.
Remaining leads, in rough order of size:
or drop the deflate level — worth checking what miniz level the o2r writer uses.
shared_ptrrefcount churn, ~17% (release + addref). Atomic refcounting onIParsedDatapassed around byshared_ptr; reducible by passing raw/const&through the hot path.
looked at 28 s.
Side note: the
ExtractAssetscmake targetNot the path users take, but
CMakeLists.txt:743ExternalProject_Add(TorchExternal ...)passes no
CMAKE_BUILD_TYPE, somake ExtractAssetsbuilds an unoptimized torch:measured 43.3 s vs 28.3 s. Adding
-DCMAKE_BUILD_TYPE=Releaseto thoseCMAKE_ARGSwould cut it ~34%.Reproducing
Harness (benchmark/profile script + the o2r content comparison used to verify these
changes): https://gist.github.com/briaguya0/135467a5243c4b28b94c16b2c6b4fdeb
Needs
hyperfineandperf. Run from a Lighthouse checkout root:Reports are written to
build/bench/.REUSE=1re-renders them from existingperf.datawithout re-recording.To check a change is behaviour-preserving, compare o2r archives by per-entry CRC and
size (
compare-o2r-contents.py), not by file hash — zip timestamps make bytecomparison useless.
🤖 Generated with Claude Code