Skip to content

perf(link): stop exporting every symbol from native macOS executables#6533

Merged
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:perf/link-no-exported-symbols
Jul 17, 2026
Merged

perf(link): stop exporting every symbol from native macOS executables#6533
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:perf/link-no-exported-symbols

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Problem

A perry-compiled executable exports every external Rust/codegen symbol — 300,281 exports on a large app binary — with MH_WEAK_DEFINES/MH_BINDS_TO_WEAK set. dyld pays for that at every launch: weak-def coalescing plus bind-target resolution against a 300k-entry export trie.

Measured on a large compiled TUI app: --version takes 1.168s wall, and sampling shows ~80% of it inside dyld (trieWalk, resolveSymbol, hasExportedSymbol, weak-def map inserts) — ~0.9s of pure dynamic-linker overhead on every single launch, before any app code runs. node runs the same app's --version in 328ms.

Fix

Pass -Wl,-no_exported_symbols on the native macOS/iOS clang link when plugins are off.

Safety audit: nothing consumes the exports —

  • plugins dlopen their own dylibs and dlsym from their own handle (plugin.rs uses RTLD_NOW | RTLD_LOCAL, never dlopen(NULL) / RTLD_DEFAULT / RTLD_SELF into the host);
  • the plugin-host mode, which genuinely needs exports, force-exports its API via -u and is explicitly excluded (!ctx.needs_plugins, unchanged behavior).

Numbers

  • Exports: 300,281 → 3; WEAK_DEFINES/BINDS_TO_WEAK header flags cleared.
  • Binary size: 5.1MB → 4.9MB on a small program.
  • Full-app launch numbers (hyperfine, before/after) to follow in a comment once the big-binary rebuild completes — the profile attributes ~0.9s/launch to the eliminated dyld work.

Summary by CodeRabbit

  • Performance Improvements
    • Improved application startup performance for builds that do not use plugins by reducing exported symbol metadata.
    • Preserved exported symbols for plugin-enabled builds to maintain compatibility.

A perry-compiled executable carried a dynamic export trie with every
external Rust/codegen symbol — 300,281 exports on a large app binary —
with MH_WEAK_DEFINES/MH_BINDS_TO_WEAK set, so dyld spent ~0.9s of EVERY
launch on weak-def coalescing and bind resolution against it. Profiling
a large compiled app's --version: ~80% of the 1.17s wall time was inside
dyld (trieWalk / resolveSymbol / weak-def map inserts).

Nothing consumes those exports: plugins dlopen their own dylibs and
resolve symbols from their own handle (never dlsym(RTLD_DEFAULT/SELF)
into the host), and the plugin-host mode — which does need exports —
force-exports its API via -u and keeps the trie (gated on
ctx.needs_plugins, unchanged here).

Pass -Wl,-no_exported_symbols on the native macOS/iOS clang link when
plugins are off. Exports: 300,281 -> 3; WEAK_DEFINES/BINDS_TO_WEAK
cleared; binary 5.1MB -> 4.9MB on a small program.

Measured (large app binary, hyperfine): --version 1.168s of which ~0.9s
was dyld — the dyld share drops to near zero (numbers for the full
binary in the PR body).
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ed8ed153-8f93-4c22-bdab-9f3ed93b3f58

📥 Commits

Reviewing files that changed from the base of the PR and between 18c21e5 and d5e729c.

📒 Files selected for processing (1)
  • crates/perry/src/commands/compile/link/build_and_run.rs

📝 Walkthrough

Walkthrough

Apple-platform non-plugin builds now pass -Wl,-no_exported_symbols during linking, while plugin-host builds retain exported symbols.

Changes

Platform linker export control

Layer / File(s) Summary
Conditional exported-symbol suppression
crates/perry/src/commands/compile/link/build_and_run.rs
The non-Windows dead-code-stripping path adds -Wl,-no_exported_symbols when ctx.needs_plugins is false and preserves exported symbols for plugin-host builds.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • PerryTS/perry#5409: Adjusts linker export behavior in build_and_run_link based on ctx.needs_plugins.
  • PerryTS/perry#5480: Changes Windows plugin-host export handling using the same plugin requirement condition.
  • PerryTS/perry#5500: Preserves plugin ABI symbols required by plugin-host builds.

Suggested reviewers: nglmercer, tangledcircuitweb

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is missing the template's Summary, Changes, Related issue, Test plan, and Checklist sections. Reformat the description to match the template and add Summary, Changes, Related issue, Test plan commands, and Checklist items.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main macOS link/export-symbols change.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Full-app numbers (197MB binary, the large esbuild-bundled TUI app, hyperfine, 10 runs, isolated HOME):

metric before (old link) after (-no_exported_symbols) node (same app JS)
--version 1.168s ± 0.036 0.247s ± 0.016 (−79%) 0.328s
--help 5.071s ± 0.090 4.390s ± 0.175 (−0.68s) 0.715s
binary size 228MB 197MB (−31MB)
dynamic exports 300,281 3

The compiled binary's --version now starts faster than node for the first time. The --help path keeps its remaining gap for app-side reasons (eager per-literal regex NFA construction — addressed separately in #6534 — plus init work), but the fixed ~0.9s dyld tax is gone from every launch including the TUI.

Verification on this branch's toolchain: perry-runtime suite 1327/0 (single-threaded), gap shards 5/8 and 7/8 locally green (only reds: pre-triaged entries + the known local zlib rustc-hash link artifact on 3662/zlib_4917, which CI compiles fine), full TUI e2e smoke on the relinked bundle (launch → prompt → 36/36 keystrokes echoed, no crash).

@proggeramlug
proggeramlug merged commit 7279998 into PerryTS:main Jul 17, 2026
25 checks passed
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