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/.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 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; } /**