Skip to content

ParparVM collections: fix an O(n) HashMap miss, and compact Hashtable - #5722

Merged
shai-almog merged 3 commits into
masterfrom
parparvm-map-probe-and-compact-hashtable
Sep 7, 2026
Merged

ParparVM collections: fix an O(n) HashMap miss, and compact Hashtable#5722
shai-almog merged 3 commits into
masterfrom
parparvm-map-probe-and-compact-hashtable

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

Rationale lives in the code comments and in vm/CLAUDE.md; this is the summary.

The bug

java.util.HashMap on ParparVM is open addressed with linear probing, and cn1Marker spreads the hash with the JDK's h ^= h >>> 16 -- a spread designed for a chained map, where colliding keys share a bucket and clustering costs nothing. Integer.hashCode() is the value itself, so a dense key range (ids from zero, epoch seconds, counters, indices) lands at slot == value: one contiguous run with no gap. Linear probing then walked that whole run for any probe that entered it and did not find its key.

Average probe length for a miss:

entries before after
20,000 2,547 1.39
100,000 16,742 1.53
1,000,000 222,721 1.98

O(n) per unsuccessful lookup, reaching get returning null, containsKey returning false, and put of a key not adjacent to the run. 3M containsKey calls against a 100k map took 32.7 seconds, against 49.9ms on HotSpot.

Hits stayed at exactly one probe throughout, which is why hashMapChurn -- get and put on keys that are present -- reported a healthy 1.12x the entire time it existed.

The fix

The probe sequence, not the spread (CPython's dict recurrence). Scrambling the hash was tried first and rejected on measurement: it fixes the miss but destroys the sequential placement, and dense-key build/scan shapes regressed 1.8x-2.2x -- HotSpot's HashMap gets that same locality from that same weak spread. Keeping the first probe at marker & mask and perturbing only the steps after it keeps the locality and still leaves the run at once.

Measured, interleaved best-of-N, all checksums bit-identical

bench before after
missHeavy 32,698 ms 44.9 ms (728x)
tombstones 7,781 ms 16.5 ms (471x)
stringKeys 33.6 ms 25.5 ms
linkedStringKeys 37.1 ms 31.0 ms
largeTable 26.6 ms 33.3 ms (the cost)
hashtableBuild 167.8 ms 96.6 ms
identityMapLookup 13.4 ms 5.0 ms
identityMapBuild 26.1 ms 9.8 ms

vm/benchmarks geomean moved 1.005, i.e. not at all.

Hashtable

Given the compact layout it never received: no Entry per mapping, a power-of-two mask instead of the % integer division it did on every operation, and the same perturbed probe. Build 1.74x.

Lookup only 1.09x, and that is the useful part: with identical probe code Hashtable is still 3x HashMap on the same workload, and the difference is synchronized. This VM keeps monitors in an address-keyed side table rather than an object header word, so an uncontended accessor costs more than the whole lookup. Further work there belongs on the monitor, not the map.

IdentityHashMap

Do not port HotSpot's hash function to this VM. Copying java.util.IdentityHashMap's made it measurably slower. HotSpot's identityHashCode is a scrambled per-object value; ours is a truncated object address, measured 32-byte aligned. The JDK's (h << 1) - (h << 8) is h * -254 -- an even multiplier, so it preserves those five zero bits and adds a sixth: 2045 distinct home slots out of 65536, 12.73 probes per lookup. The old % survived only because a non-power-of-two modulus folds high bits back in as a side effect.

Folding explicitly (h ^= h >>> 16) reaches 50000/65536 slots and 1.00 probes. Adding a multiply on top makes it worse again (2.36) -- sequential allocation means sequential addresses, so one fold is already near a perfect hash. 2.65x on lookup and build.

Its rehash() overflow guard also turned an overflowed length into 1 -- an odd array length, which would have split every key from its value.

Tests

  • MapBench adds the shapes hashMapChurn cannot reach: miss-heavy, String-keyed, large-table, tombstone-heavy, grow-dominated, identity-keyed. Deliberately outside CommonWorkloads, which port_status.py pins at exactly ten ids.
  • HtTorture (69 assertions) and IdmTorture join the gauntlet. HtTorture was written and verified against the chained Hashtable before the rewrite -- a torture that only ever runs against new code proves nothing. IdmTorture was verified non-vacuous by injecting a relocation off-by-one, which it caught.
  • IdmProbe is a diagnostic, not a benchmark: it must run on the target, because the identity-hash distribution is a property of the allocator.

GAUNTLET GREEN (all tortures bit-identical to HotSpot), vm/benchmarks checksums bit-identical.

Also removes three dead constants from parparvm_runtime.js, two of which named HashMap fields the compact layout deleted.

Not addressed

LinkedHashMap inherits the compact layout but overrides get/put/remove/clear in Java rather than native (the natives are deliberately base-class-only), which measures 1.21x over HashMap on the same shape.

🤖 Generated with Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 6, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-06T18:07:57.255183Z bf080fe New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1b0ce25fa0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vm/JavaAPI/src/java/util/Hashtable.java Outdated
int capacity = cn1Meta.length;
// Double only when genuinely full; a table that is merely
// tombstone-heavy is rebuilt at the same size, which purges them.
int newCapacity = (elementCount * 2 >= capacity) ? capacity << 1 : capacity;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Grow instead of repeatedly rebuilding below half capacity

When a caller supplies a load factor below 0.5, reaching threshold does not satisfy this hard-coded half-capacity test, so rehash() rebuilds at the same size even when there are no tombstones. The rebuilt table remains at or above its unchanged threshold, causing every subsequent put to rehash all live entries until the map becomes half full; with very small positive load factors this makes bulk insertion quadratic and effectively ignores the requested load factor. Choose growth based on the configured threshold/live count, reserving same-capacity rebuilds for tables whose threshold was reached because of tombstones.

Useful? React with 👍 / 👎.

@shai-almog

shai-almog commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 166 screenshots: 166 matched.
Native Windows port (x64 / Intel-AMD): full hellocodenameone screenshot suite rendered offscreen with Direct2D/DirectWrite, plus the real benchmarks (base64 native/CN1/SIMD, image createMask/applyMask/modifyAlpha/PNG/JPEG, SSE2 SIMD kernels). Compared against the in-repo baseline in scripts/windows/screenshots.

Benchmark Results

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 54ms / native 4ms = 13.5x speedup
SIMD float-mul (64K x300) java 61ms / native 4ms = 15.2x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 native bridge unavailable (CN1 + SIMD + image benchmarks only)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path gated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode 171.000 ms
Base64 CN1 decode 120.000 ms
Base64 SIMD encode 85.000 ms
Base64 encode ratio (SIMD/CN1) 0.497x (50.3% faster)
Base64 SIMD decode 84.000 ms
Base64 decode ratio (SIMD/CN1) 0.700x (30.0% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 28.000 ms
Image createMask (SIMD on) 4.000 ms
Image createMask ratio (SIMD on/off) 0.143x (85.7% faster)
Image applyMask (SIMD off) 62.000 ms
Image applyMask (SIMD on) 23.000 ms
Image applyMask ratio (SIMD on/off) 0.371x (62.9% faster)
Image modifyAlpha (SIMD off) 70.000 ms
Image modifyAlpha (SIMD on) 56.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.800x (20.0% faster)
Image modifyAlpha removeColor (SIMD off) 38.000 ms
Image modifyAlpha removeColor (SIMD on) 55.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 1.447x (44.7% slower)

@shai-almog

shai-almog commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 166 screenshots: 166 matched.
Native Windows port, REAL shipping pipeline: the hellocodenameone screenshot suite rendered by a binary CROSS-COMPILED on Linux (clang-cl + xwin, WebView2 linked) and RUN on a Windows x64 runner. Compared against the in-repo baseline in scripts/windows/screenshots.

Benchmark Results

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 45ms / native 3ms = 15.0x speedup
SIMD float-mul (64K x300) java 46ms / native 3ms = 15.3x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 native bridge unavailable (CN1 + SIMD + image benchmarks only)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path gated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode 146.000 ms
Base64 CN1 decode 99.000 ms
Base64 SIMD encode 75.000 ms
Base64 encode ratio (SIMD/CN1) 0.514x (48.6% faster)
Base64 SIMD decode 73.000 ms
Base64 decode ratio (SIMD/CN1) 0.737x (26.3% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 32.000 ms
Image createMask (SIMD on) 5.000 ms
Image createMask ratio (SIMD on/off) 0.156x (84.4% faster)
Image applyMask (SIMD off) 42.000 ms
Image applyMask (SIMD on) 55.000 ms
Image applyMask ratio (SIMD on/off) 1.310x (31.0% slower)
Image modifyAlpha (SIMD off) 63.000 ms
Image modifyAlpha (SIMD on) 30.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.476x (52.4% faster)
Image modifyAlpha removeColor (SIMD off) 62.000 ms
Image modifyAlpha removeColor (SIMD on) 42.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.677x (32.3% faster)

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • build-hint-catalog: 0 findings (no issues)
    • build-hint-tools: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Cloudflare Preview

@shai-almog

shai-almog commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 166 screenshots: 166 matched.
Native Linux port (x64), GTK3/Cairo/Pango, ParparVM bytecode-to-C (no JVM): the hellocodenameone screenshot suite rendered by a native ELF built + run on the GitHub x64 runner. Baseline: scripts/linux/screenshots.

@shai-almog

shai-almog commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 166 screenshots: 166 matched.
Native Linux port (arm64), GTK3/Cairo/Pango, ParparVM bytecode-to-C (no JVM): the hellocodenameone screenshot suite rendered by a native ELF built + run on the GitHub arm64 runner. Baseline: scripts/linux/screenshots-arm.

@shai-almog

shai-almog commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 166 screenshots: 166 matched.
Native Windows port (arm64 / Apple Silicon - Arm): full hellocodenameone screenshot suite rendered offscreen with Direct2D/DirectWrite, plus the real benchmarks (base64 native/CN1/SIMD, image createMask/applyMask/modifyAlpha/PNG/JPEG, NEON SIMD kernels). Compared against the in-repo baseline in scripts/windows/screenshots.

Benchmark Results

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 59ms / native 3ms = 19.6x speedup
SIMD float-mul (64K x300) java 54ms / native 4ms = 13.5x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 native bridge unavailable (CN1 + SIMD + image benchmarks only)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path gated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode 266.000 ms
Base64 CN1 decode 156.000 ms
Base64 SIMD encode 65.000 ms
Base64 encode ratio (SIMD/CN1) 0.244x (75.6% faster)
Base64 SIMD decode 63.000 ms
Base64 decode ratio (SIMD/CN1) 0.404x (59.6% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 31.000 ms
Image createMask (SIMD on) 3.000 ms
Image createMask ratio (SIMD on/off) 0.097x (90.3% faster)
Image applyMask (SIMD off) 26.000 ms
Image applyMask (SIMD on) 41.000 ms
Image applyMask ratio (SIMD on/off) 1.577x (57.7% slower)
Image modifyAlpha (SIMD off) 15.000 ms
Image modifyAlpha (SIMD on) 10.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.667x (33.3% faster)
Image modifyAlpha removeColor (SIMD off) 22.000 ms
Image modifyAlpha removeColor (SIMD on) 43.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 1.955x (95.5% slower)

@shai-almog

shai-almog commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 181 screenshots: 181 matched.
✅ JavaScript-port screenshot tests passed.

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

✅ ByteCodeTranslator Quality Report

Test & Coverage

  • Tests: 562 total, 0 failed, 54 skipped

Benchmark Results

  • Execution Time: 24054 ms

  • Hotspots (Top 20 sampled methods):

    • 23.66% com.codename1.tools.translator.Parser.addToConstantPool (485 samples)
    • 7.46% java.util.ArrayList.indexOf (153 samples)
    • 4.39% com.codename1.tools.translator.ByteCodeClass.hasDeclaredMethod (90 samples)
    • 4.00% java.lang.StringBuilder.append (82 samples)
    • 3.02% com.codename1.tools.translator.Parser.cn1EnsureSubclassIndex (62 samples)
    • 2.93% com.codename1.tools.translator.Parser.classIndex (60 samples)
    • 2.83% com.codename1.tools.translator.ByteCodeClass.fillVirtualMethodTable (58 samples)
    • 2.34% org.objectweb.asm.tree.analysis.Analyzer.analyze (48 samples)
    • 1.95% org.objectweb.asm.tree.analysis.Analyzer.findSubroutine (40 samples)
    • 1.51% com.codename1.tools.translator.bytecodes.Invoke.resolveDirectTarget (31 samples)
    • 1.32% com.codename1.tools.translator.BytecodeMethod.optimize (27 samples)
    • 1.22% com.codename1.tools.translator.Parser.cullMethods (25 samples)
    • 1.22% com.codename1.tools.translator.BytecodeMethod.appendCMethodPrefix (25 samples)
    • 1.17% java.util.HashMap.hash (24 samples)
    • 1.12% java.lang.Object.hashCode (23 samples)
    • 1.02% com.codename1.tools.translator.BytecodeMethod.addInstruction (21 samples)
    • 0.98% java.lang.String.equals (20 samples)
    • 0.88% java.util.TreeMap.getEntry (18 samples)
    • 0.88% java.lang.System.identityHashCode (18 samples)
    • 0.88% org.objectweb.asm.ClassReader.readCode (18 samples)
  • ⚠️ Coverage report not generated.

Static Analysis

  • ✅ SpotBugs: no findings (report was not generated by the build).
  • ⚠️ PMD report not generated.
  • ⚠️ Checkstyle report not generated.

Generated automatically by the PR CI workflow.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

public synchronized boolean containsKey(Object key) {
return getEntry(key) != null;

P2 Badge Avoid allocating an Entry for every containsKey hit

When containsKey finds a present key, it delegates to getEntry, which constructs a new Entry solely so this method can compare it with null. This turns every successful membership check into an allocation, whereas the previous chained representation reused its stored entry and the compact probe can answer directly from the returned index. Hot paths using Hashtable.containsKey, such as the layout and theme maps, will therefore create avoidable garbage; check cn1FindSlot(key) >= 0 here instead.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

shai-almog and others added 3 commits September 6, 2026 20:55
HashMap here is open addressed with linear probing, and cn1Marker spreads the
hash with the JDK's h ^= h >>> 16 -- a spread designed for a CHAINED map, where
colliding keys share a bucket and clustering costs nothing. Integer.hashCode()
is the value itself, so a dense key range (ids from zero, epoch seconds,
counters, indices) lands at slot == value: one contiguous run of occupied slots
with no gap. Linear probing then walked that entire run for any probe that
entered it and did not find its key.

Measured average probe length for a MISS: 2547 slots at 20k keys, 16742 at
100k, 222721 at 1M -- O(n) per unsuccessful lookup, reaching get returning null,
containsKey returning false, and put of a key not adjacent to the run. In wall
time, 3M containsKey calls against a 100k map took 32.7 SECONDS against 49.9ms
on HotSpot. A remove/insert churn shape took 7.8 seconds against 12.5ms.

Hits stayed at exactly one probe throughout, which is why hashMapChurn -- get
and put on keys that are PRESENT -- reported a healthy 1.12x the whole time.

The fix is the probe SEQUENCE, not the spread. Scrambling the hash was tried
first and rejected on measurement: it fixes the miss but destroys the sequential
placement, and dense-key build and scan shapes regressed 1.8x-2.2x (HotSpot's
HashMap gets that same locality from that same weak spread). Keeping the first
probe at marker & mask and perturbing only the steps after it -- CPython's dict
recurrence -- keeps the locality and still leaves the run at once.

  missHeavy   32698ms -> 44.9ms   (728x)
  tombstones   7781ms -> 16.5ms   (471x)
  stringKeys     33.6 -> 25.5ms
  largeTable     26.6 -> 33.3ms   (the cost)
  vm/benchmarks geomean 1.005, i.e. unmoved

Hashtable gets the same compact layout it never received: no Entry object per
mapping, a power-of-two mask instead of the % integer division it did on every
operation, and the same perturbed probe. Build 1.74x. Lookup only 1.09x, and
that is the useful part -- with identical probe code Hashtable is still 3x
HashMap on the same workload, and the difference is synchronized. This VM keeps
monitors in an address-keyed side table rather than an object header word, so an
uncontended accessor costs more than the whole lookup. Further work there
belongs on the monitor, not the map.

IdentityHashMap indexed with % (length / 2) on an unscrambled identity hash.
Copying java.util.IdentityHashMap's hash function made it SLOWER, and the reason
is worth recording: HotSpot's identityHashCode is a scrambled per-object value,
ours is a truncated object ADDRESS, measured 32-byte aligned. The JDK's
(h << 1) - (h << 8) is h * -254, an EVEN multiplier, so it preserves those five
zero bits and adds a sixth -- 2045 distinct home slots out of 65536 and 12.73
probes per lookup. The old modulo survived only because a non-power-of-two
modulus folds high bits back in as a side effect. Folding explicitly
(h ^= h >>> 16) reaches 50000/65536 slots and 1.00 probes; adding a multiply on
top makes it worse again, because sequential allocation means sequential
addresses and one fold is already near a perfect hash. 2.65x on lookup and
build. Its rehash() overflow guard also turned an overflowed length into 1 -- an
odd array length, which would have split every key from its value.

Tests: MapBench adds the map shapes hashMapChurn cannot reach (miss-heavy,
String-keyed, large-table, tombstone-heavy, grow-dominated, identity-keyed). It
is deliberately outside CommonWorkloads, which port_status.py pins at exactly
ten ids. HtTorture and IdmTorture join the gauntlet; HtTorture was written and
verified against the CHAINED Hashtable before the rewrite, and IdmTorture was
verified non-vacuous by injecting a relocation off-by-one that it caught.
IdmProbe is a diagnostic that must run on the target, because the identity-hash
distribution is a property of the allocator.

Also removes three dead constants from parparvm_runtime.js, two of which named
HashMap fields that the compact layout deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
check-copyright-headers fails any file a PR touches whose header is neither
the Codename One nor the Oracle GPLv2 + Classpath text. HashMap, Hashtable and
IdentityHashMap carry the Apache Software Foundation notice, because they are
Apache Harmony derived -- byte-identical in provenance to TimeZone.java and
LinkedHashMap.java, which the exclusions file already lists for exactly this
reason (LinkedHashMap was added when #5658 last modified it).

Excluding them is the correct fix and replacing the header is not: rewriting an
Apache-2.0 notice as the Codename One GPL header would misstate the licence of
third-party code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s allocation

Two review findings, both real.

The growth rule. All three compact maps chose between doubling and rebuilding
in place with `elementCount * 2 >= capacity` -- a capacity test standing in for
a threshold test, which silently assumes a load factor of 0.5 or more. Below
that, the threshold is reached while the table is still less than half full, so
the rebuild keeps the same capacity, the rebuilt table is immediately at its
threshold again, and every subsequent put rebuilds the whole table. Inserting
20000 entries at a load factor of 0.25 did 19999 rebuilds and rehashed 200
million entries: 22.3 SECONDS, against 16.4ms once fixed. The two-argument
constructors take any positive load factor, so `new HashMap<>(16, 0.25f)`
reached it from ordinary code.

`elementCount >= threshold` is the rule -- grow when the LIVE count has reached
the threshold, rebuild at the same size only when tombstones are what pushed
occupancy there. At 0.75 and 0.5 the two rules agree rebuild for rebuild, which
is why nothing existing moved, and why nothing existing could have caught it:
`MapBench.lowLoadFactorBuild` and `HtTorture`'s sparse case exist for this
alone, and the benchmark was verified to reproduce the 22.3s before the fix.

Fixed in Hashtable, HashMap and LinkedHashMap. Only Hashtable's was new here --
the other two carried it from #5327 -- but it is one rule with one fix, and
leaving two of the three would have been arbitrary.

containsKey. The compact Hashtable delegated to getEntry, which built an Entry
purely so the caller could compare it against null, turning every successful
membership test into an allocation; the chained representation it replaced
handed back the entry it already had. keySet().contains() routes here. It now
answers from the probe index. The other getEntry caller, entrySet().contains(),
genuinely needs the Entry for its equality test and is unchanged.

GAUNTLET GREEN.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shai-almog
shai-almog force-pushed the parparvm-map-probe-and-compact-hashtable branch from 44f97a8 to bf080fe Compare September 6, 2026 18:04

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bf080fe015

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +760 to +762
if (cn1Occupied >= threshold) {
rehash();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep mandatory growth independent of overridable rehash

When a Hashtable subclass overrides the protected rehash() method without calling super.rehash()—which remained functionally safe with the previous chained representation—insertions can now fill every open-addressed slot. Since cn1FindSlot() terminates only upon finding META_EMPTY and has no full-cycle check, the next operation for a missing key loops forever. Perform the growth needed to preserve an empty slot through a non-overridable internal method, rather than relying on the subclass hook.

Useful? React with 👍 / 👎.

@shai-almog

shai-almog commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 148 screenshots: 148 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 237 seconds

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 54ms / native 3ms = 18.0x speedup
SIMD float-mul (64K x300) java 52ms / native 2ms = 26.0x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 157.000 ms
Base64 CN1 decode 92.000 ms
Base64 native encode 653.000 ms
Base64 encode ratio (CN1/native) 0.240x (76.0% faster)
Base64 native decode 199.000 ms
Base64 decode ratio (CN1/native) 0.462x (53.8% faster)
Base64 SIMD encode 47.000 ms
Base64 encode ratio (SIMD/CN1) 0.299x (70.1% faster)
Base64 SIMD decode 44.000 ms
Base64 decode ratio (SIMD/CN1) 0.478x (52.2% faster)
Base64 encode ratio (SIMD/native) 0.072x (92.8% faster)
Base64 decode ratio (SIMD/native) 0.221x (77.9% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 7.000 ms
Image createMask (SIMD on) 1.000 ms
Image createMask ratio (SIMD on/off) 0.143x (85.7% faster)
Image applyMask (SIMD off) 32.000 ms
Image applyMask (SIMD on) 27.000 ms
Image applyMask ratio (SIMD on/off) 0.844x (15.6% faster)
Image modifyAlpha (SIMD off) 24.000 ms
Image modifyAlpha (SIMD on) 20.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.833x (16.7% faster)
Image modifyAlpha removeColor (SIMD off) 26.000 ms
Image modifyAlpha removeColor (SIMD on) 21.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.808x (19.2% faster)

@shai-almog

shai-almog commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 160 screenshots: 160 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 173 seconds

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 62ms / native 4ms = 15.5x speedup
SIMD float-mul (64K x300) java 59ms / native 3ms = 19.6x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 native bridge unavailable (CN1 + SIMD + image benchmarks only)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 144.000 ms
Base64 CN1 decode 91.000 ms
Image encode benchmark iterations 100
Image createMask (SIMD off) 8.000 ms
Image createMask (SIMD on) 2.000 ms
Image createMask ratio (SIMD on/off) 0.250x (75.0% faster)
Image applyMask (SIMD off) 41.000 ms
Image applyMask (SIMD on) 32.000 ms
Image applyMask ratio (SIMD on/off) 0.780x (22.0% faster)
Image modifyAlpha (SIMD off) 32.000 ms
Image modifyAlpha (SIMD on) 28.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.875x (12.5% faster)
Image modifyAlpha removeColor (SIMD off) 37.000 ms
Image modifyAlpha removeColor (SIMD on) 36.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.973x (2.7% faster)

@shai-almog

shai-almog commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 149 screenshots: 149 matched.
✅ Native iOS Metal screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 1499 seconds

Build and Run Timing

Metric Duration
Simulator Boot 71000 ms
Simulator Boot (Run) 2000 ms
App Install 19000 ms
App Launch 29000 ms
Test Execution 580000 ms

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 156ms / native 3ms = 52.0x speedup
SIMD float-mul (64K x300) java 314ms / native 6ms = 52.3x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 597.000 ms
Base64 CN1 decode 115.000 ms
Base64 native encode 399.000 ms
Base64 encode ratio (CN1/native) 1.496x (49.6% slower)
Base64 native decode 692.000 ms
Base64 decode ratio (CN1/native) 0.166x (83.4% faster)
Base64 SIMD encode 62.000 ms
Base64 encode ratio (SIMD/CN1) 0.104x (89.6% faster)
Base64 SIMD decode 60.000 ms
Base64 decode ratio (SIMD/CN1) 0.522x (47.8% faster)
Base64 encode ratio (SIMD/native) 0.155x (84.5% faster)
Base64 decode ratio (SIMD/native) 0.087x (91.3% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 14.000 ms
Image createMask (SIMD on) 2.000 ms
Image createMask ratio (SIMD on/off) 0.143x (85.7% faster)
Image applyMask (SIMD off) 57.000 ms
Image applyMask (SIMD on) 97.000 ms
Image applyMask ratio (SIMD on/off) 1.702x (70.2% slower)
Image modifyAlpha (SIMD off) 33.000 ms
Image modifyAlpha (SIMD on) 28.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.848x (15.2% faster)
Image modifyAlpha removeColor (SIMD off) 35.000 ms
Image modifyAlpha removeColor (SIMD on) 28.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.800x (20.0% faster)

@shai-almog

shai-almog commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 217 screenshots: 217 matched.
✅ Native Apple Watch (watchOS, Core Graphics) screenshot tests passed.

@shai-almog

shai-almog commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 143 screenshots: 143 matched.
✅ Native iOS screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 967 seconds

Build and Run Timing

Metric Duration
Simulator Boot 66000 ms
Simulator Boot (Run) 0 ms
App Install 15000 ms
App Launch 1000 ms
Test Execution 403000 ms

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 56ms / native 3ms = 18.6x speedup
SIMD float-mul (64K x300) java 97ms / native 3ms = 32.3x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 207.000 ms
Base64 CN1 decode 107.000 ms
Base64 native encode 592.000 ms
Base64 encode ratio (CN1/native) 0.350x (65.0% faster)
Base64 native decode 394.000 ms
Base64 decode ratio (CN1/native) 0.272x (72.8% faster)
Base64 SIMD encode 90.000 ms
Base64 encode ratio (SIMD/CN1) 0.435x (56.5% faster)
Base64 SIMD decode 86.000 ms
Base64 decode ratio (SIMD/CN1) 0.804x (19.6% faster)
Base64 encode ratio (SIMD/native) 0.152x (84.8% faster)
Base64 decode ratio (SIMD/native) 0.218x (78.2% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 8.000 ms
Image createMask (SIMD on) 33.000 ms
Image createMask ratio (SIMD on/off) 4.125x (312.5% slower)
Image applyMask (SIMD off) 148.000 ms
Image applyMask (SIMD on) 171.000 ms
Image applyMask ratio (SIMD on/off) 1.155x (15.5% slower)
Image modifyAlpha (SIMD off) 123.000 ms
Image modifyAlpha (SIMD on) 152.000 ms
Image modifyAlpha ratio (SIMD on/off) 1.236x (23.6% slower)
Image modifyAlpha removeColor (SIMD off) 115.000 ms
Image modifyAlpha removeColor (SIMD on) 149.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 1.296x (29.6% slower)

@shai-almog

shai-almog commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 144 screenshots: 144 matched.
✅ Native Apple TV (tvOS, Metal) screenshot tests passed.

@shai-almog
shai-almog merged commit 963764b into master Sep 7, 2026
50 checks passed
@shai-almog
shai-almog deleted the parparvm-map-probe-and-compact-hashtable branch September 7, 2026 01:52
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