Repository navigation
Conversation
Backport quickjs-ng PR #1815 by Gabriel Elkind (pixelpax) as integration patch 0033: quickjs-ng/quickjs#1815 `map_hash_key` hashes an object or symbol key as its address times 3163 and a number as `(lo ^ hi) * 3163` over its double, and the bucket is the low bits of the hash. Heap pointers are 16-byte aligned, so object and symbol keys used at most 1 bucket in 16, and every integer below 2048 landed in the same bucket. The patch passes both through the lowbias32 finalizer, a bijection on uint32_t, before the tag is mixed in. Equality is still `js_same_value_zero` and iteration follows the record list, so only bucket placement changes. Lookup cost on aarch64 (interpreter, best of 3 runs): object keys 1.6x to 2.4x the speed, small integer keys 6x to 7.4x, strings unchanged; keys of the form 2**53 + 2i, which the old hash happened to spread perfectly, are 0.78x (22% slower), as #1815 reports. The patch should be dropped when the baseline moves to a quickjs-ng release that contains #1815. Co-authored-by: Gabriel Elkind <1467013+pixelpax@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟢 Approval recommended
The backport matches the upstream change, preserves collection semantics, and includes appropriate integrity and regression coverage.
0 open findings
What changed in this PR
Backports quickjs-ng #1815 to improve Map and Set hash-bucket distribution.
Changes:
- Adds lowbias32 mixing for numeric, object, and symbol keys.
- Adds semantic regression coverage for affected key families.
- Registers, documents, and fingerprints the integration patch.
| File | Description |
|---|---|
sys/patches/0033-map-hash-mix.patch |
Implements hash mixing. |
sys/build_support/patch.rs |
Registers patch and fingerprints. |
sys/tests/jit_patch.rs |
Includes patch in integration tests. |
core/src/lib.rs |
Adds collection semantics tests. |
sys/patches/README.md |
Documents the backport. |
CHANGELOG.md |
Records the fix. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
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.
Issue # (if available)
Backport of quickjs-ng/quickjs#1815 by @pixelpax, which is open upstream and not in a quickjs-ng release yet. The C change is theirs, unchanged except for the baseline it applies to.
Description of changes
map_hash_keyhashes an object or symbol key as its address times 3163, and a number as(lo ^ hi) * 3163over the bits of its double. The bucket is the low bits of that hash (h & (hash_size - 1)). Two consequences on the pinned baseline (quickjs-ngfd0a021):This PR adds integration patch
0033-map-hash-mix.patch. It runs both hashes through the lowbias32 finalizer, a bijection onuint32_t, before the tag is mixed in. That is the same function and the same two call sites as #1815. Equality is still decided byjs_same_value_zero, and iteration still follows the record list, so only bucket placement changes. String and BigInt keys are untouched.The patch should be dropped when the baseline moves to a quickjs-ng release that contains #1815.
sys/patches/README.mdsays so.How it was found. gpui-qs runs Quickshell QML on gpui-shell (through gpui-kit's rquickjs). Its reactive runtime keys Maps, Sets and WeakMaps by objects on every property read and change. In a native profile of a 600-item panel updating at 5 Hz,
map_find_recordwas 18% of busy main-thread time.Measured effect. Lookup cost in ns per lookup, interpreter,
rquickjs-clirelease build of this branch againstmain(96593cd). Best of 5 rounds inside each run, then the three runs shown. aarch64 (Apple M-series under Asahi Linux), load average about 6. The script is below.main(ns)main{ i }{ i }Symbol(i)Symbol(i)Setof objects,hasWeakMapof objects0..n0..ni + 0.5"key" + i2**53 + 2i2**53 + 2igets slower. Above 2^53, consecutive doubles differ only in their low word, and the old multiplication happened to send such a run to every bucket in turn. After the patch those keys cost what every other key family costs. #1815 reports the same family and the same direction.In gpui-qs, the same idea (measured with a MurmurHash3 finalizer rather than lowbias32) lowered whole-process CPU on that panel by about 5% and its render time by about 12%.
This is an interpreter fix, not a JIT optimization, so the three-engine README matrix in
AGENTS.mddoes not apply.Where the remaining time goes. After the patch a lookup is still 10 to 20 times slower than in Bun, so I split the cost into layers. One script, best of 7 rounds of 2,000,000 operations,
performance.now(), same machine. Every engine was run three times, interleaved; the rows below varied by under 1%.quickjs-jitis this repository at96593cdwithjit-abi, as gpui-shell builds it, run through a file runner overContext::full; the stock engines are their ownqjsbuilt with CMakeRelease.bcb9ad19b06171(master)fd0a021(this baseline)96593cdo.xf(i)m.get(intKey)m.get(objKey)m.get(stringKey)set.has(objKey)(The "this PR" column is the default build without
jit-abi; thejit-abibuild of96593cdadds 2 to 7 ns per row over the default build.)What this says:
map_hash64/map_hash32into the high bits, so it already does not have this defect. quickjs-ngmasterstill has it (hence #1815), which is why its integer row is still near 690 ns.masterruns the base rows 1.2 to 1.3 times as fast as thefd0a021baseline, and this repository's build of that baseline is a further 1.1 to 1.3 times slower than stockqjs(thejit-abiprobes, assertions thatNDEBUGwould remove, and build flags each contribute; I did not separate them fully). For host-heavy embedders like gpui-shell, where the JIT does not engage (benchmarks/results/gpui-shell-jit-v1.mdreports 0.99x to 1.00x on the realistic 443-node panel), moving the baseline forward looks like a larger lever than any single patch. That is outside this PR.Tests.
core/src/lib.rsgainsmap_keys::every_key_family_is_found_after_mixing. It covers integer, double,-0,1.0,NaN, large doubles, BigInt, object, symbol andSymbol.forkeys in Map, Set and WeakMap, deletes, and insertion order. It is a semantics guard: the speedup itself cannot be observed without timing, as #1815 also notes.sys/tests/jit_patch.rslists the new patch, andbuild_support/patch.rspins its digest and the patchedquickjs.cfingerprint.Run locally (aarch64 Linux):
cargo fmt --all -- --checkcargo clippy -p quickjs-jit-core -p quickjs-jit-sys --all-targets(no warnings)cargo test -p quickjs-jit-sys --test jit_patch(13 passed)cargo test -p quickjs-jit-core --lib(151 passed)cargo test -p quickjs-jit-runtime --features compiler,test-support --test abi --test manifest_versions --test semantics --test helpers --test gpui_shell_surface -- --test-threads=1(all passed)cargo test -p quickjs-jit-runtime --release --features compiler,test-support --test opcodes --test differential --test regressions -- --test-threads=1(all passed)Not run locally: the sanitizer, test262, Windows and wasm jobs.
Benchmark script (
map-keys.js)Run by piping it as one line into
target/release/rquickjs-cli.Checklist
Credit for the fix and its analysis goes to @pixelpax in quickjs-ng/quickjs#1815. Their commits are credited with a
Co-authored-bytrailer.Prepared with AI assistance: Claude Code wrote the patch backport, the test and this description, and ran the measurements above on my machine.
🤖 Generated with Claude Code