From 81044b2673b6e845b604433c518918d49a609198 Mon Sep 17 00:00:00 2001 From: gabriel Date: Thu, 8 Oct 2026 11:14:04 -0500 Subject: [PATCH 1/2] Mix the numeric Map/Set key hash map_hash_key hashes a number as (lo ^ hi) * 3163 over the bits of its double. For an integer below 2048 the low word is zero and the low ten bits of the high word are zero, and multiplying by an odd constant keeps them zero, so every such key lands in the same bucket while the table has up to 1024 buckets. The ids 0..1999 all share one bucket of 1024 in a Map, and looking each one up once takes 2,001,000 key comparisons. Run the value through the lowbias32 finalizer before the seed and tag are mixed in. It is a bijection on uint32_t, so keys that hashed equal still do; only bucket placement changes. The same 2000 keys now use 869 of 1024 buckets, the longest chain is 6, and the lookups take 3,945 comparisons. Iteration order is unaffected; it follows the record list. --- quickjs.c | 15 ++++++++++++++- tests/test_builtin.js | 8 ++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/quickjs.c b/quickjs.c index 6b5541e28..4dcc8964e 100644 --- a/quickjs.c +++ b/quickjs.c @@ -53369,6 +53369,18 @@ static JSValueConst map_normalize_key_const(JSContext *ctx, JSValueConst key) return safe_const(map_normalize_key(ctx, unsafe_unconst(key))); } +/* lowbias32 integer finalizer. A bijection on uint32_t that mixes the high + bits into the low bits, which are the ones that select the bucket. */ +static inline uint32_t map_hash_mix(uint32_t h) +{ + h ^= h >> 16; + h *= 0x7feb352d; + h ^= h >> 15; + h *= 0x846ca68b; + h ^= h >> 16; + return h; +} + /* XXX: better hash ? */ static uint32_t map_hash_key(JSContext *ctx, JSValueConst key) { @@ -53409,7 +53421,8 @@ static uint32_t map_hash_key(JSContext *ctx, JSValueConst key) d = NAN; hash_float64: u.d = d; - h = (u.u32[0] ^ u.u32[1]) * 3163; + /* small integers all have the same low bits here */ + h = map_hash_mix((u.u32[0] ^ u.u32[1]) * 3163); tag = JS_TAG_FLOAT64; break; default: diff --git a/tests/test_builtin.js b/tests/test_builtin.js index 55931fb48..e220c7dbd 100644 --- a/tests/test_builtin.js +++ b/tests/test_builtin.js @@ -990,6 +990,14 @@ function test_map() for (var i = 0; i < n; i++) { a.set(i, i); } + for (var i = 0; i < n; i++) { + assert(a.get(i), i); + } + assert(a.get(-0), 0); + assert(a.get(1.0), 1); + a.set(NaN, 2); + assert(a.get(0 / 0), 2); + assert(a.size, n + 1); a.set(-2147483648, 1); assert(a.get(-2147483648), 1); assert(a.get(-2147483647 - 1), 1); From dae2327b907cdbccaddf8602a713dc3e4a6e87c0 Mon Sep 17 00:00:00 2001 From: gabriel Date: Thu, 8 Oct 2026 11:14:16 -0500 Subject: [PATCH 2/2] Mix the object and symbol Map/Set key hash Object and symbol keys hash as their address times 3163. Heap pointers are at least 8 or 16 byte aligned, and the odd multiplier keeps those low bits zero, so a Map keyed by objects uses at most 1 in 8 or 1 in 16 of its buckets, and fewer when the objects are allocated at a regular stride: 1000 objects 64 bytes apart use 8 of 512 buckets. Run the value through the same finalizer as numeric keys. As before, equal keys still hash equal and only bucket placement changes. --- quickjs.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/quickjs.c b/quickjs.c index 4dcc8964e..5cc08a6e6 100644 --- a/quickjs.c +++ b/quickjs.c @@ -53403,6 +53403,8 @@ static uint32_t map_hash_key(JSContext *ctx, JSValueConst key) case JS_TAG_OBJECT: case JS_TAG_SYMBOL: h = (uintptr_t)JS_VALUE_GET_PTR(key) * 3163; + /* heap pointers are aligned, their low bits are always zero */ + h = map_hash_mix(h); break; case JS_TAG_INT: d = JS_VALUE_GET_INT(key);