added new performance flags, extended PIC handling to size 4, introduced fast paths for ObjFastIntRangeIterator, small-arity arguments, index access, and unary operations

This commit is contained in:
Sergey Chernov 2025-11-16 00:17:15 +01:00
parent 0798bbee9b
commit 882df67909
14 changed files with 784 additions and 93 deletions

View File

@ -10,15 +10,35 @@ Optimizations are controlled by runtime‑mutable flags in `net.sergeych.lyng.Pe
All flags are `var` and can be flipped at runtime (e.g., from tests or host apps) for A/B comparisons. All flags are `var` and can be flipped at runtime (e.g., from tests or host apps) for A/B comparisons.
### Workload presets (JVM‑first)
To simplify switching between recommended flag sets for different workloads, use `net.sergeych.lyng.PerfProfiles`:
```
val snap = PerfProfiles.apply(PerfProfiles.Preset.BENCH) // or BASELINE / BOOKS
// ... run workload ...
PerfProfiles.restore(snap) // restore previous flags
```
- BASELINE: restores platform defaults from `PerfDefaults` (good rollback point).
- BENCH: expression‑heavy micro‑bench focus (aggressive R‑value and PIC optimizations on JVM).
- BOOKS: documentation workloads (prefers simpler paths; disables some PIC/arg builder features shown neutral/negative for this load in A/B).
## Key flags ## Key flags
- `LOCAL_SLOT_PIC` — Runtime cache in `LocalVarRef` to avoid repeated name→slot lookups per frame (ON JVM default). - `LOCAL_SLOT_PIC` — Runtime cache in `LocalVarRef` to avoid repeated name→slot lookups per frame (ON JVM default).
- `EMIT_FAST_LOCAL_REFS` — Compiler emits `FastLocalVarRef` for identifiers known to be locals/params (ON JVM default). - `EMIT_FAST_LOCAL_REFS` — Compiler emits `FastLocalVarRef` for identifiers known to be locals/params (ON JVM default).
- `ARG_BUILDER` — Efficient argument building: small‑arity no‑alloc and pooled builder on JVM (ON JVM default). - `ARG_BUILDER` — Efficient argument building: small‑arity no‑alloc and pooled builder on JVM (ON JVM default).
- `ARG_SMALL_ARITY_12` — Extends small‑arity no‑alloc call paths from 0–8 to 0–12 arguments (JVM‑first exploration; OFF by default). Use for codebases with many 9–12 arg calls; A/B before enabling.
- `SKIP_ARGS_ON_NULL_RECEIVER` — Early return on optional‑null receivers before building args (semantics‑compatible). A/B only. - `SKIP_ARGS_ON_NULL_RECEIVER` — Early return on optional‑null receivers before building args (semantics‑compatible). A/B only.
- `SCOPE_POOL` — Scope frame pooling for calls (JVM, per‑thread ThreadLocal pool). ON by default on JVM; togglable at runtime. - `SCOPE_POOL` — Scope frame pooling for calls (JVM, per‑thread ThreadLocal pool). ON by default on JVM; togglable at runtime.
- `FIELD_PIC` — 2‑entry polymorphic inline cache for field reads/writes keyed by `(classId, layoutVersion)` (ON JVM default). - `FIELD_PIC` — 2‑entry polymorphic inline cache for field reads/writes keyed by `(classId, layoutVersion)` (ON JVM default).
- `METHOD_PIC` — 2‑entry PIC for instance method calls keyed by `(classId, layoutVersion)` (ON JVM default). - `METHOD_PIC` — 2‑entry PIC for instance method calls keyed by `(classId, layoutVersion)` (ON JVM default).
- `FIELD_PIC_SIZE_4` — Increases Field PIC size from 2 to 4 entries (JVM-first tuning; OFF by default). Use for sites with >2 receiver shapes.
- `METHOD_PIC_SIZE_4` — Increases Method PIC size from 2 to 4 entries (JVM-first tuning; OFF by default).
- `PIC_ADAPTIVE_2_TO_4` — Adaptive growth of Field/Method PICs from 2→4 entries per-site when miss rate >20% over ≥256 accesses (JVM-first; OFF by default).
- `INDEX_PIC` — Enables polymorphic inline cache for indexing (e.g., `a[i]`) and related fast paths. Defaults to follow `FIELD_PIC` on init; can be toggled independently.
- `INDEX_PIC_SIZE_4` — Increases Index PIC size from 2 to 4 entries (JVM-first tuning). Default: ON for JVM; OFF elsewhere by default.
- `PIC_DEBUG_COUNTERS` — Enable lightweight hit/miss counters via `PerfStats` (OFF by default). - `PIC_DEBUG_COUNTERS` — Enable lightweight hit/miss counters via `PerfStats` (OFF by default).
- `PRIMITIVE_FASTOPS` — Fast paths for `(ObjInt, ObjInt)` arithmetic/comparisons and `(ObjBool, ObjBool)` logic (ON JVM default). - `PRIMITIVE_FASTOPS` — Fast paths for `(ObjInt, ObjInt)` arithmetic/comparisons and `(ObjBool, ObjBool)` logic (ON JVM default).
- `RVAL_FASTPATH` — Bypass `ObjRecord` in pure expression evaluation via `ObjRef.evalValue` (ON JVM default, OFF elsewhere). - `RVAL_FASTPATH` — Bypass `ObjRecord` in pure expression evaluation via `ObjRef.evalValue` (ON JVM default, OFF elsewhere).
@ -28,11 +48,21 @@ See `src/commonMain/kotlin/net/sergeych/lyng/PerfFlags.kt` and `PerfDefaults.*.k
## Where optimizations apply ## Where optimizations apply
- Locals: `FastLocalVarRef`, `LocalVarRef` per‑frame cache (PIC). - Locals: `FastLocalVarRef`, `LocalVarRef` per‑frame cache (PIC).
- Calls: small‑arity zero‑alloc paths (0–8 args), pooled builder (JVM), and child frame pooling (optional). - Calls: small‑arity zero‑alloc paths (0–8 args; optionally 0–12 with `ARG_SMALL_ARITY_12`), pooled builder (JVM), and child frame pooling (optional).
- Properties/methods: Field/Method PICs with receiver shape `(classId, layoutVersion)` and handle‑aware caches. - Properties/methods: Field/Method PICs with receiver shape `(classId, layoutVersion)` and handle‑aware caches; configurable 2→4 entries under flags.
- Expressions: R‑value fast paths in hot nodes (`UnaryOpRef`, `BinaryOpRef`, `ElvisRef`, logical ops, `RangeRef`, `IndexRef` read, `FieldRef` receiver eval, `ListLiteralRef` elements, `CallRef` callee, `MethodCallRef` receiver, assignment RHS). - Expressions: R‑value fast paths in hot nodes (`UnaryOpRef`, `BinaryOpRef`, `ElvisRef`, logical ops, `RangeRef`, `IndexRef` read, `FieldRef` receiver eval, `ListLiteralRef` elements, `CallRef` callee, `MethodCallRef` receiver, assignment RHS).
- Primitives: Direct boolean/int ops where safe. - Primitives: Direct boolean/int ops where safe.
### Compiler constant folding (conservative)
- The compiler folds a safe subset of literal‑only expressions at compile time to reduce runtime work:
- Integer arithmetic: `+ - * / %` (division/modulo only when divisor ≠ 0).
- Bitwise integer ops: `& ^ | << >>`.
- Comparisons and equality for ints/strings/chars: `== != < <= > >=`.
- Boolean logic for literal booleans: `|| &&` and unary `!`.
- String concatenation of literal strings: `"a" + "b"`.
- Non‑literal operands or side‑effecting constructs are not folded.
- Semantics remain unchanged; tests verify parity.
## Running JVM micro‑benchmarks ## Running JVM micro‑benchmarks
Each benchmark prints timings with `[DEBUG_LOG]` and includes correctness assertions to prevent dead‑code elimination. Each benchmark prints timings with `[DEBUG_LOG]` and includes correctness assertions to prevent dead‑code elimination.
@ -46,8 +76,11 @@ Run individual tests to avoid multiplatform matrices:
./gradlew :lynglib:jvmTest --tests CallSplatBenchmarkTest ./gradlew :lynglib:jvmTest --tests CallSplatBenchmarkTest
./gradlew :lynglib:jvmTest --tests PicBenchmarkTest ./gradlew :lynglib:jvmTest --tests PicBenchmarkTest
./gradlew :lynglib:jvmTest --tests PicInvalidationJvmTest ./gradlew :lynglib:jvmTest --tests PicInvalidationJvmTest
./gradlew :lynglib:jvmTest --tests PicAdaptiveABTest
./gradlew :lynglib:jvmTest --tests ArithmeticBenchmarkTest ./gradlew :lynglib:jvmTest --tests ArithmeticBenchmarkTest
./gradlew :lynglib:jvmTest --tests ExpressionBenchmarkTest ./gradlew :lynglib:jvmTest --tests ExpressionBenchmarkTest
./gradlew :lynglib:jvmTest --tests IndexPicABTest
./gradlew :lynglib:jvmTest --tests IndexWritePathABTest
./gradlew :lynglib:jvmTest --tests CallPoolingBenchmarkTest ./gradlew :lynglib:jvmTest --tests CallPoolingBenchmarkTest
./gradlew :lynglib:jvmTest --tests MethodPoolingBenchmarkTest ./gradlew :lynglib:jvmTest --tests MethodPoolingBenchmarkTest
./gradlew :lynglib:jvmTest --tests MixedBenchmarkTest ./gradlew :lynglib:jvmTest --tests MixedBenchmarkTest
@ -62,6 +95,18 @@ Typical output (example):
Lower time is better. Run the same bench with a flag OFF vs ON to compare. Lower time is better. Run the same bench with a flag OFF vs ON to compare.
### Optional JFR allocation profiling (JVM)
When running end‑to‑end “book” workloads or heavier benches, you can enable JFR to capture allocation and GC details:
```
./gradlew :lynglib:jvmTest --tests BookAllocationProfileTest -Dlyng.jfr=true \
-Dlyng.profile.warmup=1 -Dlyng.profile.repeats=3 -Dlyng.profile.shuffle=true
```
- Dumps are saved to `lynglib/build/jfr_*.jfr` if the JVM supports Flight Recorder.
- The test also records GC counts/time and median time/heap deltas to `lynglib/build/book_alloc_profile.txt`.
## Toggling flags in tests ## Toggling flags in tests
Flags are mutable at runtime, e.g.: Flags are mutable at runtime, e.g.:
@ -88,17 +133,46 @@ Available counters in `PerfStats`:
- Field PIC: `fieldPicHit`, `fieldPicMiss`, `fieldPicSetHit`, `fieldPicSetMiss` - Field PIC: `fieldPicHit`, `fieldPicMiss`, `fieldPicSetHit`, `fieldPicSetMiss`
- Method PIC: `methodPicHit`, `methodPicMiss` - Method PIC: `methodPicHit`, `methodPicMiss`
- Index PIC: `indexPicHit`, `indexPicMiss`
- Locals: `localVarPicHit`, `localVarPicMiss`, `fastLocalHit`, `fastLocalMiss` - Locals: `localVarPicHit`, `localVarPicMiss`, `fastLocalHit`, `fastLocalMiss`
- Primitive ops: `primitiveFastOpsHit` - Primitive ops: `primitiveFastOpsHit`
Print a summary at the end of a bench/test as needed. Remember to turn counters OFF after the test. Print a summary at the end of a bench/test as needed. Remember to turn counters OFF after the test.
## A/B scenarios and guidance (JVM)
### Adaptive PIC (fields/methods)
- Flags: `FIELD_PIC=true`, `METHOD_PIC=true`, `FIELD_PIC_SIZE_4=false`, `METHOD_PIC_SIZE_4=false`, toggle `PIC_ADAPTIVE_2_TO_4` OFF vs ON.
- Benchmarks: `PicBenchmarkTest`, `MixedBenchmarkTest`, `PicAdaptiveABTest`.
- Expect wins at sites with >2 receiver shapes; counters should show fewer misses with adaptivity ON.
### Index PIC and size
- Flags: toggle `INDEX_PIC` OFF vs ON; then `INDEX_PIC_SIZE_4` OFF vs ON.
- Benchmarks: `ExpressionBenchmarkTest` (list indexing) and `IndexPicABTest` (string/map indexing).
- Expect wins when the same index shape recurs; counters should show higher `indexPicHit`.
### Index WRITE paths (Map and List)
- Flags: toggle `INDEX_PIC` OFF vs ON; then `INDEX_PIC_SIZE_4` OFF vs ON.
- Benchmark: `IndexWritePathABTest` (Map[String] put, List[Int] set) — writes results to `lynglib/build/index_write_ab_results.txt`.
- Direct fast‑paths are used on R‑value paths where safe and semantics‑preserving (e.g., optional‑chaining no‑ops on null receivers; bounds exceptions unchanged).
## Guidance per flag (JVM) ## Guidance per flag (JVM)
- Keep `RVAL_FASTPATH = true` unless debugging a suspected expression‑semantics issue. - Keep `RVAL_FASTPATH = true` unless debugging a suspected expression‑semantics issue.
- Use `SCOPE_POOL = true` only for benchmarks or once pooling passes the deep stress tests and broader validation; currently OFF by default. - Use `SCOPE_POOL = true` only for benchmarks or once pooling passes the deep stress tests and broader validation; currently OFF by default.
- `FIELD_PIC` and `METHOD_PIC` should remain ON; they are validated with invalidation tests. - `FIELD_PIC` and `METHOD_PIC` should remain ON; they are validated with invalidation tests.
- Consider enabling `FIELD_PIC_SIZE_4`/`METHOD_PIC_SIZE_4` for sites with 3–4 receiver shapes; measure first.
- `PIC_ADAPTIVE_2_TO_4` is useful on polymorphic sites and may outperform fixed size 2 on mixed-shape workloads. Validate with `PicAdaptiveABTest`.
- `INDEX_PIC` is generally beneficial on JVM; leave ON when measuring index‑heavy workloads.
- `INDEX_PIC_SIZE_4` is ON by default on JVM as A/B showed consistent wins on String[Int] and Map[String] workloads. You can disable it by setting `PerfFlags.INDEX_PIC_SIZE_4 = false` if needed.
- `ARG_BUILDER` should remain ON; switch OFF only to get a baseline. - `ARG_BUILDER` should remain ON; switch OFF only to get a baseline.
- `ARG_SMALL_ARITY_12` is experimental and OFF by default. Enable it only if your workload frequently calls functions with 9–12 arguments and A/B shows consistent wins.
### Workload‑specific recommendations (JVM)
- “Books”/documentation loads (BookTest): prefer simpler paths; in A/B these often benefit from the BOOKS preset (e.g., `ARG_BUILDER=false`, `SCOPE_POOL=false`, `INDEX_PIC=false`). Use `PerfProfiles.apply(PerfProfiles.Preset.BOOKS)` before the run and `restore(...)` after.
- Expression‑heavy benches: use the BENCH preset (PICs and R‑value fast‑paths enabled, `INDEX_PIC_SIZE_4=true`).
- Always verify with local A/B on your environment; rollback is a flag flip or applying BASELINE.
## Notes on correctness & safety ## Notes on correctness & safety
@ -111,6 +185,12 @@ Print a summary at the end of a bench/test as needed. Remember to turn counters
- Non‑JVM defaults keep `RVAL_FASTPATH=false` for now; other low‑risk flags may be ON. - Non‑JVM defaults keep `RVAL_FASTPATH=false` for now; other low‑risk flags may be ON.
- Once JVM path is fully validated and measured, add lightweight benches for JS/Wasm/Native and enable flags incrementally. - Once JVM path is fully validated and measured, add lightweight benches for JS/Wasm/Native and enable flags incrementally.
## Range fast iteration (experimental)
- Flag: `RANGE_FAST_ITER` (default OFF).
- When enabled and applicable, simple ascending integer ranges (`0..n`, `0..<n`) use a specialized non‑allocating iterator (`ObjFastIntRangeIterator`).
- Benchmark: `RangeIterationBenchmarkTest` records OFF/ON timings for inclusive, exclusive, reversed, negative, and empty ranges. Semantics are preserved; non‑int or complex ranges fall back to the generic iterator.
## Troubleshooting ## Troubleshooting
- If a benchmark shows regressions, flip related flags OFF to isolate the source (e.g., `ARG_BUILDER`, `RVAL_FASTPATH`, `FIELD_PIC`, `METHOD_PIC`). - If a benchmark shows regressions, flip related flags OFF to isolate the source (e.g., `ARG_BUILDER`, `RVAL_FASTPATH`, `FIELD_PIC`, `METHOD_PIC`).

View File

@ -27,6 +27,11 @@ actual object PerfDefaults {
actual val FIELD_PIC: Boolean = true actual val FIELD_PIC: Boolean = true
actual val METHOD_PIC: Boolean = true actual val METHOD_PIC: Boolean = true
actual val FIELD_PIC_SIZE_4: Boolean = false
actual val METHOD_PIC_SIZE_4: Boolean = false
actual val PIC_ADAPTIVE_2_TO_4: Boolean = false
actual val PIC_ADAPTIVE_METHODS_ONLY: Boolean = false
actual val PIC_ADAPTIVE_HEURISTIC: Boolean = false
actual val PIC_DEBUG_COUNTERS: Boolean = false actual val PIC_DEBUG_COUNTERS: Boolean = false
@ -34,4 +39,7 @@ actual object PerfDefaults {
actual val RVAL_FASTPATH: Boolean = true actual val RVAL_FASTPATH: Boolean = true
// Regex caching aligns with JVM behavior on Android (Dalvik/ART) // Regex caching aligns with JVM behavior on Android (Dalvik/ART)
actual val REGEX_CACHE: Boolean = true actual val REGEX_CACHE: Boolean = true
actual val ARG_SMALL_ARITY_12: Boolean = false
actual val INDEX_PIC_SIZE_4: Boolean = false
actual val RANGE_FAST_ITER: Boolean = false
} }

View File

@ -26,12 +26,13 @@ import net.sergeych.lyng.obj.ObjList
suspend fun Collection<ParsedArgument>.toArguments(scope: Scope, tailBlockMode: Boolean): Arguments { suspend fun Collection<ParsedArgument>.toArguments(scope: Scope, tailBlockMode: Boolean): Arguments {
// Small-arity fast path (no splats) to reduce allocations // Small-arity fast path (no splats) to reduce allocations
if (PerfFlags.ARG_BUILDER) { if (PerfFlags.ARG_BUILDER) {
val limit = if (PerfFlags.ARG_SMALL_ARITY_12) 12 else 8
var hasSplat = false var hasSplat = false
var count = 0 var count = 0
for (pa in this) { for (pa in this) {
if (pa.isSplat) { hasSplat = true; break } if (pa.isSplat) { hasSplat = true; break }
count++ count++
if (count > 8) break if (count > limit) break
} }
if (!hasSplat && count == this.size) { if (!hasSplat && count == this.size) {
val quick = when (count) { val quick = when (count) {
@ -93,6 +94,60 @@ import net.sergeych.lyng.obj.ObjList
val a7 = this.elementAt(7).value.execute(scope) val a7 = this.elementAt(7).value.execute(scope)
Arguments(listOf(a0, a1, a2, a3, a4, a5, a6, a7), tailBlockMode) Arguments(listOf(a0, a1, a2, a3, a4, a5, a6, a7), tailBlockMode)
} }
9 -> if (PerfFlags.ARG_SMALL_ARITY_12) {
val a0 = this.elementAt(0).value.execute(scope)
val a1 = this.elementAt(1).value.execute(scope)
val a2 = this.elementAt(2).value.execute(scope)
val a3 = this.elementAt(3).value.execute(scope)
val a4 = this.elementAt(4).value.execute(scope)
val a5 = this.elementAt(5).value.execute(scope)
val a6 = this.elementAt(6).value.execute(scope)
val a7 = this.elementAt(7).value.execute(scope)
val a8 = this.elementAt(8).value.execute(scope)
Arguments(listOf(a0, a1, a2, a3, a4, a5, a6, a7, a8), tailBlockMode)
} else null
10 -> if (PerfFlags.ARG_SMALL_ARITY_12) {
val a0 = this.elementAt(0).value.execute(scope)
val a1 = this.elementAt(1).value.execute(scope)
val a2 = this.elementAt(2).value.execute(scope)
val a3 = this.elementAt(3).value.execute(scope)
val a4 = this.elementAt(4).value.execute(scope)
val a5 = this.elementAt(5).value.execute(scope)
val a6 = this.elementAt(6).value.execute(scope)
val a7 = this.elementAt(7).value.execute(scope)
val a8 = this.elementAt(8).value.execute(scope)
val a9 = this.elementAt(9).value.execute(scope)
Arguments(listOf(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9), tailBlockMode)
} else null
11 -> if (PerfFlags.ARG_SMALL_ARITY_12) {
val a0 = this.elementAt(0).value.execute(scope)
val a1 = this.elementAt(1).value.execute(scope)
val a2 = this.elementAt(2).value.execute(scope)
val a3 = this.elementAt(3).value.execute(scope)
val a4 = this.elementAt(4).value.execute(scope)
val a5 = this.elementAt(5).value.execute(scope)
val a6 = this.elementAt(6).value.execute(scope)
val a7 = this.elementAt(7).value.execute(scope)
val a8 = this.elementAt(8).value.execute(scope)
val a9 = this.elementAt(9).value.execute(scope)
val a10 = this.elementAt(10).value.execute(scope)
Arguments(listOf(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10), tailBlockMode)
} else null
12 -> if (PerfFlags.ARG_SMALL_ARITY_12) {
val a0 = this.elementAt(0).value.execute(scope)
val a1 = this.elementAt(1).value.execute(scope)
val a2 = this.elementAt(2).value.execute(scope)
val a3 = this.elementAt(3).value.execute(scope)
val a4 = this.elementAt(4).value.execute(scope)
val a5 = this.elementAt(5).value.execute(scope)
val a6 = this.elementAt(6).value.execute(scope)
val a7 = this.elementAt(7).value.execute(scope)
val a8 = this.elementAt(8).value.execute(scope)
val a9 = this.elementAt(9).value.execute(scope)
val a10 = this.elementAt(10).value.execute(scope)
val a11 = this.elementAt(11).value.execute(scope)
Arguments(listOf(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11), tailBlockMode)
} else null
else -> null else -> null
} }
if (quick != null) return quick if (quick != null) return quick

View File

@ -1845,6 +1845,92 @@ class Compiler(
} }
private var lastPriority = 0 private var lastPriority = 0
// Helpers for conservative constant folding (literal-only). Only pure, side-effect-free ops.
private fun constOf(r: ObjRef): Obj? = (r as? ConstRef)?.constValue
private fun foldBinary(op: BinOp, aRef: ObjRef, bRef: ObjRef): Obj? {
val a = constOf(aRef) ?: return null
val b = constOf(bRef) ?: return null
return when (op) {
// Boolean logic
BinOp.OR -> if (a is ObjBool && b is ObjBool) if (a.value || b.value) ObjTrue else ObjFalse else null
BinOp.AND -> if (a is ObjBool && b is ObjBool) if (a.value && b.value) ObjTrue else ObjFalse else null
// Equality and comparisons for ints/strings/chars
BinOp.EQ -> when {
a is ObjInt && b is ObjInt -> if (a.value == b.value) ObjTrue else ObjFalse
a is ObjString && b is ObjString -> if (a.value == b.value) ObjTrue else ObjFalse
a is ObjChar && b is ObjChar -> if (a.value == b.value) ObjTrue else ObjFalse
else -> null
}
BinOp.NEQ -> when {
a is ObjInt && b is ObjInt -> if (a.value != b.value) ObjTrue else ObjFalse
a is ObjString && b is ObjString -> if (a.value != b.value) ObjTrue else ObjFalse
a is ObjChar && b is ObjChar -> if (a.value != b.value) ObjTrue else ObjFalse
else -> null
}
BinOp.LT -> when {
a is ObjInt && b is ObjInt -> if (a.value < b.value) ObjTrue else ObjFalse
a is ObjString && b is ObjString -> if (a.value < b.value) ObjTrue else ObjFalse
a is ObjChar && b is ObjChar -> if (a.value < b.value) ObjTrue else ObjFalse
else -> null
}
BinOp.LTE -> when {
a is ObjInt && b is ObjInt -> if (a.value <= b.value) ObjTrue else ObjFalse
a is ObjString && b is ObjString -> if (a.value <= b.value) ObjTrue else ObjFalse
a is ObjChar && b is ObjChar -> if (a.value <= b.value) ObjTrue else ObjFalse
else -> null
}
BinOp.GT -> when {
a is ObjInt && b is ObjInt -> if (a.value > b.value) ObjTrue else ObjFalse
a is ObjString && b is ObjString -> if (a.value > b.value) ObjTrue else ObjFalse
a is ObjChar && b is ObjChar -> if (a.value > b.value) ObjTrue else ObjFalse
else -> null
}
BinOp.GTE -> when {
a is ObjInt && b is ObjInt -> if (a.value >= b.value) ObjTrue else ObjFalse
a is ObjString && b is ObjString -> if (a.value >= b.value) ObjTrue else ObjFalse
a is ObjChar && b is ObjChar -> if (a.value >= b.value) ObjTrue else ObjFalse
else -> null
}
// Arithmetic for ints only (keep semantics simple at compile time)
BinOp.PLUS -> when {
a is ObjInt && b is ObjInt -> ObjInt(a.value + b.value)
a is ObjString && b is ObjString -> ObjString(a.value + b.value)
else -> null
}
BinOp.MINUS -> if (a is ObjInt && b is ObjInt) ObjInt(a.value - b.value) else null
BinOp.STAR -> if (a is ObjInt && b is ObjInt) ObjInt(a.value * b.value) else null
BinOp.SLASH -> if (a is ObjInt && b is ObjInt && b.value != 0L) ObjInt(a.value / b.value) else null
BinOp.PERCENT -> if (a is ObjInt && b is ObjInt && b.value != 0L) ObjInt(a.value % b.value) else null
// Bitwise for ints
BinOp.BAND -> if (a is ObjInt && b is ObjInt) ObjInt(a.value and b.value) else null
BinOp.BXOR -> if (a is ObjInt && b is ObjInt) ObjInt(a.value xor b.value) else null
BinOp.BOR -> if (a is ObjInt && b is ObjInt) ObjInt(a.value or b.value) else null
BinOp.SHL -> if (a is ObjInt && b is ObjInt) ObjInt(a.value shl (b.value.toInt() and 63)) else null
BinOp.SHR -> if (a is ObjInt && b is ObjInt) ObjInt(a.value shr (b.value.toInt() and 63)) else null
// Non-folded / side-effecting or type-dependent ops
BinOp.EQARROW, BinOp.REF_EQ, BinOp.REF_NEQ, BinOp.MATCH, BinOp.NOTMATCH,
BinOp.IN, BinOp.NOTIN, BinOp.IS, BinOp.NOTIS, BinOp.SHUTTLE -> null
}
}
private fun foldUnary(op: UnaryOp, aRef: ObjRef): Obj? {
val a = constOf(aRef) ?: return null
return when (op) {
UnaryOp.NOT -> if (a is ObjBool) if (!a.value) ObjTrue else ObjFalse else null
UnaryOp.NEGATE -> when (a) {
is ObjInt -> ObjInt(-a.value)
is ObjReal -> ObjReal(-a.value)
else -> null
}
UnaryOp.BITNOT -> if (a is ObjInt) ObjInt(a.value.inv()) else null
}
}
val allOps = listOf( val allOps = listOf(
// assignments, lowest priority // assignments, lowest priority
Operator(Token.Type.ASSIGN, lastPriority) { pos, a, b -> Operator(Token.Type.ASSIGN, lastPriority) { pos, a, b ->
@ -1867,6 +1953,7 @@ class Compiler(
}, },
// logical 1 // logical 1
Operator(Token.Type.OR, ++lastPriority) { _, a, b -> Operator(Token.Type.OR, ++lastPriority) { _, a, b ->
foldBinary(BinOp.OR, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
LogicalOrRef(a, b) LogicalOrRef(a, b)
}, },
// logical 2 // logical 2
@ -1875,12 +1962,15 @@ class Compiler(
}, },
// bitwise or/xor/and (tighter than &&, looser than equality) // bitwise or/xor/and (tighter than &&, looser than equality)
Operator(Token.Type.BITOR, ++lastPriority) { _, a, b -> Operator(Token.Type.BITOR, ++lastPriority) { _, a, b ->
foldBinary(BinOp.BOR, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.BOR, a, b) BinaryOpRef(BinOp.BOR, a, b)
}, },
Operator(Token.Type.BITXOR, ++lastPriority) { _, a, b -> Operator(Token.Type.BITXOR, ++lastPriority) { _, a, b ->
foldBinary(BinOp.BXOR, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.BXOR, a, b) BinaryOpRef(BinOp.BXOR, a, b)
}, },
Operator(Token.Type.BITAND, ++lastPriority) { _, a, b -> Operator(Token.Type.BITAND, ++lastPriority) { _, a, b ->
foldBinary(BinOp.BAND, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.BAND, a, b) BinaryOpRef(BinOp.BAND, a, b)
}, },
// equality/not equality and related // equality/not equality and related
@ -1888,9 +1978,11 @@ class Compiler(
BinaryOpRef(BinOp.EQARROW, a, b) BinaryOpRef(BinOp.EQARROW, a, b)
}, },
Operator(Token.Type.EQ, ++lastPriority) { _, a, b -> Operator(Token.Type.EQ, ++lastPriority) { _, a, b ->
foldBinary(BinOp.EQ, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.EQ, a, b) BinaryOpRef(BinOp.EQ, a, b)
}, },
Operator(Token.Type.NEQ, lastPriority) { _, a, b -> Operator(Token.Type.NEQ, lastPriority) { _, a, b ->
foldBinary(BinOp.NEQ, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.NEQ, a, b) BinaryOpRef(BinOp.NEQ, a, b)
}, },
Operator(Token.Type.REF_EQ, lastPriority) { _, a, b -> Operator(Token.Type.REF_EQ, lastPriority) { _, a, b ->
@ -1907,15 +1999,19 @@ class Compiler(
}, },
// relational <=,... // relational <=,...
Operator(Token.Type.LTE, ++lastPriority) { _, a, b -> Operator(Token.Type.LTE, ++lastPriority) { _, a, b ->
foldBinary(BinOp.LTE, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.LTE, a, b) BinaryOpRef(BinOp.LTE, a, b)
}, },
Operator(Token.Type.LT, lastPriority) { _, a, b -> Operator(Token.Type.LT, lastPriority) { _, a, b ->
foldBinary(BinOp.LT, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.LT, a, b) BinaryOpRef(BinOp.LT, a, b)
}, },
Operator(Token.Type.GTE, lastPriority) { _, a, b -> Operator(Token.Type.GTE, lastPriority) { _, a, b ->
foldBinary(BinOp.GTE, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.GTE, a, b) BinaryOpRef(BinOp.GTE, a, b)
}, },
Operator(Token.Type.GT, lastPriority) { _, a, b -> Operator(Token.Type.GT, lastPriority) { _, a, b ->
foldBinary(BinOp.GT, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.GT, a, b) BinaryOpRef(BinOp.GT, a, b)
}, },
// in, is: // in, is:
@ -1942,25 +2038,32 @@ class Compiler(
}, },
// shifts (tighter than shuttle, looser than +/-) // shifts (tighter than shuttle, looser than +/-)
Operator(Token.Type.SHL, ++lastPriority) { _, a, b -> Operator(Token.Type.SHL, ++lastPriority) { _, a, b ->
foldBinary(BinOp.SHL, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.SHL, a, b) BinaryOpRef(BinOp.SHL, a, b)
}, },
Operator(Token.Type.SHR, lastPriority) { _, a, b -> Operator(Token.Type.SHR, lastPriority) { _, a, b ->
foldBinary(BinOp.SHR, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.SHR, a, b) BinaryOpRef(BinOp.SHR, a, b)
}, },
// arithmetic // arithmetic
Operator(Token.Type.PLUS, ++lastPriority) { _, a, b -> Operator(Token.Type.PLUS, ++lastPriority) { _, a, b ->
foldBinary(BinOp.PLUS, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.PLUS, a, b) BinaryOpRef(BinOp.PLUS, a, b)
}, },
Operator(Token.Type.MINUS, lastPriority) { _, a, b -> Operator(Token.Type.MINUS, lastPriority) { _, a, b ->
foldBinary(BinOp.MINUS, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.MINUS, a, b) BinaryOpRef(BinOp.MINUS, a, b)
}, },
Operator(Token.Type.STAR, ++lastPriority) { _, a, b -> Operator(Token.Type.STAR, ++lastPriority) { _, a, b ->
foldBinary(BinOp.STAR, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.STAR, a, b) BinaryOpRef(BinOp.STAR, a, b)
}, },
Operator(Token.Type.SLASH, lastPriority) { _, a, b -> Operator(Token.Type.SLASH, lastPriority) { _, a, b ->
foldBinary(BinOp.SLASH, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.SLASH, a, b) BinaryOpRef(BinOp.SLASH, a, b)
}, },
Operator(Token.Type.PERCENT, lastPriority) { _, a, b -> Operator(Token.Type.PERCENT, lastPriority) { _, a, b ->
foldBinary(BinOp.PERCENT, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.PERCENT, a, b) BinaryOpRef(BinOp.PERCENT, a, b)
}, },
) )

View File

@ -32,6 +32,14 @@ expect object PerfDefaults {
val FIELD_PIC: Boolean val FIELD_PIC: Boolean
val METHOD_PIC: Boolean val METHOD_PIC: Boolean
// Optional larger PICs and adaptivity (JVM-first)
val FIELD_PIC_SIZE_4: Boolean
val METHOD_PIC_SIZE_4: Boolean
val PIC_ADAPTIVE_2_TO_4: Boolean
// Enable adaptivity only for methods (independent of fields)
val PIC_ADAPTIVE_METHODS_ONLY: Boolean
// Enable per-site heuristic to revert/avoid promotion when it shows no benefit
val PIC_ADAPTIVE_HEURISTIC: Boolean
val PIC_DEBUG_COUNTERS: Boolean val PIC_DEBUG_COUNTERS: Boolean
@ -40,4 +48,13 @@ expect object PerfDefaults {
// Regex caching (JVM-first): small LRU for compiled patterns // Regex caching (JVM-first): small LRU for compiled patterns
val REGEX_CACHE: Boolean val REGEX_CACHE: Boolean
// Argument builder extended arity (JVM-first exploration)
val ARG_SMALL_ARITY_12: Boolean
// Index PIC sizing (JVM-first)
val INDEX_PIC_SIZE_4: Boolean
// Specialized non-allocating integer range iteration in hot loops
val RANGE_FAST_ITER: Boolean
} }

View File

@ -30,6 +30,8 @@ object PerfFlags {
// Enable more efficient argument building and bulk-copy for splats // Enable more efficient argument building and bulk-copy for splats
var ARG_BUILDER: Boolean = PerfDefaults.ARG_BUILDER var ARG_BUILDER: Boolean = PerfDefaults.ARG_BUILDER
// Extend small-arity no-alloc argument paths to 9..12 (JVM-first; default via PerfDefaults)
var ARG_SMALL_ARITY_12: Boolean = PerfDefaults.ARG_SMALL_ARITY_12
// Allow early-return in optional calls before building args (semantics-compatible). Present for A/B only. // Allow early-return in optional calls before building args (semantics-compatible). Present for A/B only.
var SKIP_ARGS_ON_NULL_RECEIVER: Boolean = PerfDefaults.SKIP_ARGS_ON_NULL_RECEIVER var SKIP_ARGS_ON_NULL_RECEIVER: Boolean = PerfDefaults.SKIP_ARGS_ON_NULL_RECEIVER
// Enable pooling of Scope frames for calls (may be JVM-only optimization) // Enable pooling of Scope frames for calls (may be JVM-only optimization)
@ -39,6 +41,23 @@ object PerfFlags {
var FIELD_PIC: Boolean = PerfDefaults.FIELD_PIC var FIELD_PIC: Boolean = PerfDefaults.FIELD_PIC
var METHOD_PIC: Boolean = PerfDefaults.METHOD_PIC var METHOD_PIC: Boolean = PerfDefaults.METHOD_PIC
// Optional: expand PIC size for fields/methods from 2 to 4 entries (JVM-first tuning)
// Initialized from platform defaults; still runtime-togglable.
var FIELD_PIC_SIZE_4: Boolean = PerfDefaults.FIELD_PIC_SIZE_4
var METHOD_PIC_SIZE_4: Boolean = PerfDefaults.METHOD_PIC_SIZE_4
// Adaptive growth from 2→4 entries per-site for field/method PICs when polymorphism is high (JVM-first)
var PIC_ADAPTIVE_2_TO_4: Boolean = PerfDefaults.PIC_ADAPTIVE_2_TO_4
// Adaptive growth for methods only (independent of fields)
var PIC_ADAPTIVE_METHODS_ONLY: Boolean = PerfDefaults.PIC_ADAPTIVE_METHODS_ONLY
// Heuristic to avoid or revert promotion when it shows no benefit (experimental)
var PIC_ADAPTIVE_HEURISTIC: Boolean = PerfDefaults.PIC_ADAPTIVE_HEURISTIC
// Index PIC/fast paths (JVM-first). By default follow FIELD_PIC enablement to avoid extra flags churn.
// Host apps/tests can flip independently if needed.
var INDEX_PIC: Boolean = FIELD_PIC
// Optional 4-entry PIC for IndexRef (JVM-first tuning); initialized from platform defaults
var INDEX_PIC_SIZE_4: Boolean = PerfDefaults.INDEX_PIC_SIZE_4
// Debug/observability for PICs and fast paths (JVM-first) // Debug/observability for PICs and fast paths (JVM-first)
var PIC_DEBUG_COUNTERS: Boolean = PerfDefaults.PIC_DEBUG_COUNTERS var PIC_DEBUG_COUNTERS: Boolean = PerfDefaults.PIC_DEBUG_COUNTERS
@ -50,4 +69,7 @@ object PerfFlags {
// Regex: enable small LRU cache for compiled patterns (JVM-first usage) // Regex: enable small LRU cache for compiled patterns (JVM-first usage)
var REGEX_CACHE: Boolean = PerfDefaults.REGEX_CACHE var REGEX_CACHE: Boolean = PerfDefaults.REGEX_CACHE
// Specialized non-allocating integer range iteration in hot loops
var RANGE_FAST_ITER: Boolean = PerfDefaults.RANGE_FAST_ITER
} }

View File

@ -41,6 +41,10 @@ object PerfStats {
// Primitive fast ops // Primitive fast ops
var primitiveFastOpsHit: Long = 0 var primitiveFastOpsHit: Long = 0
// Index PIC
var indexPicHit: Long = 0
var indexPicMiss: Long = 0
fun resetAll() { fun resetAll() {
fieldPicHit = 0 fieldPicHit = 0
fieldPicMiss = 0 fieldPicMiss = 0
@ -53,5 +57,7 @@ object PerfStats {
fastLocalHit = 0 fastLocalHit = 0
fastLocalMiss = 0 fastLocalMiss = 0
primitiveFastOpsHit = 0 primitiveFastOpsHit = 0
indexPicHit = 0
indexPicMiss = 0
} }
} }

View File

@ -161,6 +161,18 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
} }
addFn("iterator") { addFn("iterator") {
val self = thisAs<ObjRange>() val self = thisAs<ObjRange>()
if (net.sergeych.lyng.PerfFlags.RANGE_FAST_ITER) {
val s = self.start
val e = self.end
if (s is ObjInt && e is ObjInt) {
val start = s.value.toInt()
val endExclusive = (if (self.isEndInclusive) e.value.toInt() + 1 else e.value.toInt())
// Only for ascending simple ranges; fall back otherwise
if (start <= endExclusive) {
return@addFn ObjFastIntRangeIterator(start, endExclusive)
}
}
}
ObjRangeIterator(self).apply { init() } ObjRangeIterator(self).apply { init() }
} }
} }

View File

@ -17,6 +17,7 @@
package net.sergeych.lyng.obj package net.sergeych.lyng.obj
import net.sergeych.lyng.PerfFlags
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
class ObjRangeIterator(val self: ObjRange) : Obj() { class ObjRangeIterator(val self: ObjRange) : Obj() {
@ -65,4 +66,28 @@ class ObjRangeIterator(val self: ObjRange) : Obj() {
} }
} }
} }
}
/**
* Fast iterator for simple integer ranges (step +1). Returned only when
* [PerfFlags.RANGE_FAST_ITER] is enabled and the range is an ascending int range.
*/
class ObjFastIntRangeIterator(private val start: Int, private val endExclusive: Int) : Obj() {
private var cur: Int = start
override val objClass: ObjClass = type
fun hasNext(): Boolean = cur < endExclusive
fun next(scope: Scope): Obj =
if (cur < endExclusive) ObjInt(cur++.toLong())
else scope.raiseError(ObjIterationFinishedException(scope))
companion object {
val type = ObjClass("FastIntRangeIterator", ObjIterator).apply {
addFn("hasNext") { thisAs<ObjFastIntRangeIterator>().hasNext().toObj() }
addFn("next") { thisAs<ObjFastIntRangeIterator>().next(this) }
}
}
} }

View File

@ -62,7 +62,23 @@ enum class BinOp {
/** R-value reference for unary operations. */ /** R-value reference for unary operations. */
class UnaryOpRef(private val op: UnaryOp, private val a: ObjRef) : ObjRef { class UnaryOpRef(private val op: UnaryOp, private val a: ObjRef) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
val v = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) a.evalValue(scope) else a.get(scope).value val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH
val v = if (fastRval) a.evalValue(scope) else a.get(scope).value
if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) {
val rFast: Obj? = when (op) {
UnaryOp.NOT -> if (v is ObjBool) if (!v.value) ObjTrue else ObjFalse else null
UnaryOp.NEGATE -> when (v) {
is ObjInt -> ObjInt(-v.value)
is ObjReal -> ObjReal(-v.value)
else -> null
}
UnaryOp.BITNOT -> if (v is ObjInt) ObjInt(v.value.inv()) else null
}
if (rFast != null) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.primitiveFastOpsHit++
return rFast.asReadonly
}
}
val r = when (op) { val r = when (op) {
UnaryOp.NOT -> v.logicalNot(scope) UnaryOp.NOT -> v.logicalNot(scope)
UnaryOp.NEGATE -> v.negate(scope) UnaryOp.NEGATE -> v.negate(scope)
@ -303,8 +319,9 @@ class IncDecRef(
/** Elvis operator reference: a ?: b */ /** Elvis operator reference: a ?: b */
class ElvisRef(private val left: ObjRef, private val right: ObjRef) : ObjRef { class ElvisRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
val a = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) left.evalValue(scope) else left.get(scope).value val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH
val r = if (a != ObjNull) a else if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) right.evalValue(scope) else right.get(scope).value val a = if (fastRval) left.evalValue(scope) else left.get(scope).value
val r = if (a != ObjNull) a else if (fastRval) right.evalValue(scope) else right.get(scope).value
return r.asReadonly return r.asReadonly
} }
} }
@ -312,10 +329,12 @@ class ElvisRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
/** Logical OR with short-circuit: a || b */ /** Logical OR with short-circuit: a || b */
class LogicalOrRef(private val left: ObjRef, private val right: ObjRef) : ObjRef { class LogicalOrRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
val a = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) left.evalValue(scope) else left.get(scope).value val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH
val fastPrim = net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS
val a = if (fastRval) left.evalValue(scope) else left.get(scope).value
if ((a as? ObjBool)?.value == true) return ObjTrue.asReadonly if ((a as? ObjBool)?.value == true) return ObjTrue.asReadonly
val b = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) right.evalValue(scope) else right.get(scope).value val b = if (fastRval) right.evalValue(scope) else right.get(scope).value
if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) { if (fastPrim) {
if (a is ObjBool && b is ObjBool) { if (a is ObjBool && b is ObjBool) {
return if (a.value || b.value) ObjTrue.asReadonly else ObjFalse.asReadonly return if (a.value || b.value) ObjTrue.asReadonly else ObjFalse.asReadonly
} }
@ -347,6 +366,8 @@ class LogicalAndRef(private val left: ObjRef, private val right: ObjRef) : ObjRe
*/ */
class ConstRef(private val record: ObjRecord) : ObjRef { class ConstRef(private val record: ObjRecord) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord = record override suspend fun get(scope: Scope): ObjRecord = record
// Expose constant value for compiler constant folding (pure, read-only)
val constValue: Obj get() = record.value
} }
/** /**
@ -373,6 +394,48 @@ class FieldRef(
// Transient per-step cache to optimize read-then-write sequences within the same frame // Transient per-step cache to optimize read-then-write sequences within the same frame
private var tKey: Long = 0L; private var tVer: Int = -1; private var tFrameId: Long = -1L; private var tRecord: ObjRecord? = null private var tKey: Long = 0L; private var tVer: Int = -1; private var tFrameId: Long = -1L; private var tRecord: ObjRecord? = null
// Adaptive PIC (2→4) for reads/writes
private var rAccesses: Int = 0; private var rMisses: Int = 0; private var rPromotedTo4: Boolean = false
private var wAccesses: Int = 0; private var wMisses: Int = 0; private var wPromotedTo4: Boolean = false
private inline fun size4ReadsEnabled(): Boolean =
net.sergeych.lyng.PerfFlags.FIELD_PIC_SIZE_4 ||
(net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_2_TO_4 && rPromotedTo4)
private inline fun size4WritesEnabled(): Boolean =
net.sergeych.lyng.PerfFlags.FIELD_PIC_SIZE_4 ||
(net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_2_TO_4 && wPromotedTo4)
private fun noteReadHit() {
if (!net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_2_TO_4) return
val a = (rAccesses + 1).coerceAtMost(1_000_000)
rAccesses = a
}
private fun noteReadMiss() {
if (!net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_2_TO_4) return
val a = (rAccesses + 1).coerceAtMost(1_000_000)
rAccesses = a
rMisses = (rMisses + 1).coerceAtMost(1_000_000)
if (!rPromotedTo4 && a >= 256) {
// promote if miss rate > 20%
if (rMisses * 100 / a > 20) rPromotedTo4 = true
// reset counters after decision
rAccesses = 0; rMisses = 0
}
}
private fun noteWriteHit() {
if (!net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_2_TO_4) return
val a = (wAccesses + 1).coerceAtMost(1_000_000)
wAccesses = a
}
private fun noteWriteMiss() {
if (!net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_2_TO_4) return
val a = (wAccesses + 1).coerceAtMost(1_000_000)
wAccesses = a
wMisses = (wMisses + 1).coerceAtMost(1_000_000)
if (!wPromotedTo4 && a >= 256) {
if (wMisses * 100 / a > 20) wPromotedTo4 = true
wAccesses = 0; wMisses = 0
}
}
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH
val fieldPic = net.sergeych.lyng.PerfFlags.FIELD_PIC val fieldPic = net.sergeych.lyng.PerfFlags.FIELD_PIC
@ -383,6 +446,7 @@ class FieldRef(
val (key, ver) = receiverKeyAndVersion(base) val (key, ver) = receiverKeyAndVersion(base)
rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) { rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++ if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++
noteReadHit()
val rec0 = g(base, scope) val rec0 = g(base, scope)
if (base is ObjClass) { if (base is ObjClass) {
val idx0 = base.classScope?.getSlotIndexOf(name) val idx0 = base.classScope?.getSlotIndexOf(name)
@ -392,6 +456,7 @@ class FieldRef(
} } } }
rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) { rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++ if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++
noteReadHit()
// move-to-front: promote 2→1 // move-to-front: promote 2→1
val tK = rKey2; val tV = rVer2; val tG = rGetter2 val tK = rKey2; val tV = rVer2; val tG = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
@ -403,8 +468,9 @@ class FieldRef(
} else { tRecord = null } } else { tRecord = null }
return rec0 return rec0
} } } }
rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) { if (size4ReadsEnabled()) rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++ if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++
noteReadHit()
// move-to-front: promote 3→1 // move-to-front: promote 3→1
val tK = rKey3; val tV = rVer3; val tG = rGetter3 val tK = rKey3; val tV = rVer3; val tG = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
@ -417,8 +483,9 @@ class FieldRef(
} else { tRecord = null } } else { tRecord = null }
return rec0 return rec0
} } } }
rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) { if (size4ReadsEnabled()) rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++ if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++
noteReadHit()
// move-to-front: promote 4→1 // move-to-front: promote 4→1
val tK = rKey4; val tV = rVer4; val tG = rGetter4 val tK = rKey4; val tV = rVer4; val tG = rGetter4
rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3 rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3
@ -434,6 +501,7 @@ class FieldRef(
} } } }
// Slow path // Slow path
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicMiss++ if (picCounters) net.sergeych.lyng.PerfStats.fieldPicMiss++
noteReadMiss()
val rec = try { val rec = try {
base.readField(scope, name) base.readField(scope, name)
} catch (e: ExecutionError) { } catch (e: ExecutionError) {
@ -444,9 +512,11 @@ class FieldRef(
rKey1 = key; rVer1 = ver; rGetter1 = { _, sc -> sc.raiseError(e.message ?: "no such field: $name") } rKey1 = key; rVer1 = ver; rGetter1 = { _, sc -> sc.raiseError(e.message ?: "no such field: $name") }
throw e throw e
} }
// Install move-to-front with a handle-aware getter (shift 1→2→3→4; put new at 1) // Install move-to-front with a handle-aware getter; honor PIC size flag
rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3 if (size4ReadsEnabled()) {
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
}
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
when (base) { when (base) {
is ObjClass -> { is ObjClass -> {
@ -499,18 +569,21 @@ class FieldRef(
val (key, ver) = receiverKeyAndVersion(base) val (key, ver) = receiverKeyAndVersion(base)
wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) { wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicSetHit++ if (picCounters) net.sergeych.lyng.PerfStats.fieldPicSetHit++
noteWriteHit()
return s(base, scope, newValue) return s(base, scope, newValue)
} } } }
wSetter2?.let { s -> if (key == wKey2 && ver == wVer2) { wSetter2?.let { s -> if (key == wKey2 && ver == wVer2) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicSetHit++ if (picCounters) net.sergeych.lyng.PerfStats.fieldPicSetHit++
noteWriteHit()
// move-to-front: promote 2→1 // move-to-front: promote 2→1
val tK = wKey2; val tV = wVer2; val tS = wSetter2 val tK = wKey2; val tV = wVer2; val tS = wSetter2
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1 wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
wKey1 = tK; wVer1 = tV; wSetter1 = tS wKey1 = tK; wVer1 = tV; wSetter1 = tS
return s(base, scope, newValue) return s(base, scope, newValue)
} } } }
wSetter3?.let { s -> if (key == wKey3 && ver == wVer3) { if (size4WritesEnabled()) wSetter3?.let { s -> if (key == wKey3 && ver == wVer3) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicSetHit++ if (picCounters) net.sergeych.lyng.PerfStats.fieldPicSetHit++
noteWriteHit()
// move-to-front: promote 3→1 // move-to-front: promote 3→1
val tK = wKey3; val tV = wVer3; val tS = wSetter3 val tK = wKey3; val tV = wVer3; val tS = wSetter3
wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2 wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2
@ -518,8 +591,9 @@ class FieldRef(
wKey1 = tK; wVer1 = tV; wSetter1 = tS wKey1 = tK; wVer1 = tV; wSetter1 = tS
return s(base, scope, newValue) return s(base, scope, newValue)
} } } }
wSetter4?.let { s -> if (key == wKey4 && ver == wVer4) { if (size4WritesEnabled()) wSetter4?.let { s -> if (key == wKey4 && ver == wVer4) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicSetHit++ if (picCounters) net.sergeych.lyng.PerfStats.fieldPicSetHit++
noteWriteHit()
// move-to-front: promote 4→1 // move-to-front: promote 4→1
val tK = wKey4; val tV = wVer4; val tS = wSetter4 val tK = wKey4; val tV = wVer4; val tS = wSetter4
wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3 wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3
@ -530,10 +604,13 @@ class FieldRef(
} } } }
// Slow path // Slow path
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicSetMiss++ if (picCounters) net.sergeych.lyng.PerfStats.fieldPicSetMiss++
noteWriteMiss()
base.writeField(scope, name, newValue) base.writeField(scope, name, newValue)
// Install move-to-front with a handle-aware setter (shift 1→2→3→4; put new at 1) // Install move-to-front with a handle-aware setter; honor PIC size flag
wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3 if (size4WritesEnabled()) {
wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2 wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3
wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2
}
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1 wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
when (base) { when (base) {
is ObjClass -> { is ObjClass -> {
@ -566,6 +643,63 @@ class FieldRef(
is ObjClass -> obj.classId to obj.layoutVersion is ObjClass -> obj.classId to obj.layoutVersion
else -> 0L to -1 // no caching for primitives/dynamics without stable shape else -> 0L to -1 // no caching for primitives/dynamics without stable shape
} }
override suspend fun evalValue(scope: Scope): Obj {
// Mirror get(), but return raw Obj to avoid transient ObjRecord on R-value paths
val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH
val fieldPic = net.sergeych.lyng.PerfFlags.FIELD_PIC
val picCounters = net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS
val base = if (fastRval) target.evalValue(scope) else target.get(scope).value
if (base == ObjNull && isOptional) return ObjNull
if (fieldPic) {
val (key, ver) = receiverKeyAndVersion(base)
rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++
return g(base, scope).value
} }
rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++
val tK = rKey2; val tV = rVer2; val tG = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tK; rVer1 = tV; rGetter1 = tG
return g(base, scope).value
} }
if (size4ReadsEnabled()) rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++
val tK = rKey3; val tV = rVer3; val tG = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tK; rVer1 = tV; rGetter1 = tG
return g(base, scope).value
} }
if (size4ReadsEnabled()) rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++
val tK = rKey4; val tV = rVer4; val tG = rGetter4
rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tK; rVer1 = tV; rGetter1 = tG
return g(base, scope).value
} }
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicMiss++
val rec = base.readField(scope, name)
// install primary generic getter for this shape
when (base) {
is ObjClass -> {
rKey1 = base.classId; rVer1 = base.layoutVersion; rGetter1 = { obj, sc -> obj.readField(sc, name) }
}
is ObjInstance -> {
val cls = base.objClass
rKey1 = cls.classId; rVer1 = cls.layoutVersion; rGetter1 = { obj, sc -> obj.readField(sc, name) }
}
else -> {
rKey1 = 0L; rVer1 = -1; rGetter1 = null
}
}
return rec.value
}
return base.readField(scope, name).value
}
} }
/** /**
@ -605,47 +739,139 @@ class IndexRef(
// Bounds checks are enforced by the underlying list access; exceptions propagate as before // Bounds checks are enforced by the underlying list access; exceptions propagate as before
return base.list[i].asMutable return base.list[i].asMutable
} }
// Polymorphic inline cache for other common shapes // String[Int] fast path
val (key, ver) = when (base) { if (base is ObjString && idx is ObjInt) {
is ObjInstance -> base.objClass.classId to base.objClass.layoutVersion val i = idx.toInt()
is ObjClass -> base.classId to base.layoutVersion return ObjChar(base.value[i]).asMutable
else -> 0L to -1
} }
if (key != 0L) { // Map[String] fast path (common case); return ObjNull if absent
rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) return g(base, scope, idx).asMutable } if (base is ObjMap && idx is ObjString) {
rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) { val v = base.map[idx] ?: ObjNull
val tk = rKey2; val tv = rVer2; val tg = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tk; rVer1 = tv; rGetter1 = tg
return g(base, scope, idx).asMutable
} }
rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) {
val tk = rKey3; val tv = rVer3; val tg = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tk; rVer1 = tv; rGetter1 = tg
return g(base, scope, idx).asMutable
} }
rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) {
val tk = rKey4; val tv = rVer4; val tg = rGetter4
rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tk; rVer1 = tv; rGetter1 = tg
return g(base, scope, idx).asMutable
} }
// Miss: resolve and install generic handler
val v = base.getAt(scope, idx)
rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc, ix -> obj.getAt(sc, ix) }
return v.asMutable return v.asMutable
} }
if (net.sergeych.lyng.PerfFlags.INDEX_PIC) {
// Polymorphic inline cache for other common shapes
val (key, ver) = when (base) {
is ObjInstance -> base.objClass.classId to base.objClass.layoutVersion
is ObjClass -> base.classId to base.layoutVersion
else -> 0L to -1
}
if (key != 0L) {
rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.indexPicHit++
return g(base, scope, idx).asMutable
} }
rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.indexPicHit++
val tk = rKey2; val tv = rVer2; val tg = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tk; rVer1 = tv; rGetter1 = tg
return g(base, scope, idx).asMutable
} }
if (net.sergeych.lyng.PerfFlags.INDEX_PIC_SIZE_4) rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.indexPicHit++
val tk = rKey3; val tv = rVer3; val tg = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tk; rVer1 = tv; rGetter1 = tg
return g(base, scope, idx).asMutable
} }
if (net.sergeych.lyng.PerfFlags.INDEX_PIC_SIZE_4) rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.indexPicHit++
val tk = rKey4; val tv = rVer4; val tg = rGetter4
rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tk; rVer1 = tv; rGetter1 = tg
return g(base, scope, idx).asMutable
} }
// Miss: resolve and install generic handler
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.indexPicMiss++
val v = base.getAt(scope, idx)
if (net.sergeych.lyng.PerfFlags.INDEX_PIC_SIZE_4) {
rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
}
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc, ix -> obj.getAt(sc, ix) }
return v.asMutable
}
}
} }
return base.getAt(scope, idx).asMutable return base.getAt(scope, idx).asMutable
} }
override suspend fun evalValue(scope: Scope): Obj {
val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH
val base = if (fastRval) target.evalValue(scope) else target.get(scope).value
if (base == ObjNull && isOptional) return ObjNull
val idx = if (fastRval) index.evalValue(scope) else index.get(scope).value
if (fastRval) {
// Fast list[int] path
if (base is ObjList && idx is ObjInt) {
val i = idx.toInt()
return base.list[i]
}
// String[Int] fast path
if (base is ObjString && idx is ObjInt) {
val i = idx.toInt()
return ObjChar(base.value[i])
}
// Map[String] fast path
if (base is ObjMap && idx is ObjString) {
return base.map[idx] ?: ObjNull
}
if (net.sergeych.lyng.PerfFlags.INDEX_PIC) {
// PIC path analogous to get(), but returning raw Obj
val (key, ver) = when (base) {
is ObjInstance -> base.objClass.classId to base.objClass.layoutVersion
is ObjClass -> base.classId to base.layoutVersion
else -> 0L to -1
}
if (key != 0L) {
rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.indexPicHit++
return g(base, scope, idx)
} }
rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.indexPicHit++
val tk = rKey2; val tv = rVer2; val tg = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tk; rVer1 = tv; rGetter1 = tg
return g(base, scope, idx)
} }
if (net.sergeych.lyng.PerfFlags.INDEX_PIC_SIZE_4) rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.indexPicHit++
val tk = rKey3; val tv = rVer3; val tg = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tk; rVer1 = tv; rGetter1 = tg
return g(base, scope, idx)
} }
if (net.sergeych.lyng.PerfFlags.INDEX_PIC_SIZE_4) rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.indexPicHit++
val tk = rKey4; val tv = rVer4; val tg = rGetter4
rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tk; rVer1 = tv; rGetter1 = tg
return g(base, scope, idx)
} }
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.indexPicMiss++
val v = base.getAt(scope, idx)
if (net.sergeych.lyng.PerfFlags.INDEX_PIC_SIZE_4) {
rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
}
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc, ix -> obj.getAt(sc, ix) }
return v
}
}
}
return base.getAt(scope, idx)
}
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) { override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH
val base = if (fastRval) target.evalValue(scope) else target.get(scope).value val base = if (fastRval) target.evalValue(scope) else target.get(scope).value
@ -661,43 +887,52 @@ class IndexRef(
base.list[i] = newValue base.list[i] = newValue
return return
} }
// Polymorphic inline cache for index write // Direct write fast path for ObjMap + ObjString
val (key, ver) = when (base) { if (base is ObjMap && idx is ObjString) {
is ObjInstance -> base.objClass.classId to base.objClass.layoutVersion base.map[idx] = newValue
is ObjClass -> base.classId to base.layoutVersion
else -> 0L to -1
}
if (key != 0L) {
wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) { s(base, scope, idx, newValue); return } }
wSetter2?.let { s -> if (key == wKey2 && ver == wVer2) {
val tk = wKey2; val tv = wVer2; val ts = wSetter2
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
wKey1 = tk; wVer1 = tv; wSetter1 = ts
s(base, scope, idx, newValue); return
} }
wSetter3?.let { s -> if (key == wKey3 && ver == wVer3) {
val tk = wKey3; val tv = wVer3; val ts = wSetter3
wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
wKey1 = tk; wVer1 = tv; wSetter1 = ts
s(base, scope, idx, newValue); return
} }
wSetter4?.let { s -> if (key == wKey4 && ver == wVer4) {
val tk = wKey4; val tv = wVer4; val ts = wSetter4
wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3
wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
wKey1 = tk; wVer1 = tv; wSetter1 = ts
s(base, scope, idx, newValue); return
} }
// Miss: perform write and install generic handler
base.putAt(scope, idx, newValue)
wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3
wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, ix, v -> obj.putAt(sc, ix, v) }
return return
} }
if (net.sergeych.lyng.PerfFlags.INDEX_PIC) {
// Polymorphic inline cache for index write
val (key, ver) = when (base) {
is ObjInstance -> base.objClass.classId to base.objClass.layoutVersion
is ObjClass -> base.classId to base.layoutVersion
else -> 0L to -1
}
if (key != 0L) {
wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) { s(base, scope, idx, newValue); return } }
wSetter2?.let { s -> if (key == wKey2 && ver == wVer2) {
val tk = wKey2; val tv = wVer2; val ts = wSetter2
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
wKey1 = tk; wVer1 = tv; wSetter1 = ts
s(base, scope, idx, newValue); return
} }
if (net.sergeych.lyng.PerfFlags.INDEX_PIC_SIZE_4) wSetter3?.let { s -> if (key == wKey3 && ver == wVer3) {
val tk = wKey3; val tv = wVer3; val ts = wSetter3
wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
wKey1 = tk; wVer1 = tv; wSetter1 = ts
s(base, scope, idx, newValue); return
} }
if (net.sergeych.lyng.PerfFlags.INDEX_PIC_SIZE_4) wSetter4?.let { s -> if (key == wKey4 && ver == wVer4) {
val tk = wKey4; val tv = wVer4; val ts = wSetter4
wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3
wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
wKey1 = tk; wVer1 = tv; wSetter1 = ts
s(base, scope, idx, newValue); return
} }
// Miss: perform write and install generic handler
base.putAt(scope, idx, newValue)
if (net.sergeych.lyng.PerfFlags.INDEX_PIC_SIZE_4) {
wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3
wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2
}
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, ix, v -> obj.putAt(sc, ix, v) }
return
}
}
} }
base.putAt(scope, idx, newValue) base.putAt(scope, idx, newValue)
} }
@ -752,6 +987,63 @@ class MethodCallRef(
private var mKey3: Long = 0L; private var mVer3: Int = -1; private var mInvoker3: (suspend (Obj, Scope, Arguments) -> Obj)? = null private var mKey3: Long = 0L; private var mVer3: Int = -1; private var mInvoker3: (suspend (Obj, Scope, Arguments) -> Obj)? = null
private var mKey4: Long = 0L; private var mVer4: Int = -1; private var mInvoker4: (suspend (Obj, Scope, Arguments) -> Obj)? = null private var mKey4: Long = 0L; private var mVer4: Int = -1; private var mInvoker4: (suspend (Obj, Scope, Arguments) -> Obj)? = null
// Adaptive PIC (2→4) for methods
private var mAccesses: Int = 0; private var mMisses: Int = 0; private var mPromotedTo4: Boolean = false
// Heuristic: windowed miss-rate tracking and temporary freeze back to size=2
private var mFreezeWindowsLeft: Int = 0
private var mWindowAccesses: Int = 0
private var mWindowMisses: Int = 0
private inline fun size4MethodsEnabled(): Boolean =
net.sergeych.lyng.PerfFlags.METHOD_PIC_SIZE_4 ||
((net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_2_TO_4 || net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_METHODS_ONLY) && mPromotedTo4 && mFreezeWindowsLeft == 0)
private fun noteMethodHit() {
if (!(net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_2_TO_4 || net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_METHODS_ONLY)) return
val a = (mAccesses + 1).coerceAtMost(1_000_000)
mAccesses = a
if (net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_HEURISTIC) {
// Windowed tracking
mWindowAccesses = (mWindowAccesses + 1).coerceAtMost(1_000_000)
if (mWindowAccesses >= 256) endHeuristicWindow()
}
}
private fun noteMethodMiss() {
if (!(net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_2_TO_4 || net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_METHODS_ONLY)) return
val a = (mAccesses + 1).coerceAtMost(1_000_000)
mAccesses = a
mMisses = (mMisses + 1).coerceAtMost(1_000_000)
if (!mPromotedTo4 && mFreezeWindowsLeft == 0 && a >= 256) {
if (mMisses * 100 / a > 20) mPromotedTo4 = true
mAccesses = 0; mMisses = 0
}
if (net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_HEURISTIC) {
mWindowAccesses = (mWindowAccesses + 1).coerceAtMost(1_000_000)
mWindowMisses = (mWindowMisses + 1).coerceAtMost(1_000_000)
if (mWindowAccesses >= 256) endHeuristicWindow()
}
}
private fun endHeuristicWindow() {
// Called only when PIC_ADAPTIVE_HEURISTIC is true
val accesses = mWindowAccesses
val misses = mWindowMisses
// Reset window
mWindowAccesses = 0
mWindowMisses = 0
// Count down freeze if active
if (mFreezeWindowsLeft > 0) {
mFreezeWindowsLeft = (mFreezeWindowsLeft - 1).coerceAtLeast(0)
return
}
// If promoted, but still high miss rate, freeze back to 2 for a few windows
if (mPromotedTo4 && accesses >= 256) {
val rate = misses * 100 / accesses
if (rate >= 25) {
mPromotedTo4 = false
mFreezeWindowsLeft = 4 // freeze next 4 windows
}
}
}
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH
val methodPic = net.sergeych.lyng.PerfFlags.METHOD_PIC val methodPic = net.sergeych.lyng.PerfFlags.METHOD_PIC
@ -763,14 +1055,17 @@ class MethodCallRef(
val (key, ver) = receiverKeyAndVersion(base) val (key, ver) = receiverKeyAndVersion(base)
mInvoker1?.let { inv -> if (key == mKey1 && ver == mVer1) { mInvoker1?.let { inv -> if (key == mKey1 && ver == mVer1) {
if (picCounters) net.sergeych.lyng.PerfStats.methodPicHit++ if (picCounters) net.sergeych.lyng.PerfStats.methodPicHit++
noteMethodHit()
return inv(base, scope, callArgs).asReadonly return inv(base, scope, callArgs).asReadonly
} } } }
mInvoker2?.let { inv -> if (key == mKey2 && ver == mVer2) { mInvoker2?.let { inv -> if (key == mKey2 && ver == mVer2) {
if (picCounters) net.sergeych.lyng.PerfStats.methodPicHit++ if (picCounters) net.sergeych.lyng.PerfStats.methodPicHit++
noteMethodHit()
return inv(base, scope, callArgs).asReadonly return inv(base, scope, callArgs).asReadonly
} } } }
mInvoker3?.let { inv -> if (key == mKey3 && ver == mVer3) { if (size4MethodsEnabled()) mInvoker3?.let { inv -> if (key == mKey3 && ver == mVer3) {
if (picCounters) net.sergeych.lyng.PerfStats.methodPicHit++ if (picCounters) net.sergeych.lyng.PerfStats.methodPicHit++
noteMethodHit()
// move-to-front: promote 3→1 // move-to-front: promote 3→1
val tK = mKey3; val tV = mVer3; val tI = mInvoker3 val tK = mKey3; val tV = mVer3; val tI = mInvoker3
mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2 mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2
@ -778,8 +1073,9 @@ class MethodCallRef(
mKey1 = tK; mVer1 = tV; mInvoker1 = tI mKey1 = tK; mVer1 = tV; mInvoker1 = tI
return inv(base, scope, callArgs).asReadonly return inv(base, scope, callArgs).asReadonly
} } } }
mInvoker4?.let { inv -> if (key == mKey4 && ver == mVer4) { if (size4MethodsEnabled()) mInvoker4?.let { inv -> if (key == mKey4 && ver == mVer4) {
if (picCounters) net.sergeych.lyng.PerfStats.methodPicHit++ if (picCounters) net.sergeych.lyng.PerfStats.methodPicHit++
noteMethodHit()
// move-to-front: promote 4→1 // move-to-front: promote 4→1
val tK = mKey4; val tV = mVer4; val tI = mInvoker4 val tK = mKey4; val tV = mVer4; val tI = mInvoker4
mKey4 = mKey3; mVer4 = mVer3; mInvoker4 = mInvoker3 mKey4 = mKey3; mVer4 = mVer3; mInvoker4 = mInvoker3
@ -790,6 +1086,7 @@ class MethodCallRef(
} } } }
// Slow path // Slow path
if (picCounters) net.sergeych.lyng.PerfStats.methodPicMiss++ if (picCounters) net.sergeych.lyng.PerfStats.methodPicMiss++
noteMethodMiss()
val result = try { val result = try {
base.invokeInstanceMethod(scope, name, callArgs) base.invokeInstanceMethod(scope, name, callArgs)
} catch (e: ExecutionError) { } catch (e: ExecutionError) {
@ -800,9 +1097,11 @@ class MethodCallRef(
mKey1 = key; mVer1 = ver; mInvoker1 = { _, sc, _ -> sc.raiseError(e.message ?: "method not found: $name") } mKey1 = key; mVer1 = ver; mInvoker1 = { _, sc, _ -> sc.raiseError(e.message ?: "method not found: $name") }
throw e throw e
} }
// Install move-to-front with a handle-aware invoker: shift 1→2→3→4, put new at 1 // Install move-to-front with a handle-aware invoker; honor PIC size flag
mKey4 = mKey3; mVer4 = mVer3; mInvoker4 = mInvoker3 if (size4MethodsEnabled()) {
mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2 mKey4 = mKey3; mVer4 = mVer3; mInvoker4 = mInvoker3
mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2
}
mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1 mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1
when (base) { when (base) {
is ObjInstance -> { is ObjInstance -> {
@ -889,6 +1188,18 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
return scope[name] ?: scope.raiseError("symbol not defined: '$name'") return scope[name] ?: scope.raiseError("symbol not defined: '$name'")
} }
override suspend fun evalValue(scope: Scope): Obj {
scope.pos = atPos
if (!PerfFlags.LOCAL_SLOT_PIC) {
scope.getSlotIndexOf(name)?.let { return scope.getSlotRecord(it).value }
return (scope[name] ?: scope.raiseError("symbol not defined: '$name'")).value
}
val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
val slot = if (hit) cachedSlot else resolveSlot(scope)
if (slot >= 0) return scope.getSlotRecord(slot).value
return (scope[name] ?: scope.raiseError("symbol not defined: '$name'")).value
}
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) { override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
scope.pos = atPos scope.pos = atPos
if (!PerfFlags.LOCAL_SLOT_PIC) { if (!PerfFlags.LOCAL_SLOT_PIC) {
@ -929,6 +1240,11 @@ class BoundLocalVarRef(
return scope.getSlotRecord(slot) return scope.getSlotRecord(slot)
} }
override suspend fun evalValue(scope: Scope): Obj {
scope.pos = atPos
return scope.getSlotRecord(slot).value
}
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) { override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
scope.pos = atPos scope.pos = atPos
val rec = scope.getSlotRecord(slot) val rec = scope.getSlotRecord(slot)
@ -989,6 +1305,15 @@ class FastLocalVarRef(
return actualOwner.getSlotRecord(slot) return actualOwner.getSlotRecord(slot)
} }
override suspend fun evalValue(scope: Scope): Obj {
scope.pos = atPos
val ownerValid = isOwnerValidFor(scope)
val slot = if (ownerValid && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope)
val actualOwner = cachedOwnerScope
if (slot < 0 || actualOwner == null) scope.raiseError("local '$name' is not available in this scope")
return actualOwner.getSlotRecord(slot).value
}
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) { override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
scope.pos = atPos scope.pos = atPos
val owner = if (isOwnerValidFor(scope)) cachedOwnerScope else null val owner = if (isOwnerValidFor(scope)) cachedOwnerScope else null

View File

@ -27,6 +27,11 @@ actual object PerfDefaults {
actual val FIELD_PIC: Boolean = true actual val FIELD_PIC: Boolean = true
actual val METHOD_PIC: Boolean = true actual val METHOD_PIC: Boolean = true
actual val FIELD_PIC_SIZE_4: Boolean = false
actual val METHOD_PIC_SIZE_4: Boolean = false
actual val PIC_ADAPTIVE_2_TO_4: Boolean = false
actual val PIC_ADAPTIVE_METHODS_ONLY: Boolean = false
actual val PIC_ADAPTIVE_HEURISTIC: Boolean = false
actual val PIC_DEBUG_COUNTERS: Boolean = false actual val PIC_DEBUG_COUNTERS: Boolean = false
@ -35,4 +40,7 @@ actual object PerfDefaults {
actual val RVAL_FASTPATH: Boolean = false actual val RVAL_FASTPATH: Boolean = false
// Regex caching: disabled by default on JS until validated // Regex caching: disabled by default on JS until validated
actual val REGEX_CACHE: Boolean = false actual val REGEX_CACHE: Boolean = false
actual val ARG_SMALL_ARITY_12: Boolean = false
actual val INDEX_PIC_SIZE_4: Boolean = false
actual val RANGE_FAST_ITER: Boolean = false
} }

View File

@ -27,6 +27,11 @@ actual object PerfDefaults {
actual val FIELD_PIC: Boolean = true actual val FIELD_PIC: Boolean = true
actual val METHOD_PIC: Boolean = true actual val METHOD_PIC: Boolean = true
actual val FIELD_PIC_SIZE_4: Boolean = false
actual val METHOD_PIC_SIZE_4: Boolean = false
actual val PIC_ADAPTIVE_2_TO_4: Boolean = false
actual val PIC_ADAPTIVE_METHODS_ONLY: Boolean = false
actual val PIC_ADAPTIVE_HEURISTIC: Boolean = false
actual val PIC_DEBUG_COUNTERS: Boolean = false actual val PIC_DEBUG_COUNTERS: Boolean = false
@ -35,4 +40,13 @@ actual object PerfDefaults {
// Regex caching (JVM-first): enabled by default on JVM // Regex caching (JVM-first): enabled by default on JVM
actual val REGEX_CACHE: Boolean = true actual val REGEX_CACHE: Boolean = true
// Extended small-arity calls 9..12 (experimental; keep OFF by default)
actual val ARG_SMALL_ARITY_12: Boolean = false
// Index PIC size (beneficial on JVM in A/B): enable size=4 by default
actual val INDEX_PIC_SIZE_4: Boolean = true
// Range fast-iteration (experimental; OFF by default)
actual val RANGE_FAST_ITER: Boolean = false
} }

View File

@ -27,6 +27,11 @@ actual object PerfDefaults {
actual val FIELD_PIC: Boolean = true actual val FIELD_PIC: Boolean = true
actual val METHOD_PIC: Boolean = true actual val METHOD_PIC: Boolean = true
actual val FIELD_PIC_SIZE_4: Boolean = false
actual val METHOD_PIC_SIZE_4: Boolean = false
actual val PIC_ADAPTIVE_2_TO_4: Boolean = false
actual val PIC_ADAPTIVE_METHODS_ONLY: Boolean = false
actual val PIC_ADAPTIVE_HEURISTIC: Boolean = false
actual val PIC_DEBUG_COUNTERS: Boolean = false actual val PIC_DEBUG_COUNTERS: Boolean = false
@ -35,4 +40,7 @@ actual object PerfDefaults {
actual val RVAL_FASTPATH: Boolean = false actual val RVAL_FASTPATH: Boolean = false
// Regex caching: keep OFF by default on Native until benchmarks validate it // Regex caching: keep OFF by default on Native until benchmarks validate it
actual val REGEX_CACHE: Boolean = false actual val REGEX_CACHE: Boolean = false
actual val ARG_SMALL_ARITY_12: Boolean = false
actual val INDEX_PIC_SIZE_4: Boolean = false
actual val RANGE_FAST_ITER: Boolean = false
} }

View File

@ -27,6 +27,11 @@ actual object PerfDefaults {
actual val FIELD_PIC: Boolean = true actual val FIELD_PIC: Boolean = true
actual val METHOD_PIC: Boolean = true actual val METHOD_PIC: Boolean = true
actual val FIELD_PIC_SIZE_4: Boolean = false
actual val METHOD_PIC_SIZE_4: Boolean = false
actual val PIC_ADAPTIVE_2_TO_4: Boolean = false
actual val PIC_ADAPTIVE_METHODS_ONLY: Boolean = false
actual val PIC_ADAPTIVE_HEURISTIC: Boolean = false
actual val PIC_DEBUG_COUNTERS: Boolean = false actual val PIC_DEBUG_COUNTERS: Boolean = false
@ -35,4 +40,7 @@ actual object PerfDefaults {
actual val RVAL_FASTPATH: Boolean = false actual val RVAL_FASTPATH: Boolean = false
// Regex caching: disabled by default on WasmJS until validated // Regex caching: disabled by default on WasmJS until validated
actual val REGEX_CACHE: Boolean = false actual val REGEX_CACHE: Boolean = false
actual val ARG_SMALL_ARITY_12: Boolean = false
actual val INDEX_PIC_SIZE_4: Boolean = false
actual val RANGE_FAST_ITER: Boolean = false
} }