Skip to content

Memoize the negative result of the lazy cache getter - #1

Open
CamdenBopp wants to merge 1 commit into
MxIris-Reverse-Engineering:mainfrom
CamdenBopp:pr/memoize-cache-nil
Open

Memoize the negative result of the lazy cache getter#1
CamdenBopp wants to merge 1 commit into
MxIris-Reverse-Engineering:mainfrom
CamdenBopp:pr/memoize-cache-nil

Conversation

@CamdenBopp

Copy link
Copy Markdown

Problem

MachOFile.cache lazily loads the dyld shared cache the file belongs to with try? DyldCache.init(url:) and stores it in _cache. For a standalone (non-cache) Mach-O the init fails and _cache stays nil, which is indistinguishable from "not yet attempted" — so every later access to cache re-runs the init: another open(), mmap, and URL-path resolution of the file, only to fail again.

A consumer that reads cache once per symbol pays this once per symbol. Indexing the Swift symbols of a ~30 MB standalone binary (~45k local Swift symbols) spent ~8.5s of a 12.8s run re-opening the same file as a dyld cache tens of thousands of times, every attempt discarded. A sampling profile showed DyldCache.init(url:) as ~99% of samples.

Fix

Memoize the negative result with a one-shot flag so the failed load runs at most once. The _cache / _fullCache success paths are untouched; a genuine cache image still resolves and caches on first touch. Only repeated failed loads are elided, so behavior is unchanged.

Verification

  • Byte-identical output from a downstream Swift-metadata dumper across five fixture dylibs, before vs after.
  • On a 30 MB standalone binary: dump 12.8s → 4.3s, interface reconstruction 19s → 5.1s; DyldCache.init drops from ~99% of profile samples to absent.

`MachOFile.cache` lazily loads the dyld shared cache the file belongs to via
`try? DyldCache.init(url:)`, storing the result in `_cache`. For a standalone
(non-cache) Mach-O the init fails and `_cache` stays nil, which is
indistinguishable from "not yet attempted", so every subsequent `cache` access
re-runs the init: another `open()`, mmap and URL-path resolution of the file,
only to fail again.

A consumer that touches `cache` once per symbol pays this per symbol. Building a
Swift symbol index over a ~30 MB standalone binary (~45k local Swift symbols)
spent ~8.5s of a 12.8s run re-opening the file as a dyld cache tens of thousands
of times, all discarded.

Memoize the negative result with a one-shot flag so the failed init runs at most
once. `_cache`/`_fullCache` success paths are unchanged; a genuine cache image
still resolves and caches on first touch. Behavior is identical, only repeated
failed loads are elided.

Verified: byte-identical output from a downstream Swift metadata dumper across
five fixture dylibs; the same dump on a 30 MB binary went 12.8s -> 4.3s and an
interface reconstruction 19s -> 5.1s, with `DyldCache.init` dropping from ~99%
of samples to absent in a sampling profile.
CamdenBopp added a commit to CamdenBopp/swiftdc that referenced this pull request Sep 12, 2026
dump / interface / analyze on a large on-disk binary were dominated by
repeatedly re-opening the dyld shared cache. `MachOFile.cache` lazily tries
`DyldCache.init(url:)` and stores the result, but a standalone (non-cache)
binary's failed load leaves `_cache` nil, indistinguishable from "not yet
attempted", so every access re-runs it. MachOSwiftSection's symbol-index sweep
touches `.cache` once per local Swift symbol (~45k on swiftdc itself), so the
build spent ~8.5s of a 12.8s dump re-opening and discarding the cache.

Fixed upstream as MxIris-Reverse-Engineering/MachOKit#1 (memoize the negative
result). Pinned here to CamdenBopp/MachOKit at a fork of the 0.52.102 tag
carrying only that change; as the root package this revision requirement
overrides MachOSwiftSection's transitive 0.52.101 ..< 0.53.0 range. Revert to an
upstream version pin once #1 ships in a release.

Measured on swiftdc itself (~30 MB): dump 12.8s -> 4.3s, interface 19s -> 5.1s.
Verified: byte-identical dump on all five fixture dylibs; full suite (204 tests)
green; DyldCache.init drops from ~99% of profile samples to absent.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant