From d13a5977a05dc574194fda4ae211d216ba4cc55d Mon Sep 17 00:00:00 2001 From: Michael Simacek Date: Mon, 19 Oct 2026 19:51:36 +0200 Subject: [PATCH 1/2] Add a PE-friendly JSON memo table --- .../modules/json/JSONScannerBuiltins.java | 27 +++----- .../builtins/modules/json/PJSONScanner.java | 67 +++++++++++++++++-- 2 files changed, 74 insertions(+), 20 deletions(-) diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/json/JSONScannerBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/json/JSONScannerBuiltins.java index 2b02b5c3d9..816e830db5 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/json/JSONScannerBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/json/JSONScannerBuiltins.java @@ -15,7 +15,6 @@ import static com.oracle.graal.python.util.PythonUtils.toTruffleStringUncached; import static com.oracle.graal.python.util.PythonUtils.tsLiteral; -import java.util.HashMap; import java.util.List; import com.oracle.graal.python.PythonLanguage; @@ -26,6 +25,7 @@ import com.oracle.graal.python.builtins.CoreFunctions; import com.oracle.graal.python.builtins.PythonBuiltinClassType; import com.oracle.graal.python.builtins.PythonBuiltins; +import com.oracle.graal.python.builtins.modules.json.PJSONScanner.KeyMemo; import com.oracle.graal.python.builtins.objects.PNone; import com.oracle.graal.python.builtins.objects.common.EconomicMapStorage; import com.oracle.graal.python.builtins.objects.common.HashingStorage; @@ -174,6 +174,7 @@ protected PTuple call(VirtualFrame frame, PJSONScanner self, TruffleString strin @Cached PyLongFromUnicodeObject pyLongFromUnicodeObject, @Cached TruffleString.MaterializeNode materializeNode, @Cached TruffleString.HashCodeNode hashCodeNode, + @Cached TruffleString.EqualNode equalNode, @Cached TruffleString.CodePointAtIndexUTF32Node codePointAtIndexNode, @Cached TruffleString.RegionEqualByteIndexNode regionEqualByteIndexNode, @Cached TruffleString.SubstringByteIndexNode substringByteIndexNode, @@ -210,6 +211,7 @@ protected PTuple call(VirtualFrame frame, PJSONScanner self, TruffleString strin hashingStorageSetItem, pyLongFromUnicodeObject, hashCodeNode, + equalNode, codePointAtIndexNode, regionEqualByteIndexNode, substringByteIndexNode, @@ -232,6 +234,7 @@ protected PTuple call(VirtualFrame frame, PJSONScanner self, TruffleString strin hashingStorageSetItem, pyLongFromUnicodeObject, hashCodeNode, + equalNode, codePointAtIndexNode, regionEqualByteIndexNode, substringByteIndexNode, @@ -374,7 +377,7 @@ private enum ScannerState { @InliningCutoff private Object scanOnceUnicodeCutoff(VirtualFrame frame, BoundaryCallData boundaryCallData, Node inliningTarget, TruffleString string, int idx, IntRef nextIdx, - HashMap memo, + KeyMemo memo, boolean strict, Object objectHook, Object objectPairsHook, @@ -385,6 +388,7 @@ private Object scanOnceUnicodeCutoff(VirtualFrame frame, BoundaryCallData bounda HashingStorageSetItem hashingStorageSetItem, PyLongFromUnicodeObject pyLongFromUnicodeObject, TruffleString.HashCodeNode hashCodeNode, + TruffleString.EqualNode equalNode, TruffleString.CodePointAtIndexUTF32Node codePointAtIndexNode, TruffleString.RegionEqualByteIndexNode regionEqualByteIndexNode, TruffleString.SubstringByteIndexNode substringByteIndexNode, @@ -401,7 +405,7 @@ private Object scanOnceUnicodeCutoff(VirtualFrame frame, BoundaryCallData bounda parseInt, parseConstant, errorProfile, - hashingStorageSetItem, pyLongFromUnicodeObject, hashCodeNode, + hashingStorageSetItem, pyLongFromUnicodeObject, hashCodeNode, equalNode, codePointAtIndexNode, regionEqualByteIndexNode, substringByteIndexNode, @@ -414,7 +418,7 @@ private Object scanOnceUnicodeCutoff(VirtualFrame frame, BoundaryCallData bounda } private Object scanOnceUnicode(VirtualFrame frame, BoundaryCallData boundaryCallData, Node inliningTarget, TruffleString string, int idx, IntRef nextIdx, - HashMap memo, + KeyMemo memo, boolean strict, Object objectHook, Object objectPairsHook, @@ -425,6 +429,7 @@ private Object scanOnceUnicode(VirtualFrame frame, BoundaryCallData boundaryCall HashingStorageSetItem hashingStorageSetItem, PyLongFromUnicodeObject pyLongFromUnicodeObject, TruffleString.HashCodeNode hashCodeNode, + TruffleString.EqualNode equalNode, TruffleString.CodePointAtIndexUTF32Node codePointAtIndexNode, TruffleString.RegionEqualByteIndexNode regionEqualByteIndexNode, TruffleString.SubstringByteIndexNode substringByteIndexNode, @@ -551,13 +556,8 @@ private Object scanOnceUnicode(VirtualFrame frame, BoundaryCallData boundaryCall substringByteIndexNode, appendCodePointNode, appendSubstringByteIndexNode, builderToStringNode); - /* force hash computation */ - hashCodeNode.execute(newKey, TS_ENCODING); - TruffleString key = memoPutIfAbsent(memo, newKey); - if (key == null) { - key = newKey; - } - propertyKey = key; + int hash = hashCodeNode.execute(newKey, TS_ENCODING); + propertyKey = memo.intern(newKey, hash, equalNode); idx = nextIdx.value; /* * skip whitespace between key and : delimiter, read :, skip whitespace @@ -700,11 +700,6 @@ private Object scanOnceUnicode(VirtualFrame frame, BoundaryCallData boundaryCall } } - @TruffleBoundary - private static TruffleString memoPutIfAbsent(HashMap memo, TruffleString newKey) { - return memo.putIfAbsent(newKey, newKey); - } - private static final TruffleString[] DOUBLE_CONSTANTS = { tsLiteral("NaN"), tsLiteral("Infinity"), diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/json/PJSONScanner.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/json/PJSONScanner.java index ae62b85d31..d859087ca0 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/json/PJSONScanner.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/json/PJSONScanner.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -40,16 +40,17 @@ */ package com.oracle.graal.python.builtins.modules.json; -import java.util.HashMap; +import static com.oracle.graal.python.util.PythonUtils.TS_ENCODING; import com.oracle.graal.python.builtins.objects.object.PythonBuiltinObject; import com.oracle.truffle.api.CompilerAsserts; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.object.Shape; import com.oracle.truffle.api.strings.TruffleString; public final class PJSONScanner extends PythonBuiltinObject { - final HashMap memo; + final KeyMemo memo; final boolean strict; final Object objectHook; final Object objectPairsHook; @@ -60,7 +61,7 @@ public final class PJSONScanner extends PythonBuiltinObject { public PJSONScanner(Object cls, Shape instanceShape, boolean strict, Object objectHook, Object objectPairsHook, Object parseFloat, Object parseInt, Object parseConstant) { super(cls, instanceShape); CompilerAsserts.neverPartOfCompilation(); - this.memo = new HashMap<>(); + this.memo = new KeyMemo(); this.strict = strict; this.objectHook = objectHook; this.objectPairsHook = objectPairsHook; @@ -68,4 +69,62 @@ public PJSONScanner(Object cls, Shape instanceShape, boolean strict, Object obje this.parseInt = parseInt; this.parseConstant = parseConstant; } + + /** Interns JSON property names without calling the uncached Java string equality methods. */ + static final class KeyMemo { + private TruffleString[] keys = new TruffleString[16]; + private int size; + + TruffleString intern(TruffleString key, int hash, TruffleString.EqualNode equalNode) { + TruffleString[] table = keys; + int mask = table.length - 1; + int index = spread(hash) & mask; + TruffleString existing; + while ((existing = table[index]) != null) { + if (equalNode.execute(existing, key, TS_ENCODING)) { + return existing; + } + index = (index + 1) & mask; + } + if (size < table.length - (table.length >> 2)) { + table[index] = key; + size++; + } else { + growAndInsert(key, hash); + } + return key; + } + + private static int spread(int hash) { + return hash ^ (hash >>> 16); + } + + @TruffleBoundary + private void growAndInsert(TruffleString key, int hash) { + TruffleString[] oldKeys = keys; + if (oldKeys.length == 1 << 30) { + throw new OutOfMemoryError(); + } + TruffleString[] newKeys = new TruffleString[oldKeys.length << 1]; + for (TruffleString existing : oldKeys) { + if (existing != null) { + // All entries were hashed by HashCodeNode before being inserted. + insert(newKeys, existing, existing.hashCode()); + } + } + insert(newKeys, key, hash); + keys = newKeys; + size++; + } + + private static void insert(TruffleString[] table, TruffleString key, int hash) { + int mask = table.length - 1; + int index = spread(hash) & mask; + while (table[index] != null) { + index = (index + 1) & mask; + } + table[index] = key; + } + } + } From ae1990e769c8862ef25059cc9244d4c1b06459cf Mon Sep 17 00:00:00 2001 From: Michael Simacek Date: Tue, 20 Oct 2026 11:41:55 +0200 Subject: [PATCH 2/2] Use better hash spread function in json memo map --- .../graal/python/builtins/modules/json/PJSONScanner.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/json/PJSONScanner.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/json/PJSONScanner.java index d859087ca0..f0ac77d2f3 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/json/PJSONScanner.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/json/PJSONScanner.java @@ -96,6 +96,12 @@ TruffleString intern(TruffleString key, int hash, TruffleString.EqualNode equalN } private static int spread(int hash) { + // MurmurHash3 finalizer: mix all bits before masking so similar property names do not + // cluster in adjacent slots during linear probing. + hash ^= hash >>> 16; + hash *= 0x85ebca6b; + hash ^= hash >>> 13; + hash *= 0xc2b2ae35; return hash ^ (hash >>> 16); }