Skip to content

Make dlsym O(1) in the number of exports - #28125

Open
basvandijk wants to merge 8 commits into
emscripten-core:mainfrom
basvandijk:dlsym-no-export-enumeration
Open

basvandijk wants to merge 8 commits into
emscripten-core:mainfrom
basvandijk:dlsym-no-export-enumeration

Conversation

@basvandijk

@basvandijk basvandijk commented Nov 1, 2026 •

Copy link
Copy Markdown

_dlsym_js enumerated every export of the library on each call, Object.keys(lib.exports).indexOf(symbol), before doing anything else. For RTLD_DEFAULT on a main module that is wasmImports, which holds every export of the main module, so each dlsym call cost O(N log N) in the number of exported symbols (V8 sorts the keys of a dictionary-mode object on every Object.keys), whether the symbol exists or not. The index is only needed when addFunction allocates a new table slot and the other threads replay the insertion by index (_dlsym_catchup_js, #18590).

This change tests membership with Object.prototype.hasOwnProperty.call, which gives the same answer as Object.keys(...).indexOf(symbol) != -1 for these plain objects, and computes the enumeration index only in the branch that stores it into symbolIndex. The thread catch-up protocol is unchanged.

Measured with a main module of N exported functions calling dlsym(RTLD_DEFAULT, name) (emcc -O2 -pthread -sMAIN_MODULE=1 -sEXIT_RUNTIME=1, Node 26.13.0, ms per call, hits):

exports before after
10,000 1.70 0.008
250,000 80.9 0.009

Lean 4's lean.js (247,834 exports) makes 2,630 dlsym calls at startup, which took 210 s of a 236 s run and take 17 ms with this change;

Fixes #28124.

Tests: test/runner core0.test_dlfcn_self core0.test_dylink_basics core0.test_pthread_dlsym core0.test_pthread_dylink core0.test_pthread_dylink_basics core0.test_pthread_dylink_main_module_1 other.test_dlsym_rtld_default other.test_dlsym_rtld_default_js_symbol other.test_dylink_pthread_static_data pass.

🤖 Generated with Claude Code

`_dlsym_js` enumerated every export of the library on each call,
`Object.keys(lib.exports).indexOf(symbol)`, before doing anything else.
For `RTLD_DEFAULT` on a main module that is `wasmImports`, which holds
every export of the main module, so each `dlsym` call cost O(N log N) in
the number of exported symbols, whether the symbol exists or not. The
index is only needed when `addFunction` allocates a new table slot and
the other threads replay the insertion by index (`_dlsym_catchup_js`).

Test membership with `hasOwnProperty`, which gives the same answer as
`Object.keys(...).indexOf(symbol) != -1` for these plain objects, and
compute the enumeration index only in the branch that stores it into
`symbolIndex`. A main module with 250,000 exported functions goes from
80.9 ms to 0.009 ms per `dlsym` call.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request optimizes dlsym in src/lib/libdylink.js by avoiding the enumeration of all library exports on every lookup. Instead, it uses hasOwnProperty for the existence check and only computes the symbol's index when a table slot is actually allocated. The reviewer suggested a further optimization to cache the keys array on the library object to prevent repeated enumeration during multiple allocations.

Comment thread src/lib/libdylink.js Resolved

@sbc100 sbc100 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine you will need to rebaseline to codesize tests here.

Comment thread src/lib/libdylink.js Resolved
Comment thread src/lib/libdylink.js Resolved
Review feedback: compute the enumeration index into `newSymIndex` so the
`makeSetValue` line stays simple.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@sbc100 sbc100 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm.

We could wait for #28129 to land .. or not, if you want to get this landed today?

This is an automatic change generated by tools/maint/rebaseline_tests.py.

The following (1) test expectation files were updated by
running the tests with `--rebaseline`:

```
codesize/test_codesize_hello_dylink_all.json: 862336 => 862390 [+54 bytes / +0.01%]

Average change: +0.01% (+0.01% - +0.01%)
```

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@basvandijk

Copy link
Copy Markdown
Author

We could wait for #28129 to land .. or not, if you want to get this landed today?

No rush. I'm happy to adjust this to Object.hasOwn after #28129 lands and redo the baselining.

Comment thread src/lib/libdylink.js Resolved
Comment thread AUTHORS Resolved
sbc100 added a commit to sbc100/emscripten that referenced this pull request Nov 2, 2026
In compile-time JS code (`src/`, `tools/`, and compile-time helpers in
`libwebgl.js`), use `Object.hasOwn` directly instead of
`hasOwnProperty`.

In runtime JS library code, introduce an internal `$hasOwn` helper in
`libcore.js` that aliases `Object.hasOwn` when targeting modern browser
versions (`MIN_FIREFOX_VERSION >= 92`, `MIN_CHROME_VERSION >= 93`, and
`MIN_SAFARI_VERSION >= 150400`) and falls back to
`Object.prototype.hasOwnProperty.call` otherwise.

Note that this change comes with minor codesize change, in some cases an
improvement and in some a regression.   Note that if we were to bump
our default min browser versions we would always see an improvement
(perhaps we should always target modern browser in our codesize tests?)

See emscripten-core#28125
Comment thread ChangeLog.md Resolved
sbc100 added a commit to sbc100/emscripten that referenced this pull request Nov 2, 2026
In compile-time JS code (`src/`, `tools/`, and compile-time helpers in
`libwebgl.js`), use `Object.hasOwn` directly instead of
`hasOwnProperty`.

In runtime JS library code, introduce an internal `$hasOwn` helper in
`libcore.js` that aliases `Object.hasOwn` when targeting modern browser
versions (`MIN_FIREFOX_VERSION >= 92`, `MIN_CHROME_VERSION >= 93`, and
`MIN_SAFARI_VERSION >= 150400`) and falls back to
`Object.prototype.hasOwnProperty.call` otherwise.

Note that this change comes with minor codesize change, in some cases an
improvement and in some a regression.   Note that if we were to bump
our default min browser versions we would always see an improvement
(perhaps we should always target modern browser in our codesize tests?)

See emscripten-core#28125
Comment thread ChangeLog.md Resolved
basvandijk and others added 3 commits November 2, 2026 19:49
…ration

Resolve the conflict in test/codesize/test_codesize_hello_dylink_all.json by
taking main's version; the next commit rebaselines it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This is an automatic change generated by tools/maint/rebaseline_tests.py.

The following (1) test expectation files were updated by
running the tests with `--rebaseline`:

```
codesize/test_codesize_hello_dylink_all.json: 862405 => 862459 [+54 bytes / +0.01%]

Average change: +0.01% (+0.01% - +0.01%)
```

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sbc100 added a commit to sbc100/emscripten that referenced this pull request Nov 2, 2026
In compile-time JS code (`src/`, `tools/`, and compile-time helpers in
`libwebgl.js`), use `Object.hasOwn` directly instead of
`hasOwnProperty`.

In runtime JS library code, introduce an internal `$hasOwn` helper in
`libcore.js` that aliases `Object.hasOwn` when targeting modern browser
versions (`MIN_FIREFOX_VERSION >= 92`, `MIN_CHROME_VERSION >= 93`, and
`MIN_SAFARI_VERSION >= 150400`) and falls back to
`Object.prototype.hasOwnProperty.call` otherwise.

Note that this change comes with minor codesize change, in some cases an
improvement and in some a regression.   Note that if we were to bump
our default min browser versions we would always see an improvement
(perhaps we should always target modern browser in our codesize tests?)

See emscripten-core#28125
sbc100 added a commit that referenced this pull request Nov 2, 2026
In compile-time JS code (`src/`, `tools/`, and compile-time helpers in
`libwebgl.js`), use `Object.hasOwn` directly instead of
`hasOwnProperty`.

In runtime JS library code, introduce an internal `$hasOwn` helper in
`libcore.js` that aliases `Object.hasOwn` when targeting modern browser
versions (`MIN_FIREFOX_VERSION >= 92`, `MIN_CHROME_VERSION >= 93`, and
`MIN_SAFARI_VERSION >= 150400`) and falls back to
`Object.prototype.hasOwnProperty.call` otherwise.

Note that this change comes with minor codesize change, in some cases an
improvement and in some a regression.   Note that if we were to bump
our default min browser versions we would always see an improvement
(perhaps we should always target modern browser in our codesize tests?)

See #28125
@sbc100

sbc100 commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator

I just landed #28129, so maybe you can rebase and use the hasOwn helper from there?

@basvandijk

Copy link
Copy Markdown
Author

Will do!

This branch has not been deployed

No deployments
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.

dlsym enumerates every export of the library on each call (Object.keys(lib.exports).indexOf)

2 participants