Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@
*/
public class PerlLanguageProvider {

// Cache env var at class-init to avoid repeated native System.getenv()
// calls from the compilation fallback hot path.
private static final boolean SHOW_FALLBACK =
System.getenv("JPERL_SHOW_FALLBACK") != null;

private static boolean globalInitialized = false;

public static void resetAll() {
Expand Down Expand Up @@ -538,7 +543,7 @@ private static RuntimeCode compileToExecutable(Node ast, EmitterContext ctx) thr
// getBytecode() already compiled interpreter code as fallback
// when ASM frame computation failed (e.g., high fan-in to shared labels).
// Use the pre-compiled interpreter code directly.
boolean showFallback = System.getenv("JPERL_SHOW_FALLBACK") != null;
boolean showFallback = SHOW_FALLBACK;
if (showFallback) {
System.err.println("Note: Using interpreter fallback (ASM frame compute crash).");
}
Expand All @@ -548,7 +553,7 @@ private static RuntimeCode compileToExecutable(Node ast, EmitterContext ctx) thr
// Catch Throwable (not just RuntimeException) because ClassFormatError
// ("Too many arguments in method signature") extends Error, not Exception
if (needsInterpreterFallback(e)) {
boolean showFallback = System.getenv("JPERL_SHOW_FALLBACK") != null;
boolean showFallback = SHOW_FALLBACK;
if (showFallback) {
System.err.println("Note: Method too large, using interpreter backend.");
}
Expand Down
29 changes: 21 additions & 8 deletions src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,21 @@ public class EmitterMethodCreator implements Opcodes {
System.getenv("JPERL_DISABLE_INTERPRETER_FALLBACK") == null;
private static final boolean SHOW_FALLBACK =
System.getenv("JPERL_SHOW_FALLBACK") != null;
// Cache additional compile-time debug env vars. These were previously
// read with System.getenv() on every method compilation; the native
// lookup is ~200ns per call and added up across thousands of compiled
// subs during module load.
private static final boolean ASM_DEBUG =
System.getenv("JPERL_ASM_DEBUG") != null;
private static final String ASM_DEBUG_CLASS_FILTER =
System.getenv("JPERL_ASM_DEBUG_CLASS");
private static final String BYTECODE_SIZE_DEBUG =
System.getenv("JPERL_BYTECODE_SIZE_DEBUG");
private static final int SPILL_SLOT_COUNT;
static {
String s = System.getenv("JPERL_SPILL_SLOTS");
SPILL_SLOT_COUNT = (s != null) ? Integer.parseInt(s) : 16;
}
// Number of local variables to skip when processing a closure (this, @_, wantarray)
public static int skipVariables = 3;
// Counter for generating unique class names
Expand Down Expand Up @@ -350,7 +365,7 @@ public static Class<?> createClassWithMethod(EmitterContext ctx, Node ast, boole
}

public static byte[] getBytecode(EmitterContext ctx, Node ast, boolean useTryCatch) {
boolean asmDebug = System.getenv("JPERL_ASM_DEBUG") != null;
boolean asmDebug = ASM_DEBUG;

try {
return getBytecodeInternal(ctx, ast, useTryCatch, false);
Expand All @@ -363,7 +378,7 @@ public static byte[] getBytecode(EmitterContext ctx, Node ast, boolean useTryCat
// ASM frame computation failed - fall back to interpreter
// This commonly happens with nested defers and complex control flow

boolean showFallback = System.getenv("JPERL_SHOW_FALLBACK") != null;
boolean showFallback = SHOW_FALLBACK;
if (showFallback || asmDebug) {
frameComputeCrash.printStackTrace();
try {
Expand Down Expand Up @@ -411,8 +426,8 @@ private static byte[] getBytecodeInternal(EmitterContext ctx, Node ast, boolean
String className = ctx.javaClassInfo.javaClassName;
String methodName = "apply";
byte[] classData = null;
boolean asmDebug = System.getenv("JPERL_ASM_DEBUG") != null;
String asmDebugClassFilter = System.getenv("JPERL_ASM_DEBUG_CLASS");
boolean asmDebug = ASM_DEBUG;
String asmDebugClassFilter = ASM_DEBUG_CLASS_FILTER;
boolean asmDebugClassMatches = asmDebugClassFilter == null
|| asmDebugClassFilter.isEmpty()
|| className.contains(asmDebugClassFilter)
Expand Down Expand Up @@ -605,9 +620,7 @@ private static byte[] getBytecodeInternal(EmitterContext ctx, Node ast, boolean
mv.visitInsn(Opcodes.ICONST_0);
mv.visitVarInsn(Opcodes.ISTORE, controlFlowActionSlot);

int spillSlotCount = System.getenv("JPERL_SPILL_SLOTS") != null
? Integer.parseInt(System.getenv("JPERL_SPILL_SLOTS"))
: 16;
int spillSlotCount = SPILL_SLOT_COUNT;
ctx.javaClassInfo.spillSlots = new int[spillSlotCount];
ctx.javaClassInfo.spillTop = 0;
for (int i = 0; i < spillSlotCount; i++) {
Expand Down Expand Up @@ -1114,7 +1127,7 @@ private static byte[] getBytecodeInternal(EmitterContext ctx, Node ast, boolean
cw.visitEnd();
classData = cw.toByteArray(); // Generate the bytecode

String bytecodeSizeDebug = System.getenv("JPERL_BYTECODE_SIZE_DEBUG");
String bytecodeSizeDebug = BYTECODE_SIZE_DEBUG;
if (bytecodeSizeDebug != null && !bytecodeSizeDebug.isEmpty()) {
try {
System.err.println("BYTECODE_SIZE class=" + className + " bytes=" + classData.length);
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/org/perlonjava/core/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "b331c5d70";
public static final String gitCommitId = "17527e8e7";

/**
* Git commit date of the build (ISO format: YYYY-MM-DD).
Expand All @@ -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 21 2026 00:05:50";
public static final String buildTimestamp = "Apr 21 2026 14:19:54";

// Prevent instantiation
private Configuration() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@

public class SubroutineParser {

// Cache env var at class-init to avoid repeated native System.getenv()
// calls from the subroutine-parse hot path.
private static final boolean SHOW_FALLBACK =
System.getenv("JPERL_SHOW_FALLBACK") != null;

// Create a static semaphore with 1 permit
private static final Semaphore semaphore = new Semaphore(1);

Expand Down Expand Up @@ -1322,7 +1327,7 @@ public static ListNode handleNamedSubWithFilter(Parser parser, String subName, S
// but the verifier rejected it at link time due to StackMapTable inconsistencies
// (e.g., local variable slot type conflicts in complex methods).
// Fall back to interpreter for this subroutine.
boolean showFallback = System.getenv("JPERL_SHOW_FALLBACK") != null;
boolean showFallback = SHOW_FALLBACK;
if (showFallback) {
System.err.println("Note: JVM VerifyError during subroutine instantiation, recompiling with interpreter.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public static RuntimeScalar bitwiseAnd(RuntimeScalar runtimeScalar, RuntimeScala
int t1 = runtimeScalar.type;
int t2 = arg2.type;
if (t1 == RuntimeScalarType.INTEGER && t2 == RuntimeScalarType.INTEGER) {
long result = ((int) runtimeScalar.value) & ((int) arg2.value);
int result = ((int) runtimeScalar.value) & ((int) arg2.value);
return new RuntimeScalar(result);
}

Expand Down Expand Up @@ -95,7 +95,7 @@ public static RuntimeScalar bitwiseOr(RuntimeScalar runtimeScalar, RuntimeScalar
int t1 = runtimeScalar.type;
int t2 = arg2.type;
if (t1 == RuntimeScalarType.INTEGER && t2 == RuntimeScalarType.INTEGER) {
long result = ((int) runtimeScalar.value) | ((int) arg2.value);
int result = ((int) runtimeScalar.value) | ((int) arg2.value);
return new RuntimeScalar(result);
}

Expand Down Expand Up @@ -158,8 +158,9 @@ public static RuntimeScalar bitwiseXor(RuntimeScalar runtimeScalar, RuntimeScala
int t1 = runtimeScalar.type;
int t2 = arg2.type;
if (t1 == RuntimeScalarType.INTEGER && t2 == RuntimeScalarType.INTEGER) {
long result = ((int) runtimeScalar.value) ^ ((int) arg2.value);
return new RuntimeScalar(result);
// int ^ int produces int; call RuntimeScalar(int) directly to skip
// initializeWithLong's range-check branches (JFR hot path).
return new RuntimeScalar(((int) runtimeScalar.value) ^ ((int) arg2.value));
}

// Check for overloaded '^' operator on blessed objects
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ public class IOOperator {
// File descriptor to RuntimeIO mapping for duplication support
private static final Map<Integer, RuntimeIO> fileDescriptorMap = new ConcurrentHashMap<>();

// Cache debug flag at class-init to avoid repeated native
// System.getenv() calls in hot IO paths (open, close).
private static final boolean IO_DEBUG =
System.getenv("JPERL_IO_DEBUG") != null;

public static RuntimeScalar select(RuntimeList runtimeList, int ctx) {
if (runtimeList.isEmpty()) {
// select (returns current filehandle)
Expand Down Expand Up @@ -526,7 +531,7 @@ public static RuntimeScalar open(int ctx, RuntimeBase... args) {
// open FILEHANDLE,EXPR
// open FILEHANDLE

boolean ioDebug = System.getenv("JPERL_IO_DEBUG") != null;
boolean ioDebug = IO_DEBUG;

// Get the filehandle - this should be an lvalue RuntimeScalar
// For array/hash elements like $fh0[0], this is the actual lvalue that can be modified
Expand Down Expand Up @@ -2800,7 +2805,7 @@ private static RuntimeIO duplicateFileHandle(RuntimeIO original) {
duplicate.registerExternalFd(dupFd);
}

if (System.getenv("JPERL_IO_DEBUG") != null) {
if (IO_DEBUG) {
String origFileno;
try {
origFileno = original.ioHandle.fileno().toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,8 @@ public static boolean suppressFlush(boolean suppress) {
private static final long AUTO_SWEEP_MIN_INTERVAL_NS = 5_000_000_000L;
private static final boolean AUTO_GC_DISABLED =
System.getenv("JPERL_NO_AUTO_GC") != null;
private static final boolean GC_DEBUG =
System.getenv("JPERL_GC_DEBUG") != null;
private static boolean inAutoSweep = false;

public static void flush() {
Expand Down Expand Up @@ -584,7 +586,7 @@ private static void maybeAutoSweep() {
// Explicit Internals::jperl_gc() still fires DESTROY for
// callers that want full cleanup.
int cleared = ReachabilityWalker.sweepWeakRefs(true);
if (System.getenv("JPERL_GC_DEBUG") != null) {
if (GC_DEBUG) {
System.err.println("DBG auto-sweep cleared=" + cleared);
}
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,12 @@ public static int pushMark() {
*/
public static void register(Object var) {
stack.add(var);
if (var != null) {
// liveCounts is only consulted by ReachabilityWalker.sweepWeakRefs,
// which runs only when WeakRefRegistry.weakRefsExist is true. For
// scripts that never weaken(), this merge() is pure overhead —
// HashMap.merge with a lambda is one of the hotter per-`my`-var
// costs. See ScalarRefRegistry.registerRef for the parallel fix.
if (var != null && WeakRefRegistry.weakRefsExist) {
liveCounts.merge(var, 1, Integer::sum);
}
}
Expand Down
53 changes: 42 additions & 11 deletions src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -207,13 +207,29 @@ public static void pushArgs(RuntimeArray args) {
argsStack.get().push(args);
// Also push a shallow snapshot so @DB::args stays intact after shift/@_
// modifications inside the callee. See originalArgsStack javadoc.
RuntimeArray snapshot = new RuntimeArray();
if (args != null) {
//
// The snapshot only matters when caller() is invoked from package DB
// (Carp-style stack traces, debugger). For the common case of subs
// that neither shift @_ nor have a caller-from-DB on the stack, this
// allocation was pure overhead. Empty-args fast path + shared empty
// snapshot cuts the per-sub-call cost significantly for life_bitpacked
// and similar tight-loop workloads.
RuntimeArray snapshot;
if (args == null || args.elements.isEmpty()) {
snapshot = EMPTY_ARGS_SNAPSHOT;
} else {
snapshot = new RuntimeArray();
snapshot.elements = new java.util.ArrayList<>(args.elements);
}
originalArgsStack.get().push(snapshot);
}

// Singleton empty-args snapshot for pushArgs. Safe to share because
// originalArgsStack readers only use .getList() / iteration; they never
// mutate the snapshot itself. This avoids a per-empty-call allocation of
// a RuntimeArray and an ArrayList wrapper.
private static final RuntimeArray EMPTY_ARGS_SNAPSHOT = new RuntimeArray();

/**
* Pop @_ from the args stack when exiting a subroutine.
* Public so BytecodeInterpreter can use it when calling InterpretedCode directly.
Expand Down Expand Up @@ -317,6 +333,15 @@ public static void clearInlineMethodCache() {
public String sourcePackage = null;
// Flag to indicate this is a symbolic reference created by \&{string} that should always be "defined"
public boolean isSymbolicReference = false;
// Cached warning bits string for JVM-compiled code. getWarningBitsForCode
// resolves this from the methodHandle's declaring class name via a
// HashMap lookup. The result is stable for the lifetime of the RuntimeCode
// (the declaring class never changes post-compile), so compute it once
// lazily. null-cached-as-sentinel: WARNING_BITS_NOT_COMPUTED means
// "not yet cached"; null result gets stored as
// WARNING_BITS_EXPLICITLY_NULL.
private static final String WARNING_BITS_NOT_COMPUTED = "<uninit>";
private String cachedWarningBits = WARNING_BITS_NOT_COMPUTED;
// Flag to indicate this is a built-in operator
public boolean isBuiltin = false;
// Flag to indicate this was explicitly declared (sub foo; or sub foo { ... })
Expand Down Expand Up @@ -2595,22 +2620,28 @@ private static String getWarningBitsForCode(RuntimeCode code) {
if (code instanceof org.perlonjava.backend.bytecode.InterpretedCode interpCode) {
return interpCode.warningBitsString;
}

// For JVM-compiled code, look up by class name in the registry
// The methodHandle's class is the generated class that has WARNING_BITS field

// JVM-compiled code: cache the lookup result. The declaring class of
// the methodHandle is stable post-compile, so one HashMap lookup is
// all we ever need per RuntimeCode instance. Previously this ran on
// every sub invocation — a hot-path overhead for scripts with many
// small subs (life_bitpacked, method-chain-heavy code).
String cached = code.cachedWarningBits;
if (cached != WARNING_BITS_NOT_COMPUTED) {
return cached;
}
if (code.methodHandle != null) {
// Get the declaring class of the method handle
try {
// The type contains the declaring class as the first parameter type for instance methods
// For our generated apply methods, we use the class that was loaded
String className = code.methodHandle.type().parameterType(0).getName();
return WarningBitsRegistry.get(className);
String result = WarningBitsRegistry.get(className);
code.cachedWarningBits = result;
return result;
} catch (Exception e) {
// If we can't get the class name, fall back to null
code.cachedWarningBits = null;
return null;
}
}

code.cachedWarningBits = null;
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,11 @@ protected boolean removeEldestEntry(Map.Entry<IOHandle, Boolean> eldest) {
private static final ReferenceQueue<RuntimeGlob> globGCQueue = new ReferenceQueue<>();
private static final ConcurrentHashMap<PhantomReference<RuntimeGlob>, RuntimeIO> phantomToIO = new ConcurrentHashMap<>();

// Cache debug flag at class-init to avoid repeated native
// System.getenv() calls in the hot getRuntimeIO / close paths.
private static final boolean IO_DEBUG =
System.getenv("JPERL_IO_DEBUG") != null;

/**
* Registers an anonymous RuntimeGlob for GC-based fd recycling.
* When the glob becomes unreachable (all variables referencing it are
Expand Down Expand Up @@ -1054,7 +1059,7 @@ public static void closeAllHandles() {
*/
public static RuntimeIO getRuntimeIO(RuntimeScalar runtimeScalar) {
RuntimeIO fh = null;
boolean ioDebug = System.getenv("JPERL_IO_DEBUG") != null;
boolean ioDebug = IO_DEBUG;

if (ioDebug) {
System.err.println("[JPERL_IO_DEBUG] getRuntimeIO ENTRY: type=" + runtimeScalar.type +
Expand Down Expand Up @@ -1495,7 +1500,7 @@ public RuntimeScalar write(String data) {
}

RuntimeScalar result = ioHandle.write(data);
if (System.getenv("JPERL_IO_DEBUG") != null) {
if (IO_DEBUG) {
if (("main::STDOUT".equals(globName) || "main::STDERR".equals(globName)) &&
(ioHandle instanceof ClosedIOHandle || !result.getDefinedBoolean())) {
System.err.println("[JPERL_IO_DEBUG] write failed: glob=" + globName +
Expand Down
Loading