From d815d68b45248de88d4a7207674b3c911e9e8481 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sat, 7 Mar 2026 09:51:53 +0100 Subject: [PATCH 1/4] Optimize numeric conversion: 34% speedup for bitwise operations Performance improvements: - Replace INTEGER_PATTERN regex with simple mightBeInteger() char check in RuntimeScalar.getLong() and getInt() - Add fast-path in ScalarUtils.looksLikeNumber() for INTEGER/DOUBLE types - Split looksLikeNumber into inline fast-path + slow-path method Benchmark (life_bitpacked.pl 200x200 grid, 10000 generations): - Before: ~506 gen/s - After: ~680 gen/s (+34%) The regex pattern matching was creating new Matcher objects on every numeric conversion. Replacing with a simple first-character check avoids this overhead while still preventing exception costs for obviously non-numeric strings. Also adds .cognition/skills/profile-perlonjava/ with JFR profiling workflow documentation. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .cognition/skills/profile-perlonjava/SKILL.md | 124 ++++++++++++++++++ .../runtime/runtimetypes/RuntimeScalar.java | 26 ++-- .../runtime/runtimetypes/ScalarUtils.java | 56 ++++---- 3 files changed, 173 insertions(+), 33 deletions(-) create mode 100644 .cognition/skills/profile-perlonjava/SKILL.md diff --git a/.cognition/skills/profile-perlonjava/SKILL.md b/.cognition/skills/profile-perlonjava/SKILL.md new file mode 100644 index 000000000..d1a7a1665 --- /dev/null +++ b/.cognition/skills/profile-perlonjava/SKILL.md @@ -0,0 +1,124 @@ +# Profile PerlOnJava + +Profile and optimize PerlOnJava runtime performance using Java Flight Recorder. + +## When to Use + +- Investigating performance bottlenecks in Perl scripts running on PerlOnJava +- Finding optimization opportunities in the runtime +- Measuring impact of optimizations + +## Workflow + +### 1. Run with JFR Profiling + +```bash +cd /Users/fglock/projects/PerlOnJava2 + +# Profile a long-running script (adjust duration as needed) +java -XX:+FlightRecorder \ + -XX:StartFlightRecording=duration=60s,filename=profile.jfr \ + -jar target/perlonjava-3.0.0.jar [args...] +``` + +### 2. Analyze with JFR Tools + +```bash +# Path to jfr tool +JFR="$(/usr/libexec/java_home)/bin/jfr" + +# Summary of recorded events +$JFR summary profile.jfr + +# Extract execution samples (CPU hotspots) +$JFR print --events jdk.ExecutionSample profile.jfr + +# Aggregate hotspots by method (most useful) +$JFR print --events jdk.ExecutionSample profile.jfr 2>&1 | \ + grep -E "^\s+[a-z].*line:" | \ + sed 's/line:.*//' | \ + sort | uniq -c | sort -rn | head -40 +``` + +### 3. Key Hotspot Categories + +| Category | Methods to Watch | Optimization Approach | +|----------|------------------|----------------------| +| **Number parsing** | `Long.parseLong`, `Double.parseDouble`, `NumberParser.parseNumber` | Cache numeric values, avoid string→number conversions | +| **Type checking** | `ScalarUtils.looksLikeNumber`, `RuntimeScalar.getDefinedBoolean` | Fast-path for common types (INTEGER, DOUBLE) | +| **Bitwise ops** | `BitwiseOperators.*` | Ensure values stay as INTEGER type | +| **Regex** | `Pattern.match`, `Matcher.matches` | Reduce unnecessary regex checks | +| **Loop control** | `RuntimeControlFlowRegistry.checkLoopAndGetAction` | ThreadLocal overhead | +| **Array ops** | `ArrayList.grow`, `Arrays.copyOf` | Pre-size arrays, reduce allocations | + +### 4. Common Runtime Files + +| File | Purpose | +|------|---------| +| `src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java` | Scalar value representation, getLong/getDouble/getInt | +| `src/main/java/org/perlonjava/runtime/runtimetypes/ScalarUtils.java` | Utility functions like looksLikeNumber | +| `src/main/java/org/perlonjava/runtime/operators/BitwiseOperators.java` | Bitwise operations (&, |, ^, ~, <<, >>) | +| `src/main/java/org/perlonjava/runtime/operators/Operator.java` | General operators | +| `src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArray.java` | Array operations | + +### 5. Optimization Patterns + +#### Fast-path for common types +```java +public static boolean looksLikeNumber(RuntimeScalar runtimeScalar) { + // Inlined fast-path for most common numeric types + int t = runtimeScalar.type; + if (t == INTEGER || t == DOUBLE) { + return true; + } + return looksLikeNumberSlow(runtimeScalar, t); +} +``` + +#### Avoid repeated parsing +```java +// Bad: parses string every time +long val = runtimeScalar.getLong(); // calls Long.parseLong if STRING + +// Better: check type first, use cached value +if (runtimeScalar.type == INTEGER) { + long val = (int) runtimeScalar.value; // direct access +} +``` + +### 6. Benchmark Commands + +```bash +# Quick benchmark with life_bitpacked.pl +java -jar target/perlonjava-3.0.0.jar examples/life_bitpacked.pl \ + -w 200 -h 200 -g 10000 -r none + +# Multiple runs for consistency +for i in 1 2 3; do + java -jar target/perlonjava-3.0.0.jar examples/life_bitpacked.pl \ + -w 200 -h 200 -g 10000 -r none 2>&1 | grep "per second" +done +``` + +### 7. Build and Test + +```bash +# Rebuild after changes +mvn package -q -DskipTests + +# Run tests to verify correctness +mvn test -q +``` + +## Example Session + +``` +1. Identify slow script or operation +2. Profile with JFR (60s recording) +3. Aggregate hotspots by method +4. Identify top bottlenecks (parsing, type checks, etc.) +5. Implement fast-path optimization +6. Rebuild and benchmark +7. Profile again to verify improvement +8. Run tests to ensure correctness +``` diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java index 3c81ed905..218dac39c 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java @@ -31,11 +31,20 @@ public class RuntimeScalar extends RuntimeBase implements RuntimeScalarReference // Static stack to store saved "local" states of RuntimeScalar instances private static final Stack dynamicStateStack = new Stack<>(); - // Pre-compiled regex patterns for numification fast-paths - // These are used to avoid StackOverflowError from repeated Pattern.compile() calls - private static final Pattern INTEGER_PATTERN = Pattern.compile("^-?\\d+$"); + // Pre-compiled regex pattern for decimal numification fast-path + // INTEGER_PATTERN replaced with isIntegerString() for better performance private static final Pattern DECIMAL_PATTERN = Pattern.compile("^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?$"); + // Fast check if string might be a parseable integer + // Returns true if first char suggests it could be an integer (digit or minus) + // This avoids exception overhead for strings like "hello" while allowing + // Long.parseLong to handle edge cases like overflow + private static boolean mightBeInteger(String s) { + if (s.isEmpty()) return false; + char c = s.charAt(0); + return (c >= '0' && c <= '9') || c == '-'; + } + // Type map for scalar types to their corresponding enum private static final Map, Integer> typeMap = new HashMap<>(); @@ -327,14 +336,11 @@ private int getIntLarge() { String s = (String) value; if (s != null) { String t = s.trim(); - if (!t.isEmpty() && INTEGER_PATTERN.matcher(t).matches()) { + if (mightBeInteger(t)) { try { - // Parse as long first so we can handle values outside 32-bit range - // (Perl IV is commonly 64-bit). getInt() is used for array indices - // and similar contexts, which should behave like (int)getLong(). yield (int) Long.parseLong(t); } catch (NumberFormatException ignored) { - // Fall through to full numification. + // Fall through to full numification (handles "1.5", overflow, etc.) } } } @@ -501,11 +507,11 @@ public long getLong() { String s = (String) value; if (s != null) { String t = s.trim(); - if (!t.isEmpty() && INTEGER_PATTERN.matcher(t).matches()) { + if (mightBeInteger(t)) { try { yield Long.parseLong(t); } catch (NumberFormatException ignored) { - // Fall through to full numification. + // Fall through to full numification (handles "1.5", overflow, etc.) } } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarUtils.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarUtils.java index 219286360..cd7f4d7a0 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarUtils.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarUtils.java @@ -137,33 +137,43 @@ public static boolean isInteger(String str) { } public static boolean looksLikeNumber(RuntimeScalar runtimeScalar) { - switch (runtimeScalar.type) { - case INTEGER: - case DOUBLE: + // Inlined fast-path for most common numeric types (helps JIT inlining) + int t = runtimeScalar.type; + if (t == INTEGER || t == DOUBLE) { + return true; + } + return looksLikeNumberSlow(runtimeScalar, t); + } + + // Slow path for looksLikeNumber - handles strings and other types + private static boolean looksLikeNumberSlow(RuntimeScalar runtimeScalar, int t) { + if (t == STRING || t == BYTE_STRING || t == VSTRING) { + String str = runtimeScalar.toString().trim(); + if (str.isEmpty()) { + return false; + } + // Check for Inf and NaN + if (str.equalsIgnoreCase("Inf") || str.equalsIgnoreCase("Infinity") || str.equalsIgnoreCase("NaN")) { return true; - case STRING, BYTE_STRING, VSTRING: - String str = runtimeScalar.toString().trim(); - if (str.isEmpty()) { - return false; - } - // Check for Inf and NaN - if (str.equalsIgnoreCase("Inf") || str.equalsIgnoreCase("Infinity") || str.equalsIgnoreCase("NaN")) { - return true; - } - // Check for decimal (integer or float) - try { - Double.parseDouble(str); - return true; - } catch (NumberFormatException e) { - return false; - } - case BOOLEAN, DUALVAR: + } + // Fast check: if first char isn't digit, +, -, or . it's not a number + char first = str.charAt(0); + if (!((first >= '0' && first <= '9') || first == '+' || first == '-' || first == '.')) { + return false; + } + // Check for decimal (integer or float) + try { + Double.parseDouble(str); return true; - case TIED_SCALAR: - return looksLikeNumber(runtimeScalar.tiedFetch()); - default: + } catch (NumberFormatException e) { return false; + } + } else if (t == BOOLEAN || t == DUALVAR) { + return true; + } else if (t == TIED_SCALAR) { + return looksLikeNumber(runtimeScalar.tiedFetch()); } + return false; } /** From 4a9098484a41709a61f518180baac49adfefe0a2 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sat, 7 Mar 2026 10:07:34 +0100 Subject: [PATCH 2/4] Add applyCompiled fast-path to skip redundant compilerSupplier check Static apply methods already check compilerSupplier before calling instance apply. This adds applyCompiled() that skips the second check for compiled code. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../runtime/runtimetypes/RuntimeCode.java | 34 ++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 7b4e02707..fd5a98664 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -1475,8 +1475,8 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa } if (code.defined()) { - // Cast the value to RuntimeCode and call apply() - return code.apply(subroutineName, a, callContext); + // Cast the value to RuntimeCode and call applyCompiled() - compilerSupplier already checked + return code.applyCompiled(subroutineName, a, callContext); } // Does AUTOLOAD exist? @@ -1562,8 +1562,8 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa } if (code.defined()) { - // Cast the value to RuntimeCode and call apply() - return code.apply(subroutineName, a, callContext); + // Cast the value to RuntimeCode and call applyCompiled() - compilerSupplier already checked + return code.applyCompiled(subroutineName, a, callContext); } // Does AUTOLOAD exist? @@ -1795,6 +1795,32 @@ public RuntimeList apply(RuntimeArray a, int callContext) { } } + // Fast path for already-compiled code (skips compilerSupplier check) + public RuntimeList applyCompiled(String subroutineName, RuntimeArray a, int callContext) { + if (constantValue != null) { + return new RuntimeList(constantValue); + } + try { + if (this.methodHandle == null) { + // Fall back to full apply for AUTOLOAD handling + return apply(subroutineName, a, callContext); + } + if (isStatic) { + return (RuntimeList) this.methodHandle.invoke(a, callContext); + } else { + return (RuntimeList) this.methodHandle.invoke(this.codeObject, a, callContext); + } + } catch (InvocationTargetException e) { + Throwable targetException = e.getTargetException(); + if (!(targetException instanceof RuntimeException)) { + throw new RuntimeException(targetException); + } + throw (RuntimeException) targetException; + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + public RuntimeList apply(String subroutineName, RuntimeArray a, int callContext) { if (constantValue != null) { return new RuntimeList(constantValue); From d6eb1b61d35cb8b003c7fe4ed653a1f2fd7d7f64 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sat, 7 Mar 2026 10:07:37 +0100 Subject: [PATCH 3/4] Add .jfr files to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b2c7dfd1b..580e1f0a2 100644 --- a/.gitignore +++ b/.gitignore @@ -84,3 +84,4 @@ Image-ExifTool-* # Ignore xxx/ directory (temporary module staging area) xxx/ +*.jfr From 5a581d52f88d1f37e79e61eae9f219918546d6a0 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sat, 7 Mar 2026 10:08:37 +0100 Subject: [PATCH 4/4] Revert "Add applyCompiled fast-path to skip redundant compilerSupplier check" This reverts commit 4a9098484a41709a61f518180baac49adfefe0a2. --- .../runtime/runtimetypes/RuntimeCode.java | 34 +++---------------- 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index fd5a98664..7b4e02707 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -1475,8 +1475,8 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa } if (code.defined()) { - // Cast the value to RuntimeCode and call applyCompiled() - compilerSupplier already checked - return code.applyCompiled(subroutineName, a, callContext); + // Cast the value to RuntimeCode and call apply() + return code.apply(subroutineName, a, callContext); } // Does AUTOLOAD exist? @@ -1562,8 +1562,8 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa } if (code.defined()) { - // Cast the value to RuntimeCode and call applyCompiled() - compilerSupplier already checked - return code.applyCompiled(subroutineName, a, callContext); + // Cast the value to RuntimeCode and call apply() + return code.apply(subroutineName, a, callContext); } // Does AUTOLOAD exist? @@ -1795,32 +1795,6 @@ public RuntimeList apply(RuntimeArray a, int callContext) { } } - // Fast path for already-compiled code (skips compilerSupplier check) - public RuntimeList applyCompiled(String subroutineName, RuntimeArray a, int callContext) { - if (constantValue != null) { - return new RuntimeList(constantValue); - } - try { - if (this.methodHandle == null) { - // Fall back to full apply for AUTOLOAD handling - return apply(subroutineName, a, callContext); - } - if (isStatic) { - return (RuntimeList) this.methodHandle.invoke(a, callContext); - } else { - return (RuntimeList) this.methodHandle.invoke(this.codeObject, a, callContext); - } - } catch (InvocationTargetException e) { - Throwable targetException = e.getTargetException(); - if (!(targetException instanceof RuntimeException)) { - throw new RuntimeException(targetException); - } - throw (RuntimeException) targetException; - } catch (Throwable e) { - throw new RuntimeException(e); - } - } - public RuntimeList apply(String subroutineName, RuntimeArray a, int callContext) { if (constantValue != null) { return new RuntimeList(constantValue);