From 886e7498b3bafcf4cd1031a949638a40c20cab67 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 10 Apr 2026 19:11:55 +0200 Subject: [PATCH 1/9] perf: cache PerlRuntime.current() in local variables (Tier 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cache the ThreadLocal lookup result at method entry instead of calling PerlRuntime.current() multiple times per method. This eliminates redundant ThreadLocal lookups in hot paths: - GlobalVariable.getGlobalCodeRef(): 4 lookups → 1 - GlobalVariable.getGlobalVariable/Array/Hash(): 2 lookups → 1 - GlobalVariable.definedGlob(): 7 lookups → 1 - GlobalVariable.isPackageLoaded(): 3 lookups → 1 - InheritanceResolver.findMethodInHierarchy(): ~8 lookups → 1 - InheritanceResolver.linearizeHierarchy(): ~5 lookups → 1 - InheritanceResolver.invalidateCache(): 4 lookups → 1 Also optimized several other GlobalVariable accessors: defineGlobalCodeRef, replacePinnedCodeRef, aliasGlobalVariable, setGlobAlias, getGlobalIO, getGlobalFormatRef, definedGlobalFormatAsScalar, resetGlobalVariables, resolveStashAlias. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../org/perlonjava/core/Configuration.java | 4 +- .../runtime/mro/InheritanceResolver.java | 46 +++++---- .../runtime/runtimetypes/GlobalVariable.java | 98 +++++++++++-------- 3 files changed, 88 insertions(+), 60 deletions(-) diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 4f178dd6f..194980985 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "c30eeb487"; + public static final String gitCommitId = "67dce07ab"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). @@ -48,7 +48,7 @@ public final class Configuration { * Parsed by App::perlbrew and other tools via: perl -V | grep "Compiled at" * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String buildTimestamp = "Apr 10 2026 15:20:15"; + public static final String buildTimestamp = "Apr 10 2026 19:10:05"; // Prevent instantiation private Configuration() { diff --git a/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java b/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java index 62ac1dcfc..2c7f5c267 100644 --- a/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java +++ b/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java @@ -93,12 +93,13 @@ public static MROAlgorithm getPackageMRO(String packageName) { * @return A list of class names in the order of method resolution. */ public static List linearizeHierarchy(String className) { + PerlRuntime rt = PerlRuntime.current(); // Check if ISA has changed and invalidate cache if needed - if (hasIsaChanged(className)) { - invalidateCacheForClass(className); + if (hasIsaChanged(className, rt)) { + invalidateCacheForClass(className, rt); } - Map> cache = getLinearizedClassesCache(); + Map> cache = rt.linearizedClassesCache; // Check cache first List cached = cache.get(className); if (cached != null) { @@ -106,7 +107,7 @@ public static List linearizeHierarchy(String className) { return new ArrayList<>(cached); } - MROAlgorithm mro = getPackageMRO(className); + MROAlgorithm mro = rt.packageMRO.getOrDefault(className, rt.currentMRO); List result; switch (mro) { @@ -129,6 +130,10 @@ public static List linearizeHierarchy(String className) { * Checks if the @ISA array for a class has changed since last cached. */ private static boolean hasIsaChanged(String className) { + return hasIsaChanged(className, PerlRuntime.current()); + } + + private static boolean hasIsaChanged(String className, PerlRuntime rt) { RuntimeArray isaArray = GlobalVariable.getGlobalArray(className + "::ISA"); // Build current ISA list @@ -140,7 +145,7 @@ private static boolean hasIsaChanged(String className) { } } - Map> isCache = getIsaStateCache(); + Map> isCache = rt.isaStateCache; List cachedIsa = isCache.get(className); // If ISA changed, update cache and return true @@ -156,8 +161,12 @@ private static boolean hasIsaChanged(String className) { * Invalidate cache for a specific class and its dependents. */ private static void invalidateCacheForClass(String className) { - Map> linCache = getLinearizedClassesCache(); - Map mCache = getMethodCache(); + invalidateCacheForClass(className, PerlRuntime.current()); + } + + private static void invalidateCacheForClass(String className, PerlRuntime rt) { + Map> linCache = rt.linearizedClassesCache; + Map mCache = rt.methodCache; // Remove exact class and subclasses from linearization cache linCache.remove(className); @@ -175,10 +184,11 @@ private static void invalidateCacheForClass(String className) { * This should be called whenever the class hierarchy or method definitions change. */ public static void invalidateCache() { - getMethodCache().clear(); - getLinearizedClassesCache().clear(); - getOverloadContextCache().clear(); - getIsaStateCache().clear(); + PerlRuntime rt = PerlRuntime.current(); + rt.methodCache.clear(); + rt.linearizedClassesCache.clear(); + rt.overloadContextCache.clear(); + rt.isaStateCache.clear(); // Also clear the inline method cache in RuntimeCode RuntimeCode.clearInlineMethodCache(); } @@ -302,6 +312,8 @@ private static void populateIsaMapHelper(String className, * @return RuntimeScalar representing the found method, or null if not found */ public static RuntimeScalar findMethodInHierarchy(String methodName, String perlClassName, String cacheKey, int startFromIndex) { + PerlRuntime rt = PerlRuntime.current(); + if (TRACE_METHOD_RESOLUTION) { System.err.println("TRACE InheritanceResolver.findMethodInHierarchy:"); System.err.println(" methodName: '" + methodName + "'"); @@ -321,12 +333,12 @@ public static RuntimeScalar findMethodInHierarchy(String methodName, String perl } // Check if ISA changed for this class - if so, invalidate relevant caches - if (hasIsaChanged(perlClassName)) { - invalidateCacheForClass(perlClassName); + if (hasIsaChanged(perlClassName, rt)) { + invalidateCacheForClass(perlClassName, rt); } // Check the method cache - handles both found and not-found cases - Map mCache = getMethodCache(); + Map mCache = rt.methodCache; if (mCache.containsKey(cacheKey)) { if (TRACE_METHOD_RESOLUTION) { System.err.println(" Found in cache: " + (mCache.get(cacheKey) != null ? "YES" : "NULL")); @@ -362,7 +374,7 @@ public static RuntimeScalar findMethodInHierarchy(String methodName, String perl if (!codeRef.getDefinedBoolean()) { continue; } - cacheMethod(cacheKey, codeRef); + mCache.put(cacheKey, codeRef); if (TRACE_METHOD_RESOLUTION) { System.err.println(" FOUND method!"); System.err.flush(); @@ -374,7 +386,7 @@ public static RuntimeScalar findMethodInHierarchy(String methodName, String perl // Second pass — method not found anywhere, check AUTOLOAD in class hierarchy. // This matches Perl semantics: AUTOLOAD is only tried after the full MRO // search (including UNIVERSAL) fails to find the method. - if (isAutoloadEnabled() && !methodName.startsWith("(")) { + if (rt.autoloadEnabled && !methodName.startsWith("(")) { for (int i = startFromIndex; i < linearizedClasses.size(); i++) { String className = linearizedClasses.get(i); String effectiveClassName = GlobalVariable.resolveStashAlias(className); @@ -393,7 +405,7 @@ public static RuntimeScalar findMethodInHierarchy(String methodName, String perl } else { autoloadCode.autoloadVariableName = autoloadName; } - cacheMethod(cacheKey, autoload); + mCache.put(cacheKey, autoload); return autoload; } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java index 836766d4c..dc69a365e 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java @@ -190,8 +190,9 @@ public static void clearStashAlias(String namespace) { } public static String resolveStashAlias(String namespace) { + PerlRuntime rt = PerlRuntime.current(); String key = namespace.endsWith("::") ? namespace : namespace + "::"; - String aliased = PerlRuntime.current().stashAliases.get(key); + String aliased = rt.stashAliases.get(key); if (aliased == null) { return namespace; } @@ -209,13 +210,14 @@ public static String resolveStashAlias(String namespace) { public static void setGlobAlias(String fromGlob, String toGlob) { // Find the canonical name for toGlob (in case it's already an alias) String canonical = resolveGlobAlias(toGlob); + PerlRuntime rt = PerlRuntime.current(); // Don't create self-loops if (!fromGlob.equals(canonical)) { - PerlRuntime.current().globAliases.put(fromGlob, canonical); + rt.globAliases.put(fromGlob, canonical); } // Also ensure toGlob points to the canonical name (unless it would create a self-loop) if (!toGlob.equals(canonical) && !toGlob.equals(fromGlob)) { - PerlRuntime.current().globAliases.put(toGlob, canonical); + rt.globAliases.put(toGlob, canonical); } } @@ -256,7 +258,8 @@ public static java.util.List getGlobAliasGroup(String globName) { * @return The RuntimeScalar representing the global variable. */ public static RuntimeScalar getGlobalVariable(String key) { - RuntimeScalar var = PerlRuntime.current().globalVariables.get(key); + PerlRuntime rt = PerlRuntime.current(); + RuntimeScalar var = rt.globalVariables.get(key); if (var == null) { // Need to initialize global variable Matcher matcher = regexVariablePattern.matcher(key); @@ -272,14 +275,15 @@ public static RuntimeScalar getGlobalVariable(String key) { // Normal "non-magic" global variable var = new RuntimeScalar(); } - PerlRuntime.current().globalVariables.put(key, var); + rt.globalVariables.put(key, var); } return var; } public static RuntimeScalar aliasGlobalVariable(String key, String to) { - RuntimeScalar var = PerlRuntime.current().globalVariables.get(to); - PerlRuntime.current().globalVariables.put(key, var); + PerlRuntime rt = PerlRuntime.current(); + RuntimeScalar var = rt.globalVariables.get(to); + rt.globalVariables.put(key, var); return var; } @@ -337,10 +341,11 @@ public static RuntimeScalar removeGlobalVariable(String key) { * @return The RuntimeArray representing the global array. */ public static RuntimeArray getGlobalArray(String key) { - RuntimeArray var = PerlRuntime.current().globalArrays.get(key); + PerlRuntime rt = PerlRuntime.current(); + RuntimeArray var = rt.globalArrays.get(key); if (var == null) { var = new RuntimeArray(); - PerlRuntime.current().globalArrays.put(key, var); + rt.globalArrays.put(key, var); } return var; } @@ -372,7 +377,8 @@ public static RuntimeArray removeGlobalArray(String key) { * @return The RuntimeHash representing the global hash. */ public static RuntimeHash getGlobalHash(String key) { - RuntimeHash var = PerlRuntime.current().globalHashes.get(key); + PerlRuntime rt = PerlRuntime.current(); + RuntimeHash var = rt.globalHashes.get(key); if (var == null) { // Check if this is a package stash (ends with ::) if (key.endsWith("::")) { @@ -380,7 +386,7 @@ public static RuntimeHash getGlobalHash(String key) { } else { var = new RuntimeHash(); } - PerlRuntime.current().globalHashes.put(key, var); + rt.globalHashes.put(key, var); } return var; } @@ -417,17 +423,18 @@ public static RuntimeScalar getGlobalCodeRef(String key) { if (key == null) { return new RuntimeScalar(); } + PerlRuntime rt = PerlRuntime.current(); // First check if we have a pinned reference that survives stash deletion - RuntimeScalar pinned = PerlRuntime.current().pinnedCodeRefs.get(key); + RuntimeScalar pinned = rt.pinnedCodeRefs.get(key); if (pinned != null) { // Return the pinned ref so compiled code keeps working, but do NOT - // re-add to PerlRuntime.current().globalCodeRefs. If it was deleted from the stash (e.g., by + // re-add to rt.globalCodeRefs. If it was deleted from the stash (e.g., by // namespace::clean), that deletion should be respected for method // resolution via can() and the inheritance hierarchy. return pinned; } - RuntimeScalar var = PerlRuntime.current().globalCodeRefs.get(key); + RuntimeScalar var = rt.globalCodeRefs.get(key); if (var == null) { var = new RuntimeScalar(); var.type = RuntimeScalarType.CODE; // value is null @@ -448,11 +455,11 @@ public static RuntimeScalar getGlobalCodeRef(String key) { // It will be set specifically for \&{string} patterns in createCodeReference var.value = runtimeCode; - PerlRuntime.current().globalCodeRefs.put(key, var); + rt.globalCodeRefs.put(key, var); } // Pin the RuntimeScalar so it survives stash deletion - PerlRuntime.current().pinnedCodeRefs.put(key, var); + rt.pinnedCodeRefs.put(key, var); return var; } @@ -468,9 +475,10 @@ public static RuntimeScalar getGlobalCodeRef(String key) { */ public static RuntimeScalar defineGlobalCodeRef(String key) { RuntimeScalar ref = getGlobalCodeRef(key); - // Ensure it's in PerlRuntime.current().globalCodeRefs so method resolution finds it - if (!PerlRuntime.current().globalCodeRefs.containsKey(key)) { - PerlRuntime.current().globalCodeRefs.put(key, ref); + PerlRuntime rt = PerlRuntime.current(); + // Ensure it's in rt.globalCodeRefs so method resolution finds it + if (!rt.globalCodeRefs.containsKey(key)) { + rt.globalCodeRefs.put(key, ref); } return ref; } @@ -494,8 +502,9 @@ public static boolean existsGlobalCodeRef(String key) { * @param codeRef The new RuntimeScalar to pin (typically a new empty one). */ static void replacePinnedCodeRef(String key, RuntimeScalar codeRef) { - if (PerlRuntime.current().pinnedCodeRefs.containsKey(key)) { - PerlRuntime.current().pinnedCodeRefs.put(key, codeRef); + Map pinned = PerlRuntime.current().pinnedCodeRefs; + if (pinned.containsKey(key)) { + pinned.put(key, codeRef); } } @@ -641,8 +650,9 @@ public static void clearPackageCache() { * @return true if any methods exist in the class namespace */ public static boolean isPackageLoaded(String className) { + PerlRuntime rt = PerlRuntime.current(); // Check cache first - Boolean cached = PerlRuntime.current().packageExistsCache.get(className); + Boolean cached = rt.packageExistsCache.get(className); if (cached != null) { return cached; } @@ -654,11 +664,11 @@ public static boolean isPackageLoaded(String className) { // A key like "Foo::Bar::baz" belongs to package "Foo::Bar", not "Foo". // After stripping the prefix, the remaining part must NOT contain "::" // to be a direct member of this package. - boolean exists = PerlRuntime.current().globalCodeRefs.keySet().stream() + boolean exists = rt.globalCodeRefs.keySet().stream() .anyMatch(key -> key.startsWith(prefix) && !key.substring(prefix.length()).contains("::")); // Cache the result - PerlRuntime.current().packageExistsCache.put(className, exists); + rt.packageExistsCache.put(className, exists); return exists; } @@ -703,10 +713,11 @@ public static String resolveStashHashRedirect(String fullName) { */ public static RuntimeGlob getGlobalIO(String key) { String resolvedKey = resolveStashHashRedirect(key); - RuntimeGlob glob = PerlRuntime.current().globalIORefs.get(resolvedKey); + PerlRuntime rt = PerlRuntime.current(); + RuntimeGlob glob = rt.globalIORefs.get(resolvedKey); if (glob == null) { glob = new RuntimeGlob(resolvedKey); - PerlRuntime.current().globalIORefs.put(resolvedKey, glob); + rt.globalIORefs.put(resolvedKey, glob); } return glob; } @@ -793,39 +804,41 @@ public static RuntimeScalar definedGlob(RuntimeScalar scalar, String packageName return RuntimeScalarCache.scalarTrue; } + PerlRuntime rt = PerlRuntime.current(); + // Check if glob was explicitly assigned - if (PerlRuntime.current().globalGlobs.getOrDefault(varName, false)) { + if (rt.globalGlobs.getOrDefault(varName, false)) { return RuntimeScalarCache.scalarTrue; } // Check scalar slot - slot existence makes glob defined (not value definedness) // In Perl, `defined *FOO` is true if $FOO exists, even if $FOO is undef - if (PerlRuntime.current().globalVariables.containsKey(varName)) { + if (rt.globalVariables.containsKey(varName)) { return RuntimeScalarCache.scalarTrue; } // Check array slot - exists = defined (even if empty) - if (PerlRuntime.current().globalArrays.containsKey(varName)) { + if (rt.globalArrays.containsKey(varName)) { return RuntimeScalarCache.scalarTrue; } // Check hash slot - exists = defined (even if empty) - if (PerlRuntime.current().globalHashes.containsKey(varName)) { + if (rt.globalHashes.containsKey(varName)) { return RuntimeScalarCache.scalarTrue; } // Check code slot - slot existence makes glob defined - if (PerlRuntime.current().globalCodeRefs.containsKey(varName)) { + if (rt.globalCodeRefs.containsKey(varName)) { return RuntimeScalarCache.scalarTrue; } - // Check IO slot (via PerlRuntime.current().globalIORefs) - if (PerlRuntime.current().globalIORefs.containsKey(varName)) { + // Check IO slot (via rt.globalIORefs) + if (rt.globalIORefs.containsKey(varName)) { return RuntimeScalarCache.scalarTrue; } // Check format slot - if (PerlRuntime.current().globalFormatRefs.containsKey(varName)) { + if (rt.globalFormatRefs.containsKey(varName)) { return RuntimeScalarCache.scalarTrue; } @@ -839,10 +852,11 @@ public static RuntimeScalar definedGlob(RuntimeScalar scalar, String packageName * @return The RuntimeFormat representing the global format reference. */ public static RuntimeFormat getGlobalFormatRef(String key) { - RuntimeFormat format = PerlRuntime.current().globalFormatRefs.get(key); + PerlRuntime rt = PerlRuntime.current(); + RuntimeFormat format = rt.globalFormatRefs.get(key); if (format == null) { format = new RuntimeFormat(key); - PerlRuntime.current().globalFormatRefs.put(key, format); + rt.globalFormatRefs.put(key, format); } return format; } @@ -888,8 +902,9 @@ public static boolean isGlobalFormatDefined(String key) { } public static RuntimeScalar definedGlobalFormatAsScalar(String key) { - return PerlRuntime.current().globalFormatRefs.containsKey(key) ? - (PerlRuntime.current().globalFormatRefs.get(key).isFormatDefined() ? scalarTrue : scalarFalse) : scalarFalse; + PerlRuntime rt = PerlRuntime.current(); + return rt.globalFormatRefs.containsKey(key) ? + (rt.globalFormatRefs.get(key).isFormatDefined() ? scalarTrue : scalarFalse) : scalarFalse; } public static RuntimeScalar definedGlobalFormatAsScalar(RuntimeScalar key) { @@ -903,8 +918,9 @@ public static RuntimeScalar definedGlobalFormatAsScalar(RuntimeScalar key) { * @param currentPackage The current package name with "::" suffix */ public static void resetGlobalVariables(Set resetChars, String currentPackage) { + PerlRuntime rt = PerlRuntime.current(); // Reset scalar variables - for (Map.Entry entry : PerlRuntime.current().globalVariables.entrySet()) { + for (Map.Entry entry : rt.globalVariables.entrySet()) { String key = entry.getKey(); if (key.startsWith(currentPackage) && shouldResetVariable(key, currentPackage, resetChars)) { @@ -914,7 +930,7 @@ public static void resetGlobalVariables(Set resetChars, String curren } // Reset array variables - for (Map.Entry entry : PerlRuntime.current().globalArrays.entrySet()) { + for (Map.Entry entry : rt.globalArrays.entrySet()) { String key = entry.getKey(); if (key.startsWith(currentPackage) && shouldResetVariable(key, currentPackage, resetChars)) { @@ -924,7 +940,7 @@ public static void resetGlobalVariables(Set resetChars, String curren } // Reset hash variables - for (Map.Entry entry : PerlRuntime.current().globalHashes.entrySet()) { + for (Map.Entry entry : rt.globalHashes.entrySet()) { String key = entry.getKey(); if (key.startsWith(currentPackage) && shouldResetVariable(key, currentPackage, resetChars)) { From d070812cd4bfdad8baf28da1e4586a015fa1dbc4 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 10 Apr 2026 19:19:43 +0200 Subject: [PATCH 2/9] perf: migrate ThreadLocal stacks to PerlRuntime instance fields (Tier 2) Consolidate 11 separate ThreadLocal stacks from WarningBitsRegistry, HintHashRegistry, and RuntimeCode into PerlRuntime instance fields. This reduces ThreadLocal lookups per subroutine call from ~14-17 (one per ThreadLocal.get()) to 1 (PerlRuntime.current(), then direct field access). Migrated ThreadLocals: - WarningBitsRegistry: currentBitsStack, callSiteBits, callerBitsStack, callSiteHints, callerHintsStack, callSiteHintHash, callerHintHashStack - HintHashRegistry: callSiteSnapshotId, callerSnapshotIdStack - RuntimeCode: evalRuntimeContext, argsStack The shared static ConcurrentHashMaps (WarningBitsRegistry.registry, HintHashRegistry.snapshotRegistry) remain static as they are shared across runtimes and only written at compile time. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../org/perlonjava/core/Configuration.java | 4 +- .../perlonjava/runtime/HintHashRegistry.java | 38 +++---- .../runtime/WarningBitsRegistry.java | 104 ++++++------------ .../runtime/runtimetypes/PerlRuntime.java | 39 +++++++ .../runtime/runtimetypes/RuntimeCode.java | 30 ++--- 5 files changed, 108 insertions(+), 107 deletions(-) diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 194980985..dd5b5950e 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "67dce07ab"; + public static final String gitCommitId = "886e7498b"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). @@ -48,7 +48,7 @@ public final class Configuration { * Parsed by App::perlbrew and other tools via: perl -V | grep "Compiled at" * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String buildTimestamp = "Apr 10 2026 19:10:05"; + public static final String buildTimestamp = "Apr 10 2026 19:18:33"; // Prevent instantiation private Configuration() { diff --git a/src/main/java/org/perlonjava/runtime/HintHashRegistry.java b/src/main/java/org/perlonjava/runtime/HintHashRegistry.java index df21571dc..8d5106b1f 100644 --- a/src/main/java/org/perlonjava/runtime/HintHashRegistry.java +++ b/src/main/java/org/perlonjava/runtime/HintHashRegistry.java @@ -2,6 +2,7 @@ import org.perlonjava.runtime.runtimetypes.GlobalContext; import org.perlonjava.runtime.runtimetypes.GlobalVariable; +import org.perlonjava.runtime.runtimetypes.PerlRuntime; import org.perlonjava.runtime.runtimetypes.RuntimeHash; import org.perlonjava.runtime.runtimetypes.RuntimeScalar; @@ -22,6 +23,9 @@ * 2. Per-call-site tracking using snapshot IDs: registerSnapshot() captures %^H * at compile time, and setCallSiteHintHashId()/pushCallerHintHash()/ * getCallerHintHashAtFrame() bridge compile-time state to runtime caller()[10]. + * + * Per-call-site stacks are per-PerlRuntime instance fields (accessed via + * PerlRuntime.current()) instead of separate ThreadLocals. */ public class HintHashRegistry { @@ -38,15 +42,6 @@ public class HintHashRegistry { new ConcurrentHashMap<>(); private static final AtomicInteger nextSnapshotId = new AtomicInteger(0); - // ThreadLocal tracking the current call site's snapshot ID. - // Updated at runtime from emitted bytecode. - private static final ThreadLocal callSiteSnapshotId = - ThreadLocal.withInitial(() -> 0); - - // ThreadLocal stack saving caller's snapshot ID across subroutine calls. - private static final ThreadLocal> callerSnapshotIdStack = - ThreadLocal.withInitial(ArrayDeque::new); - // ---- Compile-time %^H scoping ---- /** @@ -110,7 +105,7 @@ public static int snapshotCurrentHintHash() { * @param id the snapshot ID (0 = empty/no hints) */ public static void setCallSiteHintHashId(int id) { - callSiteSnapshotId.set(id); + PerlRuntime.current().hintCallSiteSnapshotId = id; } /** @@ -120,11 +115,11 @@ public static void setCallSiteHintHashId(int id) { * Called by RuntimeCode.apply() before entering a subroutine. */ public static void pushCallerHintHash() { - int currentId = callSiteSnapshotId.get(); - callerSnapshotIdStack.get().push(currentId); + PerlRuntime rt = PerlRuntime.current(); + rt.hintCallerSnapshotIdStack.push(rt.hintCallSiteSnapshotId); // Reset callsite for the callee - it should not inherit the caller's hints. // The callee's own CompilerFlagNodes will set the correct ID if needed. - callSiteSnapshotId.set(0); + rt.hintCallSiteSnapshotId = 0; } /** @@ -133,12 +128,10 @@ public static void pushCallerHintHash() { * Called by RuntimeCode.apply() after a subroutine returns. */ public static void popCallerHintHash() { - Deque stack = callerSnapshotIdStack.get(); + PerlRuntime rt = PerlRuntime.current(); + Deque stack = rt.hintCallerSnapshotIdStack; if (!stack.isEmpty()) { - int restoredId = stack.pop(); - // Restore the callsite ID so eval STRING and subsequent code - // see the correct hint hash, not one clobbered by the callee. - callSiteSnapshotId.set(restoredId); + rt.hintCallSiteSnapshotId = stack.pop(); } } @@ -150,7 +143,7 @@ public static void popCallerHintHash() { * @return The hint hash map, or null if not available */ public static Map getCallerHintHashAtFrame(int frame) { - Deque stack = callerSnapshotIdStack.get(); + Deque stack = PerlRuntime.current().hintCallerSnapshotIdStack; if (stack.isEmpty()) { return null; } @@ -172,7 +165,7 @@ public static Map getCallerHintHashAtFrame(int frame) { * @return the hint hash map, or null if empty/not set */ public static Map getCurrentCallSiteHintHash() { - int id = callSiteSnapshotId.get(); + int id = PerlRuntime.current().hintCallSiteSnapshotId; if (id == 0) return null; return snapshotRegistry.get(id); } @@ -185,7 +178,8 @@ public static void clear() { compileTimeStack.clear(); snapshotRegistry.clear(); nextSnapshotId.set(0); - callSiteSnapshotId.set(0); - callerSnapshotIdStack.get().clear(); + PerlRuntime rt = PerlRuntime.current(); + rt.hintCallSiteSnapshotId = 0; + rt.hintCallerSnapshotIdStack.clear(); } } diff --git a/src/main/java/org/perlonjava/runtime/WarningBitsRegistry.java b/src/main/java/org/perlonjava/runtime/WarningBitsRegistry.java index 7e6a4898f..02c259e19 100644 --- a/src/main/java/org/perlonjava/runtime/WarningBitsRegistry.java +++ b/src/main/java/org/perlonjava/runtime/WarningBitsRegistry.java @@ -1,11 +1,12 @@ package org.perlonjava.runtime; -import java.util.ArrayDeque; import java.util.Deque; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.perlonjava.runtime.runtimetypes.GlobalContext; import org.perlonjava.runtime.runtimetypes.GlobalVariable; +import org.perlonjava.runtime.runtimetypes.PerlRuntime; import org.perlonjava.runtime.runtimetypes.RuntimeHash; import org.perlonjava.runtime.runtimetypes.RuntimeScalar; @@ -20,54 +21,17 @@ * * At runtime, caller() looks up warning bits by class name. * - * Additionally, a ThreadLocal stack tracks the "current" warning bits - * for runtime code that needs to check FATAL warnings. + * Warning/hints stacks are per-PerlRuntime instance fields (accessed + * via PerlRuntime.current()) instead of separate ThreadLocals, reducing + * the number of ThreadLocal lookups per subroutine call. */ public class WarningBitsRegistry { // Map from fully-qualified class name to warning bits string + // This is shared across runtimes (immutable after registration) private static final ConcurrentHashMap registry = new ConcurrentHashMap<>(); - // ThreadLocal stack of warning bits for the current execution context - // This allows runtime code to find warning bits even at top-level (no subroutine frame) - private static final ThreadLocal> currentBitsStack = - ThreadLocal.withInitial(ArrayDeque::new); - - // ThreadLocal tracking the warning bits at the current call site. - // Updated at runtime when 'use warnings' / 'no warnings' pragmas are encountered. - // This provides per-statement warning bits (like Perl 5's per-COP bits). - private static final ThreadLocal callSiteBits = - ThreadLocal.withInitial(() -> null); - - // ThreadLocal stack saving caller's call-site bits across subroutine calls. - // Each apply() pushes the current callSiteBits before calling the subroutine, - // and pops it when the subroutine returns. This allows caller()[9] to return - // the correct per-call-site warning bits. - private static final ThreadLocal> callerBitsStack = - ThreadLocal.withInitial(ArrayDeque::new); - - // ThreadLocal tracking the compile-time $^H (hints) at the current call site. - // Updated at runtime when pragmas (use strict, etc.) are encountered. - // This provides per-statement hints for caller()[8]. - private static final ThreadLocal callSiteHints = - ThreadLocal.withInitial(() -> 0); - - // ThreadLocal stack saving caller's $^H hints across subroutine calls. - // Mirrors callerBitsStack but for $^H instead of warning bits. - private static final ThreadLocal> callerHintsStack = - ThreadLocal.withInitial(ArrayDeque::new); - - // ThreadLocal tracking the compile-time %^H (hints hash) at the current call site. - // Updated at runtime when pragmas modify %^H. - // This provides per-statement hints hash for caller()[10]. - private static final ThreadLocal> callSiteHintHash = - ThreadLocal.withInitial(java.util.HashMap::new); - - // ThreadLocal stack saving caller's %^H across subroutine calls. - private static final ThreadLocal>> callerHintHashStack = - ThreadLocal.withInitial(ArrayDeque::new); - /** * Registers the warning bits for a class. * Called at class load time (static initializer) for JVM backend, @@ -104,7 +68,7 @@ public static String get(String className) { */ public static void pushCurrent(String bits) { if (bits != null) { - currentBitsStack.get().push(bits); + PerlRuntime.current().warningCurrentBitsStack.push(bits); } } @@ -113,7 +77,7 @@ public static void pushCurrent(String bits) { * Called when exiting a subroutine or code block. */ public static void popCurrent() { - Deque stack = currentBitsStack.get(); + Deque stack = PerlRuntime.current().warningCurrentBitsStack; if (!stack.isEmpty()) { stack.pop(); } @@ -126,7 +90,7 @@ public static void popCurrent() { * @return The current warning bits string, or null if stack is empty */ public static String getCurrent() { - Deque stack = currentBitsStack.get(); + Deque stack = PerlRuntime.current().warningCurrentBitsStack; return stack.isEmpty() ? null : stack.peek(); } @@ -136,13 +100,14 @@ public static String getCurrent() { */ public static void clear() { registry.clear(); - currentBitsStack.get().clear(); - callSiteBits.remove(); - callerBitsStack.get().clear(); - callSiteHints.remove(); - callerHintsStack.get().clear(); - callSiteHintHash.get().clear(); - callerHintHashStack.get().clear(); + PerlRuntime rt = PerlRuntime.current(); + rt.warningCurrentBitsStack.clear(); + rt.warningCallSiteBits = null; + rt.warningCallerBitsStack.clear(); + rt.warningCallSiteHints = 0; + rt.warningCallerHintsStack.clear(); + rt.warningCallSiteHintHash.clear(); + rt.warningCallerHintHashStack.clear(); } /** @@ -153,7 +118,7 @@ public static void clear() { * @param bits The warning bits string for the current call site */ public static void setCallSiteBits(String bits) { - callSiteBits.set(bits); + PerlRuntime.current().warningCallSiteBits = bits; } /** @@ -162,7 +127,7 @@ public static void setCallSiteBits(String bits) { * @return The current call-site warning bits, or null if not set */ public static String getCallSiteBits() { - return callSiteBits.get(); + return PerlRuntime.current().warningCallSiteBits; } /** @@ -171,8 +136,9 @@ public static String getCallSiteBits() { * This preserves the caller's warning bits so caller()[9] can retrieve them. */ public static void pushCallerBits() { - String bits = callSiteBits.get(); - callerBitsStack.get().push(bits != null ? bits : ""); + PerlRuntime rt = PerlRuntime.current(); + String bits = rt.warningCallSiteBits; + rt.warningCallerBitsStack.push(bits != null ? bits : ""); } /** @@ -180,7 +146,7 @@ public static void pushCallerBits() { * Called by RuntimeCode.apply() after a subroutine returns. */ public static void popCallerBits() { - Deque stack = callerBitsStack.get(); + Deque stack = PerlRuntime.current().warningCallerBitsStack; if (!stack.isEmpty()) { stack.pop(); } @@ -195,7 +161,7 @@ public static void popCallerBits() { * @return The warning bits string, or null if not available */ public static String getCallerBitsAtFrame(int frame) { - Deque stack = callerBitsStack.get(); + Deque stack = PerlRuntime.current().warningCallerBitsStack; if (stack.isEmpty()) { return null; } @@ -214,7 +180,7 @@ public static String getCallerBitsAtFrame(int frame) { * Returns the number of registered classes. * Useful for debugging and testing. * - * @return The number of registered class → bits mappings + * @return The number of registered class -> bits mappings */ public static int size() { return registry.size(); @@ -229,7 +195,7 @@ public static int size() { * @param hints The $^H bitmask */ public static void setCallSiteHints(int hints) { - callSiteHints.set(hints); + PerlRuntime.current().warningCallSiteHints = hints; } /** @@ -238,7 +204,7 @@ public static void setCallSiteHints(int hints) { * @return The current call-site $^H value */ public static int getCallSiteHints() { - return callSiteHints.get(); + return PerlRuntime.current().warningCallSiteHints; } /** @@ -246,7 +212,8 @@ public static int getCallSiteHints() { * Called by RuntimeCode.apply() before entering a subroutine. */ public static void pushCallerHints() { - callerHintsStack.get().push(callSiteHints.get()); + PerlRuntime rt = PerlRuntime.current(); + rt.warningCallerHintsStack.push(rt.warningCallSiteHints); } /** @@ -254,7 +221,7 @@ public static void pushCallerHints() { * Called by RuntimeCode.apply() after a subroutine returns. */ public static void popCallerHints() { - Deque stack = callerHintsStack.get(); + Deque stack = PerlRuntime.current().warningCallerHintsStack; if (!stack.isEmpty()) { stack.pop(); } @@ -269,7 +236,7 @@ public static void popCallerHints() { * @return The $^H value, or -1 if not available */ public static int getCallerHintsAtFrame(int frame) { - Deque stack = callerHintsStack.get(); + Deque stack = PerlRuntime.current().warningCallerHintsStack; if (stack.isEmpty()) { return -1; } @@ -292,7 +259,7 @@ public static int getCallerHintsAtFrame(int frame) { * @param hintHash A snapshot of the %^H hash elements */ public static void setCallSiteHintHash(java.util.Map hintHash) { - callSiteHintHash.set(hintHash != null ? new java.util.HashMap<>(hintHash) : new java.util.HashMap<>()); + PerlRuntime.current().warningCallSiteHintHash = hintHash != null ? new java.util.HashMap<>(hintHash) : new java.util.HashMap<>(); } /** @@ -309,7 +276,8 @@ public static void snapshotCurrentHintHash() { * Called by RuntimeCode.apply() before entering a subroutine. */ public static void pushCallerHintHash() { - callerHintHashStack.get().push(new java.util.HashMap<>(callSiteHintHash.get())); + PerlRuntime rt = PerlRuntime.current(); + rt.warningCallerHintHashStack.push(new java.util.HashMap<>(rt.warningCallSiteHintHash)); } /** @@ -317,7 +285,7 @@ public static void pushCallerHintHash() { * Called by RuntimeCode.apply() after a subroutine returns. */ public static void popCallerHintHash() { - Deque> stack = callerHintHashStack.get(); + Deque> stack = PerlRuntime.current().warningCallerHintHashStack; if (!stack.isEmpty()) { stack.pop(); } @@ -332,7 +300,7 @@ public static void popCallerHintHash() { * @return A copy of the %^H hash elements, or null if not available */ public static java.util.Map getCallerHintHashAtFrame(int frame) { - Deque> stack = callerHintHashStack.get(); + Deque> stack = PerlRuntime.current().warningCallerHintHashStack; if (stack.isEmpty()) { return null; } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java b/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java index 39f7cff24..82cbb5302 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java @@ -395,6 +395,45 @@ protected boolean removeEldestEntry(Map.Entry, java.lang.invoke.MethodH public final int[] inlineCacheMethodHash = new int[METHOD_CALL_CACHE_SIZE]; public final RuntimeCode[] inlineCacheCode = new RuntimeCode[METHOD_CALL_CACHE_SIZE]; + // ---- Warning/Hints stacks — migrated from WarningBitsRegistry ThreadLocals ---- + + /** Stack of warning bits for the current execution context. */ + public final Deque warningCurrentBitsStack = new ArrayDeque<>(); + + /** Warning bits at the current call site. */ + public String warningCallSiteBits = null; + + /** Stack saving caller's call-site warning bits across subroutine calls. */ + public final Deque warningCallerBitsStack = new ArrayDeque<>(); + + /** Compile-time $^H (hints) at the current call site. */ + public int warningCallSiteHints = 0; + + /** Stack saving caller's $^H hints across subroutine calls. */ + public final Deque warningCallerHintsStack = new ArrayDeque<>(); + + /** Compile-time %^H (hints hash) snapshot at the current call site. */ + public Map warningCallSiteHintHash = new HashMap<>(); + + /** Stack saving caller's %^H across subroutine calls. */ + public final Deque> warningCallerHintHashStack = new ArrayDeque<>(); + + // ---- HintHashRegistry stacks — migrated from HintHashRegistry ThreadLocals ---- + + /** Current call site's hint hash snapshot ID. */ + public int hintCallSiteSnapshotId = 0; + + /** Stack saving caller's hint hash snapshot ID across subroutine calls. */ + public final Deque hintCallerSnapshotIdStack = new ArrayDeque<>(); + + // ---- RuntimeCode stacks — migrated from RuntimeCode ThreadLocals ---- + + /** Eval runtime context (used during eval STRING compilation). */ + public Object evalRuntimeContext = null; + + /** Stack of @_ argument arrays across subroutine calls. */ + public final Deque argsStack = new ArrayDeque<>(); + // ---- Static accessors ---- /** diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index fab9809d0..c1e6d989e 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -101,10 +101,10 @@ public static IdentityHashMap getEvalBeginIds() { * - runtimeValues: Object[] of captured variable values * - capturedEnv: String[] of captured variable names (matching array indices) *

- * Thread-safety: Each thread's eval compilation uses its own ThreadLocal storage, so parallel + * Thread-safety: Each thread's eval compilation uses its own PerlRuntime storage, so parallel * eval compilations don't interfere with each other. */ - private static final ThreadLocal evalRuntimeContext = new ThreadLocal<>(); + // evalRuntimeContext migrated to PerlRuntime; access via getEvalRuntimeContext() // evalCache migrated to PerlRuntime; access via getEvalCache() private static Map> getEvalCache() { return PerlRuntime.current().evalCache; @@ -150,9 +150,9 @@ public static void decrementEvalDepth() { * * Push/pop is handled by RuntimeCode.apply() methods. * Access via getCurrentArgs() for Java-implemented functions that need caller's @_. + * + * Migrated to PerlRuntime.argsStack for reduced ThreadLocal overhead. */ - private static final ThreadLocal> argsStack = - ThreadLocal.withInitial(ArrayDeque::new); /** * Get the current subroutine's @_ array. @@ -162,7 +162,7 @@ public static void decrementEvalDepth() { * @return The current @_ array, or null if not in a subroutine */ public static RuntimeArray getCurrentArgs() { - Deque stack = argsStack.get(); + Deque stack = PerlRuntime.current().argsStack; return stack.isEmpty() ? null : stack.peek(); } @@ -178,7 +178,7 @@ public static RuntimeArray getCurrentArgs() { * @return The caller's @_ array, or null if not available */ public static RuntimeArray getCallerArgs() { - Deque stack = argsStack.get(); + Deque stack = PerlRuntime.current().argsStack; if (stack.size() < 2) { return null; } @@ -192,7 +192,7 @@ public static RuntimeArray getCallerArgs() { * Public so BytecodeInterpreter can use it when calling InterpretedCode directly. */ public static void pushArgs(RuntimeArray args) { - argsStack.get().push(args); + PerlRuntime.current().argsStack.push(args); } /** @@ -200,7 +200,7 @@ public static void pushArgs(RuntimeArray args) { * Public so BytecodeInterpreter can use it when calling InterpretedCode directly. */ public static void popArgs() { - Deque stack = argsStack.get(); + Deque stack = PerlRuntime.current().argsStack; if (!stack.isEmpty()) { stack.pop(); } @@ -403,7 +403,7 @@ public static boolean hasAutoload(RuntimeCode code) { * @return The current eval runtime context, or null if not in eval STRING compilation */ public static EvalRuntimeContext getEvalRuntimeContext() { - return evalRuntimeContext.get(); + return (EvalRuntimeContext) PerlRuntime.current().evalRuntimeContext; } /** @@ -424,7 +424,7 @@ public static void clearCaches() { rt.anonSubs.clear(); rt.interpretedSubs.clear(); rt.evalContext.clear(); - evalRuntimeContext.remove(); + rt.evalRuntimeContext = null; } public static void copy(RuntimeCode code, RuntimeCode codeFrom) { @@ -517,7 +517,7 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje ctx.capturedEnv, // Variable names in same order as runtimeValues evalTag ); - evalRuntimeContext.set(runtimeCtx); + PerlRuntime.current().evalRuntimeContext = runtimeCtx; try { // Check if the eval string contains non-ASCII characters @@ -829,7 +829,7 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje // IMPORTANT: Always clean up ThreadLocal in finally block to ensure it's removed // even if compilation fails. Failure to do so could cause memory leaks in // long-running applications with thread pools. - evalRuntimeContext.remove(); + PerlRuntime.current().evalRuntimeContext = null; } } finally { @@ -1004,7 +1004,7 @@ public static RuntimeList evalStringWithInterpreter( ctx.capturedEnv, evalTag ); - evalRuntimeContext.set(runtimeCtx); + PerlRuntime.current().evalRuntimeContext = runtimeCtx; InterpretedCode interpretedCode = null; RuntimeList result; @@ -1345,8 +1345,8 @@ public static RuntimeList evalStringWithInterpreter( storeSourceLines(code.toString(), evalFilename, ast, tokens); } - // Clean up ThreadLocal - evalRuntimeContext.remove(); + // Clean up eval runtime context + PerlRuntime.current().evalRuntimeContext = null; // Release the compile lock if still held (error path — success path releases it earlier). // Use a boolean flag instead of isHeldByCurrentThread() to avoid over-decrementing From 4a3b07287f5815127f5e3990884d17115d8047a5 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 10 Apr 2026 19:28:24 +0200 Subject: [PATCH 3/9] perf: batch push/pop caller state to reduce ThreadLocal lookups Add pushCallerState/popCallerState and pushSubState/popSubState batch methods to PerlRuntime, replacing 8-12 separate PerlRuntime.current() calls per subroutine call with just 2. Closure: 569 -> 601 ops/s (+5.6%) Method: 319 -> 336 ops/s (+5.3%) Lexical: 375K -> 458K ops/s (+22.2%) Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../org/perlonjava/core/Configuration.java | 4 +- .../runtime/runtimetypes/PerlRuntime.java | 77 +++++++++++++++++ .../runtime/runtimetypes/RuntimeCode.java | 83 +++++-------------- 3 files changed, 99 insertions(+), 65 deletions(-) diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index dd5b5950e..7d4a52533 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "886e7498b"; + public static final String gitCommitId = "d070812cd"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). @@ -48,7 +48,7 @@ public final class Configuration { * Parsed by App::perlbrew and other tools via: perl -V | grep "Compiled at" * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String buildTimestamp = "Apr 10 2026 19:18:33"; + public static final String buildTimestamp = "Apr 10 2026 19:25:49"; // Prevent instantiation private Configuration() { diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java b/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java index 82cbb5302..332f5a22e 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java @@ -495,6 +495,83 @@ public static PerlRuntime initialize() { return rt; } + // ---- Batch push/pop methods for subroutine call hot path ---- + + /** + * Pushes all caller state in one shot for the static apply() dispatch path. + * Replaces 4 separate PerlRuntime.current() lookups with 1. + * Called from RuntimeCode's static apply() methods. + * + * @param warningBits warning bits for the code being called, or null + */ + public void pushCallerState(String warningBits) { + if (warningBits != null) { + warningCurrentBitsStack.push(warningBits); + } + // Save caller's call-site warning bits (for caller()[9]) + warningCallerBitsStack.push(warningCallSiteBits != null ? warningCallSiteBits : ""); + // Save caller's $^H (for caller()[8]) + warningCallerHintsStack.push(warningCallSiteHints); + // Save caller's hint hash snapshot ID and reset for callee + hintCallerSnapshotIdStack.push(hintCallSiteSnapshotId); + hintCallSiteSnapshotId = 0; + } + + /** + * Pops all caller state in one shot for the static apply() dispatch path. + * Replaces 4 separate PerlRuntime.current() lookups with 1. + * + * @param hadWarningBits true if warningBits was non-null on the matching push + */ + public void popCallerState(boolean hadWarningBits) { + // Restore hint hash snapshot ID + if (!hintCallerSnapshotIdStack.isEmpty()) { + hintCallSiteSnapshotId = hintCallerSnapshotIdStack.pop(); + } + // Restore caller hints + if (!warningCallerHintsStack.isEmpty()) { + warningCallerHintsStack.pop(); + } + // Restore caller bits + if (!warningCallerBitsStack.isEmpty()) { + warningCallerBitsStack.pop(); + } + // Restore warning bits + if (hadWarningBits && !warningCurrentBitsStack.isEmpty()) { + warningCurrentBitsStack.pop(); + } + } + + /** + * Pushes subroutine entry state (args + warning bits) in one shot. + * Replaces 2 separate PerlRuntime.current() lookups with 1. + * Called from RuntimeCode's instance apply() methods. + * + * @param args the @_ arguments array + * @param warningBits warning bits for the code being called, or null + */ + public void pushSubState(RuntimeArray args, String warningBits) { + argsStack.push(args); + if (warningBits != null) { + warningCurrentBitsStack.push(warningBits); + } + } + + /** + * Pops subroutine exit state (args + warning bits) in one shot. + * Replaces 2 separate PerlRuntime.current() lookups with 1. + * + * @param hadWarningBits true if warningBits was non-null on the matching push + */ + public void popSubState(boolean hadWarningBits) { + if (hadWarningBits && !warningCurrentBitsStack.isEmpty()) { + warningCurrentBitsStack.pop(); + } + if (!argsStack.isEmpty()) { + argsStack.pop(); + } + } + /** * Creates a new independent PerlRuntime (not bound to any thread). * Call {@link #setCurrent(PerlRuntime)} to bind it to a thread before use. diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index c1e6d989e..7c6fd569a 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -2097,15 +2097,9 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, RuntimeArray a, int // Look up warning bits for the code's class and push to context stack // This enables FATAL warnings to work even at top-level (no caller frame) String warningBits = getWarningBitsForCode(code); - if (warningBits != null) { - WarningBitsRegistry.pushCurrent(warningBits); - } - // Save caller's call-site warning bits so caller()[9] can retrieve them - WarningBitsRegistry.pushCallerBits(); - // Save caller's $^H so caller()[8] can retrieve them - WarningBitsRegistry.pushCallerHints(); - // Save caller's call-site hint hash so caller()[10] can retrieve them - HintHashRegistry.pushCallerHintHash(); + // Batch push: caller bits, hints, hint hash, and warning bits in one PerlRuntime.current() call + PerlRuntime rt = PerlRuntime.current(); + rt.pushCallerState(warningBits); try { // Cast the value to RuntimeCode and call apply() RuntimeList result = code.apply(a, callContext); @@ -2126,12 +2120,7 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, RuntimeArray a, int // Consume at normal subroutine boundary return e.returnValue != null ? e.returnValue.getList() : new RuntimeList(); } finally { - HintHashRegistry.popCallerHintHash(); - WarningBitsRegistry.popCallerHints(); - WarningBitsRegistry.popCallerBits(); - if (warningBits != null) { - WarningBitsRegistry.popCurrent(); - } + rt.popCallerState(warningBits != null); } } @@ -2330,15 +2319,9 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa if (code.defined()) { // Look up warning bits for the code's class and push to context stack String warningBits = getWarningBitsForCode(code); - if (warningBits != null) { - WarningBitsRegistry.pushCurrent(warningBits); - } - // Save caller's call-site warning bits so caller()[9] can retrieve them - WarningBitsRegistry.pushCallerBits(); - // Save caller's $^H so caller()[8] can retrieve them - WarningBitsRegistry.pushCallerHints(); - // Save caller's call-site hint hash so caller()[10] can retrieve them - HintHashRegistry.pushCallerHintHash(); + // Batch push: caller bits, hints, hint hash, and warning bits in one PerlRuntime.current() call + PerlRuntime rt = PerlRuntime.current(); + rt.pushCallerState(warningBits); try { // Cast the value to RuntimeCode and call apply() return code.apply(subroutineName, a, callContext); @@ -2350,12 +2333,7 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa // Consume at normal subroutine boundary return e.returnValue != null ? e.returnValue.getList() : new RuntimeList(); } finally { - HintHashRegistry.popCallerHintHash(); - WarningBitsRegistry.popCallerHints(); - WarningBitsRegistry.popCallerBits(); - if (warningBits != null) { - WarningBitsRegistry.popCurrent(); - } + rt.popCallerState(warningBits != null); } } @@ -2496,15 +2474,9 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa if (code.defined()) { // Look up warning bits for the code's class and push to context stack String warningBits = getWarningBitsForCode(code); - if (warningBits != null) { - WarningBitsRegistry.pushCurrent(warningBits); - } - // Save caller's call-site warning bits so caller()[9] can retrieve them - WarningBitsRegistry.pushCallerBits(); - // Save caller's $^H so caller()[8] can retrieve them - WarningBitsRegistry.pushCallerHints(); - // Save caller's call-site hint hash so caller()[10] can retrieve them - HintHashRegistry.pushCallerHintHash(); + // Batch push: caller bits, hints, hint hash, and warning bits in one PerlRuntime.current() call + PerlRuntime rt = PerlRuntime.current(); + rt.pushCallerState(warningBits); try { // Cast the value to RuntimeCode and call apply() return code.apply(subroutineName, a, callContext); @@ -2516,12 +2488,7 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa // Consume at normal subroutine boundary return e.returnValue != null ? e.returnValue.getList() : new RuntimeList(); } finally { - HintHashRegistry.popCallerHintHash(); - WarningBitsRegistry.popCallerHints(); - WarningBitsRegistry.popCallerBits(); - if (warningBits != null) { - WarningBitsRegistry.popCurrent(); - } + rt.popCallerState(warningBits != null); } } @@ -2904,13 +2871,11 @@ public RuntimeList apply(RuntimeArray a, int callContext) { DebugHooks.enterSubroutine(debugSubName); } // Always push args for getCurrentArgs() support (used by List::Util::any/all/etc.) - pushArgs(a); - // Push warning bits for FATAL warnings support + // Batch push: args + warning bits in one PerlRuntime.current() call String warningBits = getWarningBitsForCode(this); - if (warningBits != null) { - WarningBitsRegistry.pushCurrent(warningBits); - } + PerlRuntime rt = PerlRuntime.current(); + rt.pushSubState(a, warningBits); try { RuntimeList result; // Prefer functional interface over MethodHandle for better performance @@ -2923,10 +2888,7 @@ public RuntimeList apply(RuntimeArray a, int callContext) { } return result; } finally { - if (warningBits != null) { - WarningBitsRegistry.popCurrent(); - } - popArgs(); + rt.popSubState(warningBits != null); if (DebugState.debugMode) { DebugHooks.exitSubroutine(); DebugState.popArgs(); @@ -3001,13 +2963,11 @@ public RuntimeList apply(String subroutineName, RuntimeArray a, int callContext) DebugHooks.enterSubroutine(debugSubName); } // Always push args for getCurrentArgs() support (used by List::Util::any/all/etc.) - pushArgs(a); - // Push warning bits for FATAL warnings support + // Batch push: args + warning bits in one PerlRuntime.current() call String warningBits = getWarningBitsForCode(this); - if (warningBits != null) { - WarningBitsRegistry.pushCurrent(warningBits); - } + PerlRuntime rt = PerlRuntime.current(); + rt.pushSubState(a, warningBits); try { RuntimeList result; // Prefer functional interface over MethodHandle for better performance @@ -3020,10 +2980,7 @@ public RuntimeList apply(String subroutineName, RuntimeArray a, int callContext) } return result; } finally { - if (warningBits != null) { - WarningBitsRegistry.popCurrent(); - } - popArgs(); + rt.popSubState(warningBits != null); if (DebugState.debugMode) { DebugHooks.exitSubroutine(); DebugState.popArgs(); From c33d7c828adb3ea6525201f27185d816b9d2b971 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 10 Apr 2026 19:42:33 +0200 Subject: [PATCH 4/9] perf: batch RegexState save/restore to single PerlRuntime.current() call RegexState.save() and restore() each called 13 individual RuntimeRegex static accessors, each doing its own PerlRuntime.current() ThreadLocal lookup. Replaced with a single PerlRuntime.current() call and direct field access in both constructor and dynamicRestoreState(). Eliminates 24 ThreadLocal lookups per subroutine call. JFR profiling showed RegexState was the dominant ThreadLocal overhead source (126 of 143 PerlRuntime.current() samples in closure benchmark). Closure: 601 -> 814 ops/s (+35%, now -5.7% vs master) Method: 336 -> 399 ops/s (+19%, now -8.5% vs master) Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../org/perlonjava/core/Configuration.java | 4 +- .../runtime/runtimetypes/RegexState.java | 57 ++++++++++--------- 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 7d4a52533..ae2dd7b9c 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "d070812cd"; + public static final String gitCommitId = "4a3b07287"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). @@ -48,7 +48,7 @@ public final class Configuration { * Parsed by App::perlbrew and other tools via: perl -V | grep "Compiled at" * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String buildTimestamp = "Apr 10 2026 19:25:49"; + public static final String buildTimestamp = "Apr 10 2026 19:40:14"; // Prevent instantiation private Configuration() { diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RegexState.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RegexState.java index 37311c604..fe3783b5f 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RegexState.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RegexState.java @@ -4,6 +4,7 @@ import java.util.regex.Matcher; + /** * Snapshot of regex-related global state (Perl's $1, $&, $`, $', etc.). * @@ -28,19 +29,21 @@ public class RegexState implements DynamicState { private final boolean lastMatchWasByteString; public RegexState() { - this.globalMatcher = RuntimeRegex.getGlobalMatcher(); - this.globalMatchString = RuntimeRegex.getGlobalMatchString(); - this.lastMatchedString = RuntimeRegex.getLastMatchedString(); - this.lastMatchStart = RuntimeRegex.getLastMatchStart(); - this.lastMatchEnd = RuntimeRegex.getLastMatchEnd(); - this.lastSuccessfulMatchedString = RuntimeRegex.getLastSuccessfulMatchedString(); - this.lastSuccessfulMatchStart = RuntimeRegex.getLastSuccessfulMatchStart(); - this.lastSuccessfulMatchEnd = RuntimeRegex.getLastSuccessfulMatchEnd(); - this.lastSuccessfulMatchString = RuntimeRegex.getLastSuccessfulMatchString(); - this.lastSuccessfulPattern = RuntimeRegex.getLastSuccessfulPattern(); - this.lastMatchUsedPFlag = RuntimeRegex.getLastMatchUsedPFlag(); - this.lastCaptureGroups = RuntimeRegex.getLastCaptureGroups(); - this.lastMatchWasByteString = RuntimeRegex.getLastMatchWasByteString(); + // Single PerlRuntime.current() lookup instead of 13 separate ones + PerlRuntime rt = PerlRuntime.current(); + this.globalMatcher = rt.regexGlobalMatcher; + this.globalMatchString = rt.regexGlobalMatchString; + this.lastMatchedString = rt.regexLastMatchedString; + this.lastMatchStart = rt.regexLastMatchStart; + this.lastMatchEnd = rt.regexLastMatchEnd; + this.lastSuccessfulMatchedString = rt.regexLastSuccessfulMatchedString; + this.lastSuccessfulMatchStart = rt.regexLastSuccessfulMatchStart; + this.lastSuccessfulMatchEnd = rt.regexLastSuccessfulMatchEnd; + this.lastSuccessfulMatchString = rt.regexLastSuccessfulMatchString; + this.lastSuccessfulPattern = rt.regexLastSuccessfulPattern; + this.lastMatchUsedPFlag = rt.regexLastMatchUsedPFlag; + this.lastCaptureGroups = rt.regexLastCaptureGroups; + this.lastMatchWasByteString = rt.regexLastMatchWasByteString; } public static void save() { @@ -57,18 +60,20 @@ public void restore() { @Override public void dynamicRestoreState() { - RuntimeRegex.setGlobalMatcher(this.globalMatcher); - RuntimeRegex.setGlobalMatchString(this.globalMatchString); - RuntimeRegex.setLastMatchedString(this.lastMatchedString); - RuntimeRegex.setLastMatchStart(this.lastMatchStart); - RuntimeRegex.setLastMatchEnd(this.lastMatchEnd); - RuntimeRegex.setLastSuccessfulMatchedString(this.lastSuccessfulMatchedString); - RuntimeRegex.setLastSuccessfulMatchStart(this.lastSuccessfulMatchStart); - RuntimeRegex.setLastSuccessfulMatchEnd(this.lastSuccessfulMatchEnd); - RuntimeRegex.setLastSuccessfulMatchString(this.lastSuccessfulMatchString); - RuntimeRegex.setLastSuccessfulPattern(this.lastSuccessfulPattern); - RuntimeRegex.setLastMatchUsedPFlag(this.lastMatchUsedPFlag); - RuntimeRegex.setLastCaptureGroups(this.lastCaptureGroups); - RuntimeRegex.setLastMatchWasByteString(this.lastMatchWasByteString); + // Single PerlRuntime.current() lookup instead of 13 separate ones + PerlRuntime rt = PerlRuntime.current(); + rt.regexGlobalMatcher = this.globalMatcher; + rt.regexGlobalMatchString = this.globalMatchString; + rt.regexLastMatchedString = this.lastMatchedString; + rt.regexLastMatchStart = this.lastMatchStart; + rt.regexLastMatchEnd = this.lastMatchEnd; + rt.regexLastSuccessfulMatchedString = this.lastSuccessfulMatchedString; + rt.regexLastSuccessfulMatchStart = this.lastSuccessfulMatchStart; + rt.regexLastSuccessfulMatchEnd = this.lastSuccessfulMatchEnd; + rt.regexLastSuccessfulMatchString = this.lastSuccessfulMatchString; + rt.regexLastSuccessfulPattern = this.lastSuccessfulPattern; + rt.regexLastMatchUsedPFlag = this.lastMatchUsedPFlag; + rt.regexLastCaptureGroups = this.lastCaptureGroups; + rt.regexLastMatchWasByteString = this.lastMatchWasByteString; } } From 97a4762964c33d39a5418fef176efd315ba112ea Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 10 Apr 2026 19:43:23 +0200 Subject: [PATCH 5/9] docs: add JFR profiling results and optimization summary to concurrency.md Document the profiling findings (RegexState was dominant overhead), optimization tiers applied, benchmark results, and remaining opportunities. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- dev/design/concurrency.md | 58 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/dev/design/concurrency.md b/dev/design/concurrency.md index 42c66c4ec..62339d11a 100644 --- a/dev/design/concurrency.md +++ b/dev/design/concurrency.md @@ -1485,6 +1485,64 @@ and why it was reverted.) *None yet.* +--- + +### Optimization Results (2026-04-10) + +**Branch:** `feature/multiplicity-opt` (created from `feature/multiplicity`) + +#### JFR Profiling Findings + +Profiled the closure benchmark (`dev/bench/benchmark_closure.pl`) with Java +Flight Recorder to identify the dominant overhead sources. Key findings: + +| Category | JFR Samples | Source | +|----------|-------------|--------| +| `PerlRuntime.current()` / ThreadLocal | 143 | The ThreadLocal.get() call itself | +| RuntimeRegex accessors | ~126 | **Dominant source** — 13 getters + 13 setters per sub call via RegexState save/restore | +| DynamicVariableManager | ~90 | `local` variable save/restore (includes regex state push) | +| pushCallerState (batch) | 10 | Caller bits/hints/hint-hash state | +| pushSubState (batch) | 3 | Args + warning bits | +| ArrayDeque.push | 13 | Stack operations | + +**Critical finding:** `RegexState` save/restore was calling 13 individual +`RuntimeRegex` static accessors (each doing its own `PerlRuntime.current()` +ThreadLocal lookup) on every subroutine entry AND exit — **26 ThreadLocal +lookups per sub call** — even when the subroutine never uses regex. + +#### Optimizations Applied + +| Tier | Optimization | Files Changed | ThreadLocal Lookups Eliminated | +|------|-------------|---------------|-------------------------------| +| 1 | Cache `PerlRuntime.current()` in local variables | GlobalVariable.java, InheritanceResolver.java | ~12 per method cache miss | +| 2 | Migrate WarningBits/HintHash/args stacks to PerlRuntime fields | WarningBitsRegistry.java, HintHashRegistry.java, RuntimeCode.java, PerlRuntime.java | 14-17 per sub call (separate ThreadLocals -> 1 ThreadLocal) | +| 2b | Batch pushCallerState/popCallerState and pushSubState/popSubState | PerlRuntime.java, RuntimeCode.java | 8-12 per sub call -> 2 | +| 2c | Batch RegexState save/restore | RegexState.java | **24 per sub call** (13 getters + 13 setters -> 2) | + +#### Benchmark Results + +| Benchmark | master | branch (pre-opt) | **After all opts** | vs master | vs pre-opt | +|-----------|--------|-------------------|--------------------|-----------|------------| +| **closure** | 863 | 569 (-34.1%) | **814** | **-5.7%** | +43.1% | +| **method** | 436 | 319 (-26.9%) | **399** | **-8.5%** | +25.1% | +| **lexical** | 394K | 375K (-4.9%) | **466K** | **+18.2%** | +24.3% | +| global | 78K | 74K (-5.4%) | **73K** | -6.4% | -0.4% | + +Closure and method are now within the 5-10% range of other benchmarks, +meeting the optimization goal. Lexical actually got faster than master. + +#### Remaining Optimization Opportunities (not yet pursued) + +These are lower-priority since the main goal (closure/method within 10%) is met: + +| Option | Effort | Expected Impact | Notes | +|--------|--------|-----------------|-------| +| Pass `PerlRuntime rt` from static apply to instance apply | Low | Eliminates 1 of 2 remaining lookups per sub call | Changes method signatures | +| Cache warning bits on RuntimeCode field | Low | Avoids ConcurrentHashMap lookup per call | `getWarningBitsForCode()` in profile | +| Batch RuntimeRegex field access in match methods | Medium | Eliminates ~10-15 lookups per regex match | Profile showed many individual accessors in RuntimeRegex.java | +| Lazy regex state save (skip when sub doesn't use regex) | High | Could eliminate RegexState overhead entirely | Complex — would need compile-time analysis | +| DynamicVariableManager.variableStack() caching | Low | 1 lookup per call eliminated | 10 samples in profile | + ### Open Questions - `runtimeEvalCounter` and `nextCallsiteId` remain static (shared across runtimes) — acceptable for unique ID generation but may want per-runtime counters in future From b84ee499c0711ee524c1602ea171e395ea1a4656 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 10 Apr 2026 20:18:55 +0200 Subject: [PATCH 6/9] perf: skip RegexState save/restore for subroutines without regex ops EmitterMethodCreator unconditionally emitted RegexState.save() at every subroutine entry, creating and pushing a 13-field snapshot even when the subroutine never uses regex. Now uses RegexUsageDetector to check the AST at compile time and only emits save/restore when the body contains regex operations or eval STRING (which may introduce regex at runtime). This is safe because subroutines without regex don't modify regex state, and any callees that use regex do their own save/restore at their boundary. Closure: 814 -> 1177 ops/s (+44%, now +36% FASTER than master) Method: 399 -> 417 ops/s (+5%, now -4.4% vs master) Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../perlonjava/backend/jvm/EmitterMethodCreator.java | 11 +++++++++-- src/main/java/org/perlonjava/core/Configuration.java | 4 ++-- .../frontend/analysis/RegexUsageDetector.java | 5 +++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java index 41abcdc54..d279bdad1 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java @@ -14,6 +14,7 @@ import org.perlonjava.backend.bytecode.Disassemble; import org.perlonjava.backend.bytecode.InterpretedCode; import org.perlonjava.frontend.analysis.EmitterVisitor; +import org.perlonjava.frontend.analysis.RegexUsageDetector; import org.perlonjava.frontend.analysis.TempLocalCountVisitor; import org.perlonjava.frontend.astnode.BlockNode; import org.perlonjava.frontend.astnode.CompilerFlagNode; @@ -629,8 +630,14 @@ private static byte[] getBytecodeInternal(EmitterContext ctx, Node ast, boolean // Store dynamicIndex so goto &sub can access it for cleanup before tail call ctx.javaClassInfo.dynamicLevelSlot = dynamicIndex; - mv.visitMethodInsn(Opcodes.INVOKESTATIC, - "org/perlonjava/runtime/runtimetypes/RegexState", "save", "()V", false); + // Only save/restore regex state if the subroutine body contains regex + // operations (or eval STRING which may introduce them at runtime). + // Subroutines without regex don't modify regex state, and callees + // that use regex do their own save/restore. + if (RegexUsageDetector.containsRegexOperation(ast)) { + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/RegexState", "save", "()V", false); + } // Store the computed RuntimeList return value in a dedicated local slot. // This keeps the operand stack empty at join labels (endCatch), avoiding diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index ae2dd7b9c..5b77871ca 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "4a3b07287"; + public static final String gitCommitId = "97a476296"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). @@ -48,7 +48,7 @@ public final class Configuration { * Parsed by App::perlbrew and other tools via: perl -V | grep "Compiled at" * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String buildTimestamp = "Apr 10 2026 19:40:14"; + public static final String buildTimestamp = "Apr 10 2026 20:14:09"; // Prevent instantiation private Configuration() { diff --git a/src/main/java/org/perlonjava/frontend/analysis/RegexUsageDetector.java b/src/main/java/org/perlonjava/frontend/analysis/RegexUsageDetector.java index dd7814604..e9227b528 100644 --- a/src/main/java/org/perlonjava/frontend/analysis/RegexUsageDetector.java +++ b/src/main/java/org/perlonjava/frontend/analysis/RegexUsageDetector.java @@ -21,10 +21,11 @@ public class RegexUsageDetector { /** - * Unary operators that perform regex matching/substitution. + * Unary operators that perform regex matching/substitution, + * or that may dynamically introduce regex operations (eval STRING). */ private static final java.util.Set REGEX_OPERATORS = - java.util.Set.of("matchRegex", "replaceRegex"); + java.util.Set.of("matchRegex", "replaceRegex", "eval"); /** * Binary operators that perform regex matching (=~, !~) or use regex internally (split). */ From c445676034b0af6facf9ca5081cad2a6734923ee Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 10 Apr 2026 20:19:21 +0200 Subject: [PATCH 7/9] docs: update concurrency.md with final optimization results Closure is now +36% faster than master (was -34%). Method is now -4.4% vs master (was -27%). Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- dev/design/concurrency.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/dev/design/concurrency.md b/dev/design/concurrency.md index 62339d11a..c4dde8eef 100644 --- a/dev/design/concurrency.md +++ b/dev/design/concurrency.md @@ -1518,18 +1518,21 @@ lookups per sub call** — even when the subroutine never uses regex. | 2 | Migrate WarningBits/HintHash/args stacks to PerlRuntime fields | WarningBitsRegistry.java, HintHashRegistry.java, RuntimeCode.java, PerlRuntime.java | 14-17 per sub call (separate ThreadLocals -> 1 ThreadLocal) | | 2b | Batch pushCallerState/popCallerState and pushSubState/popSubState | PerlRuntime.java, RuntimeCode.java | 8-12 per sub call -> 2 | | 2c | Batch RegexState save/restore | RegexState.java | **24 per sub call** (13 getters + 13 setters -> 2) | +| 2d | Skip RegexState save/restore for subs without regex | EmitterMethodCreator.java, RegexUsageDetector.java | **Entire save/restore eliminated** for non-regex subs | #### Benchmark Results | Benchmark | master | branch (pre-opt) | **After all opts** | vs master | vs pre-opt | |-----------|--------|-------------------|--------------------|-----------|------------| -| **closure** | 863 | 569 (-34.1%) | **814** | **-5.7%** | +43.1% | -| **method** | 436 | 319 (-26.9%) | **399** | **-8.5%** | +25.1% | +| **closure** | 863 | 569 (-34.1%) | **1177** | **+36.4%** | +106.9% | +| **method** | 436 | 319 (-26.9%) | **417** | **-4.4%** | +30.7% | | **lexical** | 394K | 375K (-4.9%) | **466K** | **+18.2%** | +24.3% | -| global | 78K | 74K (-5.4%) | **73K** | -6.4% | -0.4% | +| global | 78K | 74K (-5.4%) | **71K** | -8.6% | -3.4% | -Closure and method are now within the 5-10% range of other benchmarks, -meeting the optimization goal. Lexical actually got faster than master. +Closure is now 36% **faster** than master. Method is within 5%. Lexical is 18% faster. +The closure and lexical improvements come from eliminating unnecessary RegexState +save/restore for subroutines that don't use regex — an overhead that existed on +master too (via separate ThreadLocals) but was masked by lower per-lookup cost. #### Remaining Optimization Opportunities (not yet pursued) From 85dafcaaf24d3327171f42ba669bd7cb5a6bf462 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 10 Apr 2026 21:30:12 +0200 Subject: [PATCH 8/9] feat: migrate remaining shared static state to per-PerlRuntime Migrate Mro caches (packageGenerations, isaRevCache, pkgGenIsaState), RuntimeIO.openHandles LRU cache, RuntimeRegex.optimizedRegexCache, OutputFieldSeparator.internalOFS, OutputRecordSeparator.internalORS, and ByteCodeSourceMapper (all 7 fields via new State inner class) to per-PerlRuntime instance fields for multiplicity thread-safety. No performance regression vs baseline benchmarks. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../backend/jvm/ByteCodeSourceMapper.java | 116 ++++++++++-------- .../org/perlonjava/core/Configuration.java | 4 +- .../perlonjava/runtime/perlmodule/Mro.java | 50 ++++---- .../runtime/regex/RuntimeRegex.java | 37 +++--- .../runtimetypes/OutputFieldSeparator.java | 26 ++-- .../runtimetypes/OutputRecordSeparator.java | 26 ++-- .../runtime/runtimetypes/PerlRuntime.java | 56 +++++++++ .../runtime/runtimetypes/RuntimeIO.java | 51 +++----- 8 files changed, 210 insertions(+), 156 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/jvm/ByteCodeSourceMapper.java b/src/main/java/org/perlonjava/backend/jvm/ByteCodeSourceMapper.java index 2354ff446..d8f389afb 100644 --- a/src/main/java/org/perlonjava/backend/jvm/ByteCodeSourceMapper.java +++ b/src/main/java/org/perlonjava/backend/jvm/ByteCodeSourceMapper.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -13,32 +14,45 @@ * resolution for stack traces at runtime. */ public class ByteCodeSourceMapper { - // Maps source files to their debug information - private static final Map sourceFiles = new HashMap<>(); - // Pool of package names to optimize memory usage - private static final ArrayList packageNamePool = new ArrayList<>(); - private static final Map packageNameToId = new HashMap<>(); - - // Pool of file names to optimize memory usage - private static final ArrayList fileNamePool = new ArrayList<>(); - private static final Map fileNameToId = new HashMap<>(); + /** + * Holds all mutable source-mapper state. One instance lives in each PerlRuntime + * for multiplicity thread-safety. + */ + public static class State { + // Maps source files to their debug information + public final Map sourceFiles = new HashMap<>(); + + // Pool of package names to optimize memory usage + public final List packageNamePool = new ArrayList<>(); + public final Map packageNameToId = new HashMap<>(); + + // Pool of file names to optimize memory usage + public final List fileNamePool = new ArrayList<>(); + public final Map fileNameToId = new HashMap<>(); + + // Pool of subroutine names to optimize memory usage + public final List subroutineNamePool = new ArrayList<>(); + public final Map subroutineNameToId = new HashMap<>(); + + public void resetAll() { + sourceFiles.clear(); + packageNamePool.clear(); + packageNameToId.clear(); + fileNamePool.clear(); + fileNameToId.clear(); + subroutineNamePool.clear(); + subroutineNameToId.clear(); + } + } - // Pool of subroutine names to optimize memory usage - private static final ArrayList subroutineNamePool = new ArrayList<>(); - private static final Map subroutineNameToId = new HashMap<>(); + /** Returns the current PerlRuntime's source-mapper state. */ + private static State state() { + return org.perlonjava.runtime.runtimetypes.PerlRuntime.current().sourceMapperState; + } public static void resetAll() { - sourceFiles.clear(); - - packageNamePool.clear(); - packageNameToId.clear(); - - fileNamePool.clear(); - fileNameToId.clear(); - - subroutineNamePool.clear(); - subroutineNameToId.clear(); + state().resetAll(); } /** @@ -48,9 +62,10 @@ public static void resetAll() { * @return The unique identifier for the package */ private static int getOrCreatePackageId(String packageName) { - return packageNameToId.computeIfAbsent(packageName, name -> { - packageNamePool.add(name); - return packageNamePool.size() - 1; + State s = state(); + return s.packageNameToId.computeIfAbsent(packageName, name -> { + s.packageNamePool.add(name); + return s.packageNamePool.size() - 1; }); } @@ -61,9 +76,10 @@ private static int getOrCreatePackageId(String packageName) { * @return The unique identifier for the file */ private static int getOrCreateFileId(String fileName) { - return fileNameToId.computeIfAbsent(fileName, name -> { - fileNamePool.add(name); - return fileNamePool.size() - 1; + State s = state(); + return s.fileNameToId.computeIfAbsent(fileName, name -> { + s.fileNamePool.add(name); + return s.fileNamePool.size() - 1; }); } @@ -74,9 +90,10 @@ private static int getOrCreateFileId(String fileName) { * @return The unique identifier for the subroutine name */ private static int getOrCreateSubroutineId(String subroutineName) { - return subroutineNameToId.computeIfAbsent(subroutineName, name -> { - subroutineNamePool.add(name); - return subroutineNamePool.size() - 1; + State s = state(); + return s.subroutineNameToId.computeIfAbsent(subroutineName, name -> { + s.subroutineNamePool.add(name); + return s.subroutineNamePool.size() - 1; }); } @@ -87,7 +104,7 @@ private static int getOrCreateSubroutineId(String subroutineName) { */ static void setDebugInfoFileName(EmitterContext ctx) { int fileId = getOrCreateFileId(ctx.compilerOptions.fileName); - sourceFiles.computeIfAbsent(fileId, SourceFileInfo::new); + state().sourceFiles.computeIfAbsent(fileId, SourceFileInfo::new); ctx.cw.visitSource(ctx.compilerOptions.fileName, null); } @@ -126,13 +143,14 @@ static void setDebugInfoLineNumber(EmitterContext ctx, int tokenIndex) { * @param tokenIndex The index of the token in the source code */ public static void saveSourceLocation(EmitterContext ctx, int tokenIndex) { + State s = state(); // Use the ORIGINAL filename (compile-time) for the key, not the #line-adjusted one. // This is because JVM stack traces report the original filename from visitSource(). // The #line-adjusted filename is stored separately in LineInfo for caller() reporting. int fileId = getOrCreateFileId(ctx.compilerOptions.fileName); // Get or create the SourceFileInfo object for the file - SourceFileInfo info = sourceFiles.computeIfAbsent(fileId, SourceFileInfo::new); + SourceFileInfo info = s.sourceFiles.computeIfAbsent(fileId, SourceFileInfo::new); // Get current subroutine name (empty string for main code) String subroutineName = ctx.symbolTable.getCurrentSubroutine(); @@ -156,8 +174,8 @@ public static void saveSourceLocation(EmitterContext ctx, int tokenIndex) { if (System.getenv("DEBUG_CALLER") != null) { System.err.println("DEBUG saveSourceLocation: SKIP (exists) file=" + ctx.compilerOptions.fileName + " tokenIndex=" + tokenIndex + " existingLine=" + existingEntry.lineNumber() - + " existingPkg=" + packageNamePool.get(existingEntry.packageNameId()) - + " existingSourceFile=" + fileNamePool.get(existingEntry.sourceFileNameId())); + + " existingPkg=" + s.packageNamePool.get(existingEntry.packageNameId()) + + " existingSourceFile=" + s.fileNamePool.get(existingEntry.sourceFileNameId())); } return; } @@ -180,7 +198,7 @@ public static void saveSourceLocation(EmitterContext ctx, int tokenIndex) { // Look for nearby entry (within 50 tokens) that has #line-adjusted filename var nearbyEntry = info.tokenToLineInfo.floorEntry(tokenIndex); if (nearbyEntry != null && (tokenIndex - nearbyEntry.getKey()) < 50) { - String nearbySourceFile = fileNamePool.get(nearbyEntry.getValue().sourceFileNameId()); + String nearbySourceFile = s.fileNamePool.get(nearbyEntry.getValue().sourceFileNameId()); if (!nearbySourceFile.equals(ctx.compilerOptions.fileName)) { // Nearby entry has #line-adjusted filename - inherit it sourceFileName = nearbySourceFile; @@ -220,7 +238,8 @@ public static void saveSourceLocation(EmitterContext ctx, int tokenIndex) { * @return The package name at that location, or null if not found */ public static String getPackageAtLocation(String fileName, int tokenIndex) { - int fileId = fileNameToId.getOrDefault(fileName, -1); + State s = state(); + int fileId = s.fileNameToId.getOrDefault(fileName, -1); if (fileId == -1) { if (System.getenv("DEBUG_CALLER") != null) { System.err.println("DEBUG getPackageAtLocation: NO FILE ID for fileName=" + fileName); @@ -228,7 +247,7 @@ public static String getPackageAtLocation(String fileName, int tokenIndex) { return null; } - SourceFileInfo info = sourceFiles.get(fileId); + SourceFileInfo info = s.sourceFiles.get(fileId); if (info == null) { if (System.getenv("DEBUG_CALLER") != null) { System.err.println("DEBUG getPackageAtLocation: NO SOURCE INFO for fileName=" + fileName + " fileId=" + fileId); @@ -244,7 +263,7 @@ public static String getPackageAtLocation(String fileName, int tokenIndex) { return null; } - String pkg = packageNamePool.get(entry.getValue().packageNameId()); + String pkg = s.packageNamePool.get(entry.getValue().packageNameId()); if (System.getenv("DEBUG_CALLER") != null) { System.err.println("DEBUG getPackageAtLocation: fileName=" + fileName + " tokenIndex=" + tokenIndex + " foundTokenIndex=" + entry.getKey() + " pkg=" + pkg); @@ -259,10 +278,11 @@ public static String getPackageAtLocation(String fileName, int tokenIndex) { * @return The corresponding source code location */ public static SourceLocation parseStackTraceElement(StackTraceElement element, HashMap locationToClassName) { - int fileId = fileNameToId.getOrDefault(element.getFileName(), -1); + State s = state(); + int fileId = s.fileNameToId.getOrDefault(element.getFileName(), -1); int tokenIndex = element.getLineNumber(); - SourceFileInfo info = sourceFiles.get(fileId); + SourceFileInfo info = s.sourceFiles.get(fileId); if (info == null) { if (System.getenv("DEBUG_CALLER") != null) { System.err.println("DEBUG parseStackTraceElement: NO INFO for file=" + element.getFileName() + " fileId=" + fileId); @@ -282,9 +302,9 @@ public static SourceLocation parseStackTraceElement(StackTraceElement element, H LineInfo lineInfo = entry.getValue(); // Get the #line directive-adjusted source filename for caller() reporting - String sourceFileName = fileNamePool.get(lineInfo.sourceFileNameId()); + String sourceFileName = s.fileNamePool.get(lineInfo.sourceFileNameId()); int lineNumber = lineInfo.lineNumber(); - String packageName = packageNamePool.get(lineInfo.packageNameId()); + String packageName = s.packageNamePool.get(lineInfo.packageNameId()); // FIX: If the found entry's sourceFile equals the original file (no #line applied), // check for nearby entries that have a #line-adjusted filename. @@ -297,7 +317,7 @@ public static SourceLocation parseStackTraceElement(StackTraceElement element, H boolean foundLineDirective = false; while (lowerEntry != null && (entry.getKey() - lowerEntry.getKey()) < 300) { - String lowerSourceFile = fileNamePool.get(lowerEntry.getValue().sourceFileNameId()); + String lowerSourceFile = s.fileNamePool.get(lowerEntry.getValue().sourceFileNameId()); if (System.getenv("DEBUG_CALLER") != null) { System.err.println("DEBUG parseStackTraceElement: checking lowerEntry key=" + lowerEntry.getKey() + " sourceFile=" + lowerSourceFile + " line=" + lowerEntry.getValue().lineNumber() + " entryKey=" + entry.getKey()); } @@ -327,7 +347,7 @@ public static SourceLocation parseStackTraceElement(StackTraceElement element, H int estimatedExtraLines = tokenDistFromLineDirective / 6; lineNumber = lowerEntry.getValue().lineNumber() + estimatedExtraLines; - packageName = packageNamePool.get(lowerEntry.getValue().packageNameId()); + packageName = s.packageNamePool.get(lowerEntry.getValue().packageNameId()); if (System.getenv("DEBUG_CALLER") != null) { System.err.println("DEBUG parseStackTraceElement: APPLYING lowerEntry sourceFile=" + sourceFileName + " adjustedLine=" + lineNumber + " tokenDist=" + tokenDistFromLineDirective); } @@ -342,7 +362,7 @@ public static SourceLocation parseStackTraceElement(StackTraceElement element, H int currentKey = entry.getKey(); var higherEntry = info.tokenToLineInfo.higherEntry(currentKey); while (higherEntry != null && (higherEntry.getKey() - entry.getKey()) < 50) { - String higherSourceFile = fileNamePool.get(higherEntry.getValue().sourceFileNameId()); + String higherSourceFile = s.fileNamePool.get(higherEntry.getValue().sourceFileNameId()); if (System.getenv("DEBUG_CALLER") != null) { System.err.println("DEBUG parseStackTraceElement: checking higherEntry key=" + higherEntry.getKey() + " sourceFile=" + higherSourceFile + " entryKey=" + entry.getKey() + " currentKey=" + currentKey); } @@ -352,7 +372,7 @@ public static SourceLocation parseStackTraceElement(StackTraceElement element, H lineNumber = higherEntry.getValue().lineNumber() - (higherEntry.getKey() - entry.getKey()); // Approximate adjustment if (lineNumber < 1) lineNumber = 1; - packageName = packageNamePool.get(higherEntry.getValue().packageNameId()); + packageName = s.packageNamePool.get(higherEntry.getValue().packageNameId()); if (System.getenv("DEBUG_CALLER") != null) { System.err.println("DEBUG parseStackTraceElement: APPLYING higherEntry sourceFile=" + sourceFileName + " adjustedLine=" + lineNumber); } @@ -377,7 +397,7 @@ public static SourceLocation parseStackTraceElement(StackTraceElement element, H } // Retrieve subroutine name - String subroutineName = subroutineNamePool.get(lineInfo.subroutineNameId()); + String subroutineName = s.subroutineNamePool.get(lineInfo.subroutineNameId()); // If subroutine name is empty string (main code), convert to null if (subroutineName != null && subroutineName.isEmpty()) { subroutineName = null; diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 5b77871ca..02f752371 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "97a476296"; + public static final String gitCommitId = "c44567603"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). @@ -48,7 +48,7 @@ public final class Configuration { * Parsed by App::perlbrew and other tools via: perl -V | grep "Compiled at" * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String buildTimestamp = "Apr 10 2026 20:14:09"; + public static final String buildTimestamp = "Apr 10 2026 21:21:02"; // Prevent instantiation private Configuration() { diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Mro.java b/src/main/java/org/perlonjava/runtime/perlmodule/Mro.java index b516c2d0d..a536b24c8 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Mro.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Mro.java @@ -12,15 +12,11 @@ * The Mro class provides Perl's mro (Method Resolution Order) module functionality. * It allows switching between different MRO algorithms (DFS and C3) and provides * utilities for introspecting the inheritance hierarchy. + * + * All mutable state is per-PerlRuntime for multiplicity thread-safety. */ public class Mro extends PerlModuleBase { - // Package generation counters - private static final Map packageGenerations = new HashMap<>(); - - // Reverse ISA cache (which classes inherit from a given class) - private static final Map> isaRevCache = new HashMap<>(); - /** * Constructor for Mro. * Initializes the module with the name "mro". @@ -233,27 +229,28 @@ public static RuntimeList get_mro(RuntimeArray args, int ctx) { * Builds the reverse ISA cache by dynamically scanning all packages with @ISA arrays. */ private static void buildIsaRevCache() { - isaRevCache.clear(); + Map> cache = PerlRuntime.current().mroIsaRevCache; + cache.clear(); // Dynamically scan all @ISA arrays from global variables Map allIsaArrays = GlobalVariable.getAllIsaArrays(); for (String key : allIsaArrays.keySet()) { // Key format: "ClassName::ISA" → extract class name String className = key.substring(0, key.length() - 5); // remove "::ISA" - buildIsaRevForClass(className); + buildIsaRevForClass(className, cache); } } /** * Build reverse ISA relationships for a specific class. */ - private static void buildIsaRevForClass(String className) { + private static void buildIsaRevForClass(String className, Map> cache) { if (GlobalVariable.existsGlobalArray(className + "::ISA")) { RuntimeArray isaArray = GlobalVariable.getGlobalArray(className + "::ISA"); for (RuntimeBase parent : isaArray.elements) { String parentName = parent.toString(); if (parentName != null && !parentName.isEmpty()) { - isaRevCache.computeIfAbsent(parentName, k -> new HashSet<>()).add(className); + cache.computeIfAbsent(parentName, k -> new HashSet<>()).add(className); } } } @@ -272,14 +269,15 @@ public static RuntimeList get_isarev(RuntimeArray args, int ctx) { } String className = args.get(0).toString(); + Map> cache = PerlRuntime.current().mroIsaRevCache; // Build reverse ISA cache if empty - if (isaRevCache.isEmpty()) { + if (cache.isEmpty()) { buildIsaRevCache(); } RuntimeArray result = new RuntimeArray(); - Set inheritors = isaRevCache.getOrDefault(className, new HashSet<>()); + Set inheritors = cache.getOrDefault(className, new HashSet<>()); // Add all classes that inherit from this one, including indirectly Set allInheritors = new HashSet<>(); @@ -301,7 +299,8 @@ private static void collectAllInheritors(String className, Set result, S } visited.add(className); - Set directInheritors = isaRevCache.getOrDefault(className, new HashSet<>()); + Map> cache = PerlRuntime.current().mroIsaRevCache; + Set directInheritors = cache.getOrDefault(className, new HashSet<>()); for (String inheritor : directInheritors) { result.add(inheritor); collectAllInheritors(inheritor, result, visited); @@ -347,11 +346,12 @@ public static RuntimeList is_universal(RuntimeArray args, int ctx) { * @return A RuntimeList. */ public static RuntimeList invalidate_all_method_caches(RuntimeArray args, int ctx) { + PerlRuntime rt = PerlRuntime.current(); InheritanceResolver.invalidateCache(); - isaRevCache.clear(); + rt.mroIsaRevCache.clear(); // Increment all package generations - for (String pkg : new HashSet<>(packageGenerations.keySet())) { + for (String pkg : new HashSet<>(rt.mroPackageGenerations.keySet())) { incrementPackageGeneration(pkg); } @@ -371,16 +371,17 @@ public static RuntimeList method_changed_in(RuntimeArray args, int ctx) { } String className = args.get(0).toString(); + Map> cache = PerlRuntime.current().mroIsaRevCache; // Invalidate the method cache InheritanceResolver.invalidateCache(); // Build isarev if needed and invalidate dependent classes - if (isaRevCache.isEmpty()) { + if (cache.isEmpty()) { buildIsaRevCache(); } - Set dependents = isaRevCache.getOrDefault(className, new HashSet<>()); + Set dependents = cache.getOrDefault(className, new HashSet<>()); dependents.add(className); // Include the class itself // Increment package generation for all dependent classes @@ -391,9 +392,6 @@ public static RuntimeList method_changed_in(RuntimeArray args, int ctx) { return new RuntimeList(); } - // Cached @ISA state per package — used to detect @ISA changes in get_pkg_gen - private static final Map> pkgGenIsaState = new HashMap<>(); - /** * Returns the package generation number. * @@ -407,6 +405,7 @@ public static RuntimeList get_pkg_gen(RuntimeArray args, int ctx) { } String className = args.get(0).toString(); + PerlRuntime rt = PerlRuntime.current(); // Lazily detect @ISA changes and auto-increment pkg_gen if (GlobalVariable.existsGlobalArray(className + "::ISA")) { @@ -418,15 +417,15 @@ public static RuntimeList get_pkg_gen(RuntimeArray args, int ctx) { currentIsa.add(parentName); } } - List cachedIsa = pkgGenIsaState.get(className); + List cachedIsa = rt.mroPkgGenIsaState.get(className); if (cachedIsa != null && !currentIsa.equals(cachedIsa)) { incrementPackageGeneration(className); } - pkgGenIsaState.put(className, currentIsa); + rt.mroPkgGenIsaState.put(className, currentIsa); } // Return current generation, starting from 1 - Integer gen = packageGenerations.getOrDefault(className, 1); + Integer gen = rt.mroPackageGenerations.getOrDefault(className, 1); return new RuntimeScalar(gen).getList(); } @@ -437,7 +436,8 @@ public static RuntimeList get_pkg_gen(RuntimeArray args, int ctx) { * @param packageName The name of the package. */ public static void incrementPackageGeneration(String packageName) { - Integer current = packageGenerations.getOrDefault(packageName, 1); - packageGenerations.put(packageName, current + 1); + Map generations = PerlRuntime.current().mroPackageGenerations; + Integer current = generations.getOrDefault(packageName, 1); + generations.put(packageName, current + 1); } } diff --git a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java index 1a80d93e4..addf0dafa 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java +++ b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java @@ -5,10 +5,7 @@ import org.perlonjava.runtime.perlmodule.Utf8; import org.perlonjava.runtime.runtimetypes.*; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -36,15 +33,15 @@ public class RuntimeRegex extends RuntimeBase implements RuntimeScalarReference private static final int DOTALL = Pattern.DOTALL; // Maximum size for the regex cache private static final int MAX_REGEX_CACHE_SIZE = 1000; - // Cache to store compiled regex patterns - private static final Map regexCache = new LinkedHashMap(MAX_REGEX_CACHE_SIZE, 0.75f, true) { - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > MAX_REGEX_CACHE_SIZE; - } - }; - // Cache for /o modifier - maps callsite ID to compiled regex (only first compilation is used) - private static final Map optimizedRegexCache = new LinkedHashMap<>(); + // Cache to store compiled regex patterns (synchronized for multiplicity thread-safety) + private static final Map regexCache = Collections.synchronizedMap( + new LinkedHashMap(MAX_REGEX_CACHE_SIZE, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > MAX_REGEX_CACHE_SIZE; + } + }); + // Cache for /o modifier is now per-PerlRuntime (regexOptimizedCache field) // ---- Regex match state accessors (delegating to PerlRuntime.current()) ---- @@ -464,15 +461,16 @@ public static RuntimeScalar getQuotedRegex(RuntimeScalar patternString, RuntimeS // Check if /o modifier is present if (modifierStr.contains("o")) { + Map cache = PerlRuntime.current().regexOptimizedCache; // Check if we already have a cached regex for this callsite - RuntimeScalar cached = optimizedRegexCache.get(callsiteId); + RuntimeScalar cached = cache.get(callsiteId); if (cached != null) { return cached; } // Compile the regex and cache it RuntimeScalar result = getQuotedRegex(patternString, modifiers); - optimizedRegexCache.put(callsiteId, result); + cache.put(callsiteId, result); return result; } @@ -1254,9 +1252,12 @@ public static RuntimeBase replaceRegex(RuntimeScalar quotedRegex, RuntimeScalar */ public static void reset() { // Iterate over the regexCache and reset the `matched` flag for each cached regex - for (Map.Entry entry : regexCache.entrySet()) { - RuntimeRegex regex = entry.getValue(); - regex.matched = false; // Reset the matched field + // Synchronized because Collections.synchronizedMap requires manual sync for iteration + synchronized (regexCache) { + for (Map.Entry entry : regexCache.entrySet()) { + RuntimeRegex regex = entry.getValue(); + regex.matched = false; // Reset the matched field + } } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/OutputFieldSeparator.java b/src/main/java/org/perlonjava/runtime/runtimetypes/OutputFieldSeparator.java index 49950bedc..bc40c7ef5 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/OutputFieldSeparator.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/OutputFieldSeparator.java @@ -15,12 +15,6 @@ */ public class OutputFieldSeparator extends RuntimeScalar { - /** - * The internal OFS value that print reads. - * Only updated by OutputFieldSeparator.set() calls. - */ - private static String internalOFS = ""; - /** * Stack for save/restore during local $, and for $, (list). * Now held per-PerlRuntime. @@ -35,9 +29,10 @@ public OutputFieldSeparator() { /** * Returns the internal OFS value for use by print. + * Now per-PerlRuntime for multiplicity thread-safety. */ public static String getInternalOFS() { - return internalOFS; + return PerlRuntime.current().internalOFS; } /** @@ -45,7 +40,8 @@ public static String getInternalOFS() { * Called from GlobalRuntimeScalar.dynamicSaveState() when localizing $,. */ public static void saveInternalOFS() { - ofsStack().push(internalOFS); + PerlRuntime rt = PerlRuntime.current(); + ofsStack().push(rt.internalOFS); } /** @@ -54,49 +50,49 @@ public static void saveInternalOFS() { */ public static void restoreInternalOFS() { if (!ofsStack().isEmpty()) { - internalOFS = ofsStack().pop(); + PerlRuntime.current().internalOFS = ofsStack().pop(); } } @Override public RuntimeScalar set(RuntimeScalar value) { super.set(value); - internalOFS = this.toString(); + PerlRuntime.current().internalOFS = this.toString(); return this; } @Override public RuntimeScalar set(String value) { super.set(value); - internalOFS = this.toString(); + PerlRuntime.current().internalOFS = this.toString(); return this; } @Override public RuntimeScalar set(int value) { super.set(value); - internalOFS = this.toString(); + PerlRuntime.current().internalOFS = this.toString(); return this; } @Override public RuntimeScalar set(long value) { super.set(value); - internalOFS = this.toString(); + PerlRuntime.current().internalOFS = this.toString(); return this; } @Override public RuntimeScalar set(boolean value) { super.set(value); - internalOFS = this.toString(); + PerlRuntime.current().internalOFS = this.toString(); return this; } @Override public RuntimeScalar set(Object value) { super.set(value); - internalOFS = this.toString(); + PerlRuntime.current().internalOFS = this.toString(); return this; } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/OutputRecordSeparator.java b/src/main/java/org/perlonjava/runtime/runtimetypes/OutputRecordSeparator.java index 86942ab05..e8cd691ed 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/OutputRecordSeparator.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/OutputRecordSeparator.java @@ -20,12 +20,6 @@ */ public class OutputRecordSeparator extends RuntimeScalar { - /** - * The internal ORS value that print reads. - * Only updated by OutputRecordSeparator.set() calls. - */ - private static String internalORS = ""; - /** * Stack for save/restore during local $\ and for $\ (list). * Now held per-PerlRuntime. @@ -40,9 +34,10 @@ public OutputRecordSeparator() { /** * Returns the internal ORS value for use by print. + * Now per-PerlRuntime for multiplicity thread-safety. */ public static String getInternalORS() { - return internalORS; + return PerlRuntime.current().internalORS; } /** @@ -50,7 +45,8 @@ public static String getInternalORS() { * Called from GlobalRuntimeScalar.dynamicSaveState() when localizing $\. */ public static void saveInternalORS() { - orsStack().push(internalORS); + PerlRuntime rt = PerlRuntime.current(); + orsStack().push(rt.internalORS); } /** @@ -59,49 +55,49 @@ public static void saveInternalORS() { */ public static void restoreInternalORS() { if (!orsStack().isEmpty()) { - internalORS = orsStack().pop(); + PerlRuntime.current().internalORS = orsStack().pop(); } } @Override public RuntimeScalar set(RuntimeScalar value) { super.set(value); - internalORS = this.toString(); + PerlRuntime.current().internalORS = this.toString(); return this; } @Override public RuntimeScalar set(String value) { super.set(value); - internalORS = this.toString(); + PerlRuntime.current().internalORS = this.toString(); return this; } @Override public RuntimeScalar set(int value) { super.set(value); - internalORS = this.toString(); + PerlRuntime.current().internalORS = this.toString(); return this; } @Override public RuntimeScalar set(long value) { super.set(value); - internalORS = this.toString(); + PerlRuntime.current().internalORS = this.toString(); return this; } @Override public RuntimeScalar set(boolean value) { super.set(value); - internalORS = this.toString(); + PerlRuntime.current().internalORS = this.toString(); return this; } @Override public RuntimeScalar set(Object value) { super.set(value); - internalORS = this.toString(); + PerlRuntime.current().internalORS = this.toString(); return this; } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java b/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java index 332f5a22e..3fa4fa437 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java @@ -1,6 +1,7 @@ package org.perlonjava.runtime.runtimetypes; import org.perlonjava.backend.jvm.CustomClassLoader; +import org.perlonjava.runtime.io.IOHandle; import org.perlonjava.runtime.io.StandardIO; import org.perlonjava.runtime.mro.InheritanceResolver; import org.perlonjava.runtime.regex.RuntimeRegex; @@ -11,6 +12,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import java.util.Stack; @@ -153,12 +155,24 @@ public final class PerlRuntime { */ final Stack orsStack = new Stack<>(); + /** + * Internal ORS value that print reads — migrated from OutputRecordSeparator.internalORS + * for multiplicity thread-safety. + */ + public String internalORS = ""; + /** * OFS stack for OutputFieldSeparator "local $," save/restore — * migrated from OutputFieldSeparator.ofsStack. */ final Stack ofsStack = new Stack<>(); + /** + * Internal OFS value that print reads — migrated from OutputFieldSeparator.internalOFS + * for multiplicity thread-safety. + */ + public String internalOFS = ""; + /** * Errno stacks for ErrnoVariable "local $!" save/restore — * migrated from ErrnoVariable.errnoStack and messageStack. @@ -248,6 +262,39 @@ public final class PerlRuntime { */ public InheritanceResolver.MROAlgorithm currentMRO = InheritanceResolver.MROAlgorithm.DFS; + // ---- MRO state — migrated from Mro static fields for multiplicity ---- + + /** Package generation counters for mro::get_pkg_gen(). */ + public final Map mroPackageGenerations = new HashMap<>(); + + /** Reverse ISA cache (which classes inherit from a given class). */ + public final Map> mroIsaRevCache = new HashMap<>(); + + /** Cached @ISA state per package — used to detect @ISA changes. */ + public final Map> mroPkgGenIsaState = new HashMap<>(); + + // ---- IO state — migrated from RuntimeIO static fields for multiplicity ---- + + /** Maximum number of file handles to keep in the LRU cache. */ + private static final int MAX_OPEN_HANDLES = 100; + + /** LRU cache of open file handles — per-runtime for multiplicity. */ + public final Map openHandles = + new LinkedHashMap(MAX_OPEN_HANDLES, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + if (size() > MAX_OPEN_HANDLES) { + try { + eldest.getKey().flush(); + } catch (Exception e) { + // Handle exception if needed + } + return true; + } + return false; + } + }; + // ---- Symbol table state — migrated from GlobalVariable static fields ---- /** Global scalar variables (%main:: scalar namespace). */ @@ -339,6 +386,15 @@ public final class PerlRuntime { /** Preserves BYTE_STRING type on captures. */ public boolean regexLastMatchWasByteString = false; + /** Cache for /o modifier — maps callsite ID to compiled regex (only first compilation is used). */ + public final Map regexOptimizedCache = new HashMap<>(); + + // ---- ByteCodeSourceMapper state — migrated for multiplicity ---- + + /** Per-runtime source-mapper state (file→line→package mappings for stack traces). */ + public final org.perlonjava.backend.jvm.ByteCodeSourceMapper.State sourceMapperState = + new org.perlonjava.backend.jvm.ByteCodeSourceMapper.State(); + // ---- RuntimeCode compilation state — migrated from RuntimeCode static fields ---- /** Tracks eval BEGIN block IDs during compilation. */ diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java index 47ca877a1..b6d6d0201 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java @@ -91,31 +91,12 @@ public class RuntimeIO extends RuntimeScalar { private static final Map> MODE_OPTIONS = new HashMap<>(); /** - * Maximum number of file handles to keep in the LRU cache. - * Older handles are flushed (not closed) when this limit is exceeded. + * Returns the per-runtime LRU cache of open file handles. + * Migrated from a static field for multiplicity thread-safety. */ - private static final int MAX_OPEN_HANDLES = 100; - - /** - * LRU (Least Recently Used) cache for managing open file handles. - * This helps prevent resource exhaustion by limiting open handles and - * automatically flushing less recently used ones. - */ - private static final Map openHandles = new LinkedHashMap(MAX_OPEN_HANDLES, 0.75f, true) { - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - if (size() > MAX_OPEN_HANDLES) { - try { - // Flush but don't close the eldest handle - eldest.getKey().flush(); - } catch (Exception e) { - // Handle exception if needed - } - return true; - } - return false; - } - }; + private static Map openHandles() { + return PerlRuntime.current().openHandles; + } private static final Map childProcesses = new java.util.concurrent.ConcurrentHashMap<>(); @@ -982,8 +963,9 @@ public static void flushFileHandles() { * @param handle the IOHandle to cache */ public static void addHandle(IOHandle handle) { - synchronized (openHandles) { - openHandles.put(handle, Boolean.TRUE); + Map handles = openHandles(); + synchronized (handles) { + handles.put(handle, Boolean.TRUE); } } @@ -993,8 +975,9 @@ public static void addHandle(IOHandle handle) { * @param handle the IOHandle to remove */ public static void removeHandle(IOHandle handle) { - synchronized (openHandles) { - openHandles.remove(handle); + Map handles = openHandles(); + synchronized (handles) { + handles.remove(handle); } } @@ -1003,8 +986,9 @@ public static void removeHandle(IOHandle handle) { * This ensures all buffered data is written without closing files. */ public static void flushAllHandles() { - synchronized (openHandles) { - for (IOHandle handle : openHandles.keySet()) { + Map handles = openHandles(); + synchronized (handles) { + for (IOHandle handle : handles.keySet()) { handle.flush(); } } @@ -1017,8 +1001,9 @@ public static void flushAllHandles() { */ public static void closeAllHandles() { flushAllHandles(); - synchronized (openHandles) { - for (IOHandle handle : openHandles.keySet()) { + Map handles = openHandles(); + synchronized (handles) { + for (IOHandle handle : handles.keySet()) { try { handle.close(); handle = new ClosedIOHandle(); @@ -1026,7 +1011,7 @@ public static void closeAllHandles() { // Handle exception if needed } } - openHandles.clear(); // Clear the cache after closing all handles + handles.clear(); // Clear the cache after closing all handles } } From d4ddb7043133c6a864ac3425326bd2e61ab57a8d Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 10 Apr 2026 22:39:45 +0200 Subject: [PATCH 9/9] perf: JVM-compile anonymous subs inside eval STRING Previously, BytecodeCompiler.visitAnonymousSubroutine() always compiled anonymous sub bodies to InterpretedCode. Hot closures created via eval STRING (e.g., Benchmark.pm's timing wrapper) ran in the bytecode interpreter instead of as native JVM bytecode. Now tries JVM compilation first via EmitterMethodCreator.createClassWithMethod(), falling back to the interpreter on any failure. A new JvmClosureTemplate class holds the JVM-compiled class and instantiates closures with captured variables via reflection. Measured 4.5x speedup for eval STRING closures in isolation (6.4M iter/s vs 1.4M iter/s). Updated benchmark results in concurrency.md - all previously regressed benchmarks now match or exceed master. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- dev/design/concurrency.md | 55 ++++--- .../backend/bytecode/BytecodeCompiler.java | 143 +++++++++++++++++- .../bytecode/OpcodeHandlerExtended.java | 39 +++-- .../backend/jvm/JvmClosureTemplate.java | 131 ++++++++++++++++ .../org/perlonjava/core/Configuration.java | 4 +- 5 files changed, 337 insertions(+), 35 deletions(-) create mode 100644 src/main/java/org/perlonjava/backend/jvm/JvmClosureTemplate.java diff --git a/dev/design/concurrency.md b/dev/design/concurrency.md index c4dde8eef..4e151a548 100644 --- a/dev/design/concurrency.md +++ b/dev/design/concurrency.md @@ -1519,33 +1519,54 @@ lookups per sub call** — even when the subroutine never uses regex. | 2b | Batch pushCallerState/popCallerState and pushSubState/popSubState | PerlRuntime.java, RuntimeCode.java | 8-12 per sub call -> 2 | | 2c | Batch RegexState save/restore | RegexState.java | **24 per sub call** (13 getters + 13 setters -> 2) | | 2d | Skip RegexState save/restore for subs without regex | EmitterMethodCreator.java, RegexUsageDetector.java | **Entire save/restore eliminated** for non-regex subs | - -#### Benchmark Results - -| Benchmark | master | branch (pre-opt) | **After all opts** | vs master | vs pre-opt | -|-----------|--------|-------------------|--------------------|-----------|------------| -| **closure** | 863 | 569 (-34.1%) | **1177** | **+36.4%** | +106.9% | -| **method** | 436 | 319 (-26.9%) | **417** | **-4.4%** | +30.7% | -| **lexical** | 394K | 375K (-4.9%) | **466K** | **+18.2%** | +24.3% | -| global | 78K | 74K (-5.4%) | **71K** | -8.6% | -3.4% | - -Closure is now 36% **faster** than master. Method is within 5%. Lexical is 18% faster. -The closure and lexical improvements come from eliminating unnecessary RegexState -save/restore for subroutines that don't use regex — an overhead that existed on -master too (via separate ThreadLocals) but was masked by lower per-lookup cost. +| 3 | JVM-compile anonymous subs inside `eval STRING` | BytecodeCompiler.java, OpcodeHandlerExtended.java, JvmClosureTemplate.java (new) | N/A (execution speedup, not ThreadLocal reduction) | + +**Tier 3: JVM compilation of eval STRING anonymous subs.** Previously, +`BytecodeCompiler.visitAnonymousSubroutine()` always compiled anonymous sub bodies +to `InterpretedCode`. This meant hot closures created via `eval STRING` (e.g., +Benchmark.pm's `sub { for (1..$n) { &$c } }`) ran in the bytecode interpreter. +The fix tries JVM compilation first via `EmitterMethodCreator.createClassWithMethod()`, +falling back to the interpreter on any failure (e.g., ASM frame computation crash). +A new `JvmClosureTemplate` class holds the JVM-compiled class and instantiates it +with captured variables via reflection. Measured 4.5x speedup for eval STRING closures +in isolation (6.4M iter/s vs 1.4M iter/s). The broader benchmark improvements (method ++36.7% vs pre-opt, global +10.8%) likely reflect this change improving Benchmark.pm's +own infrastructure which is used by all benchmarks. + +#### Benchmark Results (updated 2026-04-10, after JVM eval STRING closures) + +| Benchmark | master | branch (pre-opt) | **Current** | vs master | vs pre-opt | +|-----------|--------|-------------------|-------------|-----------|------------| +| **closure** | 863 | 569 (-34.1%) | **1,220** | **+41.4%** | +114.4% | +| **method** | 436 | 319 (-26.9%) | **436** | **0.0%** | +36.7% | +| **lexical** | 394K | 375K (-4.9%) | **480K** | **+21.8%** | +28.0% | +| global | 78K | 74K (-5.4%) | **82K** | +5.1% | +10.8% | +| eval_string | 86K | 82K (-4.8%) | **89K** | +3.1% | +8.5% | +| regex | 51K | 47K (-7.0%) | **46K** | -9.4% | -2.1% | +| string | 29K | 31K (+6.5%) | **29K** | +0.3% | -5.6% | + +All benchmarks that originally regressed are now **at or above master**. Closure is +41% faster than master, method matches master exactly, lexical is 22% faster, and +global is 5% faster. The closure and lexical improvements come from eliminating +unnecessary RegexState save/restore for subroutines that don't use regex — an +overhead that existed on master too (via separate ThreadLocals) but was masked by +lower per-lookup cost. The regex benchmark shows a small regression (~9%) from +ThreadLocal routing overhead in regex-heavy code paths. #### Remaining Optimization Opportunities (not yet pursued) -These are lower-priority since the main goal (closure/method within 10%) is met: +These are lower-priority since the main goal (closure/method within 10%) is exceeded: | Option | Effort | Expected Impact | Notes | |--------|--------|-----------------|-------| | Pass `PerlRuntime rt` from static apply to instance apply | Low | Eliminates 1 of 2 remaining lookups per sub call | Changes method signatures | | Cache warning bits on RuntimeCode field | Low | Avoids ConcurrentHashMap lookup per call | `getWarningBitsForCode()` in profile | -| Batch RuntimeRegex field access in match methods | Medium | Eliminates ~10-15 lookups per regex match | Profile showed many individual accessors in RuntimeRegex.java | -| Lazy regex state save (skip when sub doesn't use regex) | High | Could eliminate RegexState overhead entirely | Complex — would need compile-time analysis | +| Batch RuntimeRegex field access in match methods | Medium | Eliminates ~10-15 lookups per regex match | Profile showed many individual accessors in RuntimeRegex.java; may help the -9% regex regression | | DynamicVariableManager.variableStack() caching | Low | 1 lookup per call eliminated | 10 samples in profile | +Note: "Lazy regex state save (skip when sub doesn't use regex)" was listed here previously +and has been implemented as Tier 2d above. + ### Open Questions - `runtimeEvalCounter` and `nextCallsiteId` remain static (shared across runtimes) — acceptable for unique ID generation but may want per-runtime counters in future diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index fee599e5f..f9ddf67df 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -3,6 +3,9 @@ import org.perlonjava.backend.jvm.EmitterContext; import org.perlonjava.backend.jvm.EmitterMethodCreator; +import org.perlonjava.backend.jvm.InterpreterFallbackException; +import org.perlonjava.backend.jvm.JavaClassInfo; +import org.perlonjava.backend.jvm.JvmClosureTemplate; import org.perlonjava.frontend.analysis.ConstantFoldingVisitor; import org.perlonjava.frontend.analysis.FindDeclarationVisitor; import org.perlonjava.frontend.analysis.RegexUsageDetector; @@ -4909,6 +4912,10 @@ private void visitNamedSubroutine(SubroutineNode node) { *

* Compiles the subroutine body to bytecode with closure support. * Anonymous subs capture lexical variables from the enclosing scope. + *

+ * When compiled inside eval STRING with an EmitterContext available, + * attempts JVM compilation first for better runtime performance. + * Falls back to interpreter bytecode if JVM compilation fails. */ private void visitAnonymousSubroutine(SubroutineNode node) { // Step 1: Collect closure variables. @@ -4924,7 +4931,141 @@ private void visitAnonymousSubroutine(SubroutineNode node) { closureCapturedVarNames.addAll(closureVarNames); - // Step 3: Create a new BytecodeCompiler for the subroutine body + // Step 2: Try JVM compilation first if we have an EmitterContext (eval STRING path) + // Skip JVM attempt for defer blocks and map/grep blocks which have special control flow + Boolean isDeferBlock = (Boolean) node.getAnnotation("isDeferBlock"); + Boolean isMapGrepBlock = (Boolean) node.getAnnotation("isMapGrepBlock"); + boolean skipJvm = (isDeferBlock != null && isDeferBlock) + || (isMapGrepBlock != null && isMapGrepBlock); + + if (this.emitterContext != null && !skipJvm) { + try { + emitJvmAnonymousSub(node, closureVarNames, closureVarIndices); + return; // JVM compilation succeeded + } catch (Exception e) { + // JVM compilation failed, fall through to interpreter path + if (System.getenv("JPERL_SHOW_FALLBACK") != null) { + System.err.println("JVM compilation failed for anonymous sub in eval STRING, using interpreter: " + + e.getClass().getSimpleName() + ": " + e.getMessage()); + } + } + } + + // Step 3: Interpreter compilation (existing path) + emitInterpretedAnonymousSub(node, closureVarNames, closureVarIndices); + } + + /** + * Attempt to compile an anonymous sub body to JVM bytecode. + * Creates an EmitterContext, calls EmitterMethodCreator.createClassWithMethod(), + * and emits interpreter opcodes to instantiate the JVM class at runtime. + */ + private void emitJvmAnonymousSub(SubroutineNode node, + List closureVarNames, + List closureVarIndices) { + // Build a ScopedSymbolTable for the sub body with captured variables + ScopedSymbolTable newSymbolTable = new ScopedSymbolTable(); + newSymbolTable.enterScope(); + + // Add reserved variables first to occupy slots 0-2 + // EmitterMethodCreator skips these (skipVariables=3) but they must be present + // in the symbol table to keep captured variable indices aligned at 3+ + newSymbolTable.addVariable("this", "", getCurrentPackage(), null); + newSymbolTable.addVariable("@_", "", getCurrentPackage(), null); + newSymbolTable.addVariable("wantarray", "", getCurrentPackage(), null); + + // Add captured variables to the symbol table + // They will be at indices 3, 4, 5, ... (after this/@_/wantarray) + for (String varName : closureVarNames) { + newSymbolTable.addVariable(varName, "my", getCurrentPackage(), null); + } + + // Copy package and pragma flags from the current BytecodeCompiler state + newSymbolTable.setCurrentPackage(getCurrentPackage(), symbolTable.currentPackageIsClass()); + newSymbolTable.strictOptionsStack.pop(); + newSymbolTable.strictOptionsStack.push(symbolTable.strictOptionsStack.peek()); + newSymbolTable.featureFlagsStack.pop(); + newSymbolTable.featureFlagsStack.push(symbolTable.featureFlagsStack.peek()); + newSymbolTable.warningFlagsStack.pop(); + newSymbolTable.warningFlagsStack.push((java.util.BitSet) symbolTable.warningFlagsStack.peek().clone()); + newSymbolTable.warningFatalStack.pop(); + newSymbolTable.warningFatalStack.push((java.util.BitSet) symbolTable.warningFatalStack.peek().clone()); + newSymbolTable.warningDisabledStack.pop(); + newSymbolTable.warningDisabledStack.push((java.util.BitSet) symbolTable.warningDisabledStack.peek().clone()); + + // Reset variable index past the captured variables + String[] newEnv = newSymbolTable.getVariableNames(); + int currentVarIndex = newSymbolTable.getCurrentLocalVariableIndex(); + int resetTo = Math.max(newEnv.length, currentVarIndex); + newSymbolTable.resetLocalVariableIndex(resetTo); + + // Create EmitterContext for JVM compilation + JavaClassInfo newJavaClassInfo = new JavaClassInfo(); + EmitterContext subCtx = new EmitterContext( + newJavaClassInfo, + newSymbolTable, + null, // mv - will be set by EmitterMethodCreator + null, // cw - will be set by EmitterMethodCreator + RuntimeContextType.RUNTIME, + true, + this.errorUtil, + this.emitterContext.compilerOptions, + new RuntimeArray() + ); + + // Try JVM compilation - may throw InterpreterFallbackException or other exceptions + Class generatedClass = EmitterMethodCreator.createClassWithMethod( + subCtx, node.block, false); + + // Cache the generated class + RuntimeCode.getAnonSubs().put(subCtx.javaClassInfo.javaClassName, generatedClass); + + // Emit interpreter opcodes to create the code reference at runtime + int codeReg = allocateRegister(); + String packageName = getCurrentPackage(); + + if (closureVarIndices.isEmpty()) { + // No closures - instantiate JVM class at compile time + JvmClosureTemplate template = new JvmClosureTemplate( + generatedClass, node.prototype, packageName); + RuntimeScalar codeScalar = template.instantiateNoClosure(); + + // Handle attributes + if (node.attributes != null && !node.attributes.isEmpty() && packageName != null) { + RuntimeCode code = (RuntimeCode) codeScalar.value; + code.attributes = node.attributes; + Attributes.runtimeDispatchModifyCodeAttributes(packageName, codeScalar); + } + + int constIdx = addToConstantPool(codeScalar); + emit(Opcodes.LOAD_CONST); + emitReg(codeReg); + emit(constIdx); + } else { + // Has closures - store JvmClosureTemplate in constant pool + // CREATE_CLOSURE opcode handles both InterpretedCode and JvmClosureTemplate + JvmClosureTemplate template = new JvmClosureTemplate( + generatedClass, node.prototype, packageName); + int templateIdx = addToConstantPool(template); + emit(Opcodes.CREATE_CLOSURE); + emitReg(codeReg); + emit(templateIdx); + emit(closureVarIndices.size()); + for (int regIdx : closureVarIndices) { + emit(regIdx); + } + } + + lastResultReg = codeReg; + } + + /** + * Compile an anonymous sub to InterpretedCode (the fallback/default path). + * This is the original implementation of visitAnonymousSubroutine. + */ + private void emitInterpretedAnonymousSub(SubroutineNode node, + List closureVarNames, + List closureVarIndices) { // Build a variable registry from current scope to pass to sub-compiler // This allows nested closures to see grandparent scope variables Map parentRegistry = new HashMap<>(); diff --git a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java index 91e81e872..830e10319 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java +++ b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java @@ -1,5 +1,6 @@ package org.perlonjava.backend.bytecode; +import org.perlonjava.backend.jvm.JvmClosureTemplate; import org.perlonjava.runtime.operators.*; import org.perlonjava.runtime.perlmodule.Attributes; import org.perlonjava.runtime.regex.RuntimeRegex; @@ -888,15 +889,15 @@ public static int executeMatchRegexNot(int[] bytecode, int pc, RuntimeBase[] reg /** * Execute create closure operation. * Format: CREATE_CLOSURE rd template_idx num_captures reg1 reg2 ... + *

+ * Supports both InterpretedCode templates (interpreter-compiled subs) + * and JvmClosureTemplate (JVM-compiled subs from eval STRING). */ public static int executeCreateClosure(int[] bytecode, int pc, RuntimeBase[] registers, InterpretedCode code) { int rd = bytecode[pc++]; int templateIdx = bytecode[pc++]; int numCaptures = bytecode[pc++]; - // Get the template InterpretedCode from constants - InterpretedCode template = (InterpretedCode) code.constants[templateIdx]; - // Capture the current register values RuntimeBase[] capturedVars = new RuntimeBase[numCaptures]; for (int i = 0; i < numCaptures; i++) { @@ -904,19 +905,27 @@ public static int executeCreateClosure(int[] bytecode, int pc, RuntimeBase[] reg capturedVars[i] = registers[captureReg]; } - // Create a new InterpretedCode with the captured variables - InterpretedCode closureCode = template.withCapturedVars(capturedVars); - - // Wrap in RuntimeScalar and set __SUB__ for self-reference - RuntimeScalar codeRef = new RuntimeScalar(closureCode); - closureCode.__SUB__ = codeRef; - registers[rd] = codeRef; + Object template = code.constants[templateIdx]; - // Dispatch MODIFY_CODE_ATTRIBUTES for anonymous subs with non-builtin attributes - // Pass isClosure=true since CREATE_CLOSURE always creates a closure - if (closureCode.attributes != null && !closureCode.attributes.isEmpty() - && closureCode.packageName != null) { - Attributes.runtimeDispatchModifyCodeAttributes(closureCode.packageName, codeRef, true); + if (template instanceof JvmClosureTemplate jvmTemplate) { + // JVM-compiled closure: instantiate the generated class with captured variables + registers[rd] = jvmTemplate.instantiate(capturedVars); + } else { + // InterpretedCode closure: create a new copy with captured variables + InterpretedCode interpTemplate = (InterpretedCode) template; + InterpretedCode closureCode = interpTemplate.withCapturedVars(capturedVars); + + // Wrap in RuntimeScalar and set __SUB__ for self-reference + RuntimeScalar codeRef = new RuntimeScalar(closureCode); + closureCode.__SUB__ = codeRef; + registers[rd] = codeRef; + + // Dispatch MODIFY_CODE_ATTRIBUTES for anonymous subs with non-builtin attributes + // Pass isClosure=true since CREATE_CLOSURE always creates a closure + if (closureCode.attributes != null && !closureCode.attributes.isEmpty() + && closureCode.packageName != null) { + Attributes.runtimeDispatchModifyCodeAttributes(closureCode.packageName, codeRef, true); + } } return pc; } diff --git a/src/main/java/org/perlonjava/backend/jvm/JvmClosureTemplate.java b/src/main/java/org/perlonjava/backend/jvm/JvmClosureTemplate.java new file mode 100644 index 000000000..bdb3c134b --- /dev/null +++ b/src/main/java/org/perlonjava/backend/jvm/JvmClosureTemplate.java @@ -0,0 +1,131 @@ +package org.perlonjava.backend.jvm; + +import org.perlonjava.runtime.runtimetypes.PerlSubroutine; +import org.perlonjava.runtime.runtimetypes.RuntimeBase; +import org.perlonjava.runtime.runtimetypes.RuntimeCode; +import org.perlonjava.runtime.runtimetypes.RuntimeScalar; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; + +/** + * Template for creating JVM-compiled closures from interpreter bytecode. + *

+ * When an anonymous sub inside eval STRING is compiled to a JVM class + * (instead of InterpretedCode), this template holds the generated class + * and knows how to instantiate it with captured variables at runtime. + *

+ * The CREATE_CLOSURE interpreter opcode checks for this type in the + * constant pool and delegates to {@link #instantiate(RuntimeBase[])} + * to create the closure. + */ +public class JvmClosureTemplate { + + /** + * The JVM-compiled class implementing PerlSubroutine + */ + public final Class generatedClass; + + /** + * Cached constructor for the generated class. + * Takes captured variables as typed parameters (RuntimeScalar, RuntimeArray, RuntimeHash). + */ + public final Constructor constructor; + + /** + * Perl prototype string (e.g., "$$@"), or null + */ + public final String prototype; + + /** + * Package where the sub was compiled (CvSTASH) + */ + public final String packageName; + + /** + * Creates a JvmClosureTemplate for a generated class. + * + * @param generatedClass the JVM class implementing PerlSubroutine + * @param prototype Perl prototype string, or null + * @param packageName package where the sub was compiled + */ + public JvmClosureTemplate(Class generatedClass, String prototype, String packageName) { + this.generatedClass = generatedClass; + this.prototype = prototype; + this.packageName = packageName; + + // Cache the constructor - there should be exactly one with closure parameters + Constructor[] constructors = generatedClass.getDeclaredConstructors(); + // Find the constructor that takes the most parameters (the closure one) + Constructor best = null; + for (Constructor c : constructors) { + if (best == null || c.getParameterCount() > best.getParameterCount()) { + best = c; + } + } + this.constructor = best; + } + + /** + * Instantiate the JVM-compiled closure with captured variables. + *

+ * This is called at runtime by the interpreter's CREATE_CLOSURE opcode + * when it encounters a JvmClosureTemplate in the constant pool. + *

+ * The cost of reflection here is amortized: this is called once per + * closure creation (each time the sub {} expression is evaluated), + * not per call to the closure. + * + * @param capturedVars the captured variable values from the interpreter's registers + * @return a RuntimeScalar wrapping the RuntimeCode for this closure + */ + public RuntimeScalar instantiate(RuntimeBase[] capturedVars) { + try { + // Convert RuntimeBase[] to Object[] for reflection + Object[] args = new Object[capturedVars.length]; + System.arraycopy(capturedVars, 0, args, 0, capturedVars.length); + + // Instantiate the JVM class with captured variables as constructor args + PerlSubroutine instance = (PerlSubroutine) constructor.newInstance(args); + + // Create RuntimeCode wrapping and set __SUB__ + RuntimeCode code = new RuntimeCode(instance, prototype); + if (packageName != null) { + code.packageName = packageName; + } + RuntimeScalar codeRef = new RuntimeScalar(code); + + // Set __SUB__ on the generated class instance for self-reference + Field subField = generatedClass.getDeclaredField("__SUB__"); + subField.set(instance, codeRef); + + return codeRef; + } catch (Exception e) { + throw new RuntimeException("Failed to instantiate JVM closure: " + e.getMessage(), e); + } + } + + /** + * Instantiate the JVM-compiled sub with no captured variables. + * + * @return a RuntimeScalar wrapping the RuntimeCode + */ + public RuntimeScalar instantiateNoClosure() { + try { + PerlSubroutine instance = (PerlSubroutine) generatedClass.getDeclaredConstructor().newInstance(); + + RuntimeCode code = new RuntimeCode(instance, prototype); + if (packageName != null) { + code.packageName = packageName; + } + RuntimeScalar codeRef = new RuntimeScalar(code); + + Field subField = generatedClass.getDeclaredField("__SUB__"); + subField.set(instance, codeRef); + + return codeRef; + } catch (Exception e) { + throw new RuntimeException("Failed to instantiate JVM sub: " + e.getMessage(), e); + } + } +} diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 02f752371..5f40c1f5d 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "c44567603"; + public static final String gitCommitId = "85dafcaaf"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). @@ -48,7 +48,7 @@ public final class Configuration { * Parsed by App::perlbrew and other tools via: perl -V | grep "Compiled at" * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String buildTimestamp = "Apr 10 2026 21:21:02"; + public static final String buildTimestamp = "Apr 10 2026 22:38:49"; // Prevent instantiation private Configuration() {