From 882df679090e92d97a7d17c8d7f498ba88eb6453 Mon Sep 17 00:00:00 2001 From: sergeych Date: Sun, 16 Nov 2025 00:17:15 +0100 Subject: [PATCH] added new performance flags, extended PIC handling to size 4, introduced fast paths for ObjFastIntRangeIterator, small-arity arguments, index access, and unary operations --- docs/perf_guide.md | 84 ++- .../net/sergeych/lyng/PerfDefaults.android.kt | 8 + .../kotlin/net/sergeych/lyng/Arguments.kt | 57 +- .../kotlin/net/sergeych/lyng/Compiler.kt | 103 ++++ .../kotlin/net/sergeych/lyng/PerfDefaults.kt | 17 + .../kotlin/net/sergeych/lyng/PerfFlags.kt | 22 + .../kotlin/net/sergeych/lyng/PerfStats.kt | 6 + .../kotlin/net/sergeych/lyng/obj/ObjRange.kt | 12 + .../net/sergeych/lyng/obj/ObjRangeIterator.kt | 25 + .../kotlin/net/sergeych/lyng/obj/ObjRef.kt | 505 ++++++++++++++---- .../net/sergeych/lyng/PerfDefaults.js.kt | 8 + .../net/sergeych/lyng/PerfDefaults.jvm.kt | 14 + .../net/sergeych/lyng/PerfDefaults.native.kt | 8 + .../net/sergeych/lyng/PerfDefaults.wasmJs.kt | 8 + 14 files changed, 784 insertions(+), 93 deletions(-) diff --git a/docs/perf_guide.md b/docs/perf_guide.md index a5f0594..488be66 100644 --- a/docs/perf_guide.md +++ b/docs/perf_guide.md @@ -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. +### 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 - `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). - `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. - `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). - `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). - `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). @@ -28,11 +48,21 @@ See `src/commonMain/kotlin/net/sergeych/lyng/PerfFlags.kt` and `PerfDefaults.*.k ## Where optimizations apply - Locals: `FastLocalVarRef`, `LocalVarRef` per‑frame cache (PIC). -- Calls: small‑arity zero‑alloc paths (0–8 args), pooled builder (JVM), and child frame pooling (optional). -- Properties/methods: Field/Method PICs with receiver shape `(classId, layoutVersion)` and handle‑aware caches. +- 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; 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). - 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 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 PicBenchmarkTest ./gradlew :lynglib:jvmTest --tests PicInvalidationJvmTest +./gradlew :lynglib:jvmTest --tests PicAdaptiveABTest ./gradlew :lynglib:jvmTest --tests ArithmeticBenchmarkTest ./gradlew :lynglib:jvmTest --tests ExpressionBenchmarkTest +./gradlew :lynglib:jvmTest --tests IndexPicABTest +./gradlew :lynglib:jvmTest --tests IndexWritePathABTest ./gradlew :lynglib:jvmTest --tests CallPoolingBenchmarkTest ./gradlew :lynglib:jvmTest --tests MethodPoolingBenchmarkTest ./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. +### 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 Flags are mutable at runtime, e.g.: @@ -88,17 +133,46 @@ Available counters in `PerfStats`: - Field PIC: `fieldPicHit`, `fieldPicMiss`, `fieldPicSetHit`, `fieldPicSetMiss` - Method PIC: `methodPicHit`, `methodPicMiss` +- Index PIC: `indexPicHit`, `indexPicMiss` - Locals: `localVarPicHit`, `localVarPicMiss`, `fastLocalHit`, `fastLocalMiss` - Primitive ops: `primitiveFastOpsHit` 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) - 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. - `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_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 @@ -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. - 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...toArguments(scope: Scope, tailBlockMode: Boolean): Arguments { // Small-arity fast path (no splats) to reduce allocations if (PerfFlags.ARG_BUILDER) { + val limit = if (PerfFlags.ARG_SMALL_ARITY_12) 12 else 8 var hasSplat = false var count = 0 for (pa in this) { if (pa.isSplat) { hasSplat = true; break } count++ - if (count > 8) break + if (count > limit) break } if (!hasSplat && count == this.size) { val quick = when (count) { @@ -93,6 +94,60 @@ import net.sergeych.lyng.obj.ObjList val a7 = this.elementAt(7).value.execute(scope) 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 } if (quick != null) return quick diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index a71440a..9cc7bd0 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -1845,6 +1845,92 @@ class Compiler( } 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( // assignments, lowest priority Operator(Token.Type.ASSIGN, lastPriority) { pos, a, b -> @@ -1867,6 +1953,7 @@ class Compiler( }, // logical 1 Operator(Token.Type.OR, ++lastPriority) { _, a, b -> + foldBinary(BinOp.OR, a, b)?.let { return@Operator ConstRef(it.asReadonly) } LogicalOrRef(a, b) }, // logical 2 @@ -1875,12 +1962,15 @@ class Compiler( }, // bitwise or/xor/and (tighter than &&, looser than equality) Operator(Token.Type.BITOR, ++lastPriority) { _, a, b -> + foldBinary(BinOp.BOR, a, b)?.let { return@Operator ConstRef(it.asReadonly) } BinaryOpRef(BinOp.BOR, 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) }, Operator(Token.Type.BITAND, ++lastPriority) { _, a, b -> + foldBinary(BinOp.BAND, a, b)?.let { return@Operator ConstRef(it.asReadonly) } BinaryOpRef(BinOp.BAND, a, b) }, // equality/not equality and related @@ -1888,9 +1978,11 @@ class Compiler( BinaryOpRef(BinOp.EQARROW, 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) }, Operator(Token.Type.NEQ, lastPriority) { _, a, b -> + foldBinary(BinOp.NEQ, a, b)?.let { return@Operator ConstRef(it.asReadonly) } BinaryOpRef(BinOp.NEQ, a, b) }, Operator(Token.Type.REF_EQ, lastPriority) { _, a, b -> @@ -1907,15 +1999,19 @@ class Compiler( }, // relational <=,... Operator(Token.Type.LTE, ++lastPriority) { _, a, b -> + foldBinary(BinOp.LTE, a, b)?.let { return@Operator ConstRef(it.asReadonly) } BinaryOpRef(BinOp.LTE, 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) }, Operator(Token.Type.GTE, lastPriority) { _, a, b -> + foldBinary(BinOp.GTE, a, b)?.let { return@Operator ConstRef(it.asReadonly) } BinaryOpRef(BinOp.GTE, 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) }, // in, is: @@ -1942,25 +2038,32 @@ class Compiler( }, // shifts (tighter than shuttle, looser than +/-) Operator(Token.Type.SHL, ++lastPriority) { _, a, b -> + foldBinary(BinOp.SHL, a, b)?.let { return@Operator ConstRef(it.asReadonly) } BinaryOpRef(BinOp.SHL, 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) }, // arithmetic Operator(Token.Type.PLUS, ++lastPriority) { _, a, b -> + foldBinary(BinOp.PLUS, a, b)?.let { return@Operator ConstRef(it.asReadonly) } BinaryOpRef(BinOp.PLUS, 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) }, Operator(Token.Type.STAR, ++lastPriority) { _, a, b -> + foldBinary(BinOp.STAR, a, b)?.let { return@Operator ConstRef(it.asReadonly) } BinaryOpRef(BinOp.STAR, 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) }, Operator(Token.Type.PERCENT, lastPriority) { _, a, b -> + foldBinary(BinOp.PERCENT, a, b)?.let { return@Operator ConstRef(it.asReadonly) } BinaryOpRef(BinOp.PERCENT, a, b) }, ) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfDefaults.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfDefaults.kt index f1fbeb6..607b6ab 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfDefaults.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfDefaults.kt @@ -32,6 +32,14 @@ expect object PerfDefaults { val FIELD_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 @@ -40,4 +48,13 @@ expect object PerfDefaults { // Regex caching (JVM-first): small LRU for compiled patterns 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 } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfFlags.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfFlags.kt index 26d4098..a176d41 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfFlags.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfFlags.kt @@ -30,6 +30,8 @@ object PerfFlags { // Enable more efficient argument building and bulk-copy for splats 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. 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) @@ -39,6 +41,23 @@ object PerfFlags { var FIELD_PIC: Boolean = PerfDefaults.FIELD_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) 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) 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 } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfStats.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfStats.kt index 90618a8..7e72c7c 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfStats.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfStats.kt @@ -41,6 +41,10 @@ object PerfStats { // Primitive fast ops var primitiveFastOpsHit: Long = 0 + // Index PIC + var indexPicHit: Long = 0 + var indexPicMiss: Long = 0 + fun resetAll() { fieldPicHit = 0 fieldPicMiss = 0 @@ -53,5 +57,7 @@ object PerfStats { fastLocalHit = 0 fastLocalMiss = 0 primitiveFastOpsHit = 0 + indexPicHit = 0 + indexPicMiss = 0 } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRange.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRange.kt index 26c276b..5ea4676 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRange.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRange.kt @@ -161,6 +161,18 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob } addFn("iterator") { val self = thisAs() + 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() } } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRangeIterator.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRangeIterator.kt index 8c41d89..27748c6 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRangeIterator.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRangeIterator.kt @@ -17,6 +17,7 @@ package net.sergeych.lyng.obj +import net.sergeych.lyng.PerfFlags import net.sergeych.lyng.Scope 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().hasNext().toObj() } + addFn("next") { thisAs().next(this) } + } + } } \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt index 1397c37..76761f2 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt @@ -62,7 +62,23 @@ enum class BinOp { /** R-value reference for unary operations. */ class UnaryOpRef(private val op: UnaryOp, private val a: ObjRef) : ObjRef { 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) { UnaryOp.NOT -> v.logicalNot(scope) UnaryOp.NEGATE -> v.negate(scope) @@ -303,8 +319,9 @@ class IncDecRef( /** Elvis operator reference: a ?: b */ class ElvisRef(private val left: ObjRef, private val right: ObjRef) : ObjRef { 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 r = if (a != ObjNull) a else if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) right.evalValue(scope) else right.get(scope).value + val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH + 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 } } @@ -312,10 +329,12 @@ class ElvisRef(private val left: ObjRef, private val right: ObjRef) : ObjRef { /** Logical OR with short-circuit: a || b */ class LogicalOrRef(private val left: ObjRef, private val right: ObjRef) : ObjRef { 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 - val b = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) right.evalValue(scope) else right.get(scope).value - if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) { + val b = if (fastRval) right.evalValue(scope) else right.get(scope).value + if (fastPrim) { if (a is ObjBool && b is ObjBool) { 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 { 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 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 { val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH val fieldPic = net.sergeych.lyng.PerfFlags.FIELD_PIC @@ -383,6 +446,7 @@ class FieldRef( val (key, ver) = receiverKeyAndVersion(base) rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) { if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++ + noteReadHit() val rec0 = g(base, scope) if (base is ObjClass) { val idx0 = base.classScope?.getSlotIndexOf(name) @@ -392,6 +456,7 @@ class FieldRef( } } rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) { if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++ + noteReadHit() // move-to-front: promote 2→1 val tK = rKey2; val tV = rVer2; val tG = rGetter2 rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 @@ -403,8 +468,9 @@ class FieldRef( } else { tRecord = null } 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++ + noteReadHit() // move-to-front: promote 3→1 val tK = rKey3; val tV = rVer3; val tG = rGetter3 rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 @@ -417,8 +483,9 @@ class FieldRef( } else { tRecord = null } 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++ + noteReadHit() // move-to-front: promote 4→1 val tK = rKey4; val tV = rVer4; val tG = rGetter4 rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3 @@ -434,6 +501,7 @@ class FieldRef( } } // Slow path if (picCounters) net.sergeych.lyng.PerfStats.fieldPicMiss++ + noteReadMiss() val rec = try { base.readField(scope, name) } catch (e: ExecutionError) { @@ -444,9 +512,11 @@ class FieldRef( rKey1 = key; rVer1 = ver; rGetter1 = { _, sc -> sc.raiseError(e.message ?: "no such field: $name") } throw e } - // Install move-to-front with a handle-aware getter (shift 1→2→3→4; put new at 1) - rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3 - rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 + // Install move-to-front with a handle-aware getter; honor PIC size flag + if (size4ReadsEnabled()) { + rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3 + rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 + } rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 when (base) { is ObjClass -> { @@ -499,18 +569,21 @@ class FieldRef( val (key, ver) = receiverKeyAndVersion(base) wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) { if (picCounters) net.sergeych.lyng.PerfStats.fieldPicSetHit++ + noteWriteHit() return s(base, scope, newValue) } } wSetter2?.let { s -> if (key == wKey2 && ver == wVer2) { if (picCounters) net.sergeych.lyng.PerfStats.fieldPicSetHit++ + noteWriteHit() // move-to-front: promote 2→1 val tK = wKey2; val tV = wVer2; val tS = wSetter2 wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1 wKey1 = tK; wVer1 = tV; wSetter1 = tS 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++ + noteWriteHit() // move-to-front: promote 3→1 val tK = wKey3; val tV = wVer3; val tS = wSetter3 wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2 @@ -518,8 +591,9 @@ class FieldRef( wKey1 = tK; wVer1 = tV; wSetter1 = tS 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++ + noteWriteHit() // move-to-front: promote 4→1 val tK = wKey4; val tV = wVer4; val tS = wSetter4 wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3 @@ -530,10 +604,13 @@ class FieldRef( } } // Slow path if (picCounters) net.sergeych.lyng.PerfStats.fieldPicSetMiss++ + noteWriteMiss() base.writeField(scope, name, newValue) - // Install move-to-front with a handle-aware setter (shift 1→2→3→4; put new at 1) - wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3 - wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2 + // Install move-to-front with a handle-aware setter; honor PIC size flag + if (size4WritesEnabled()) { + wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3 + wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2 + } wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1 when (base) { is ObjClass -> { @@ -566,6 +643,63 @@ class FieldRef( is ObjClass -> obj.classId to obj.layoutVersion 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 return base.list[i].asMutable } - // 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 + // String[Int] fast path + if (base is ObjString && idx is ObjInt) { + val i = idx.toInt() + return ObjChar(base.value[i]).asMutable } - if (key != 0L) { - rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) return g(base, scope, idx).asMutable } - rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) { - 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) } + // Map[String] fast path (common case); return ObjNull if absent + if (base is ObjMap && idx is ObjString) { + val v = base.map[idx] ?: ObjNull 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 } + 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) { val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH val base = if (fastRval) target.evalValue(scope) else target.get(scope).value @@ -661,43 +887,52 @@ class IndexRef( base.list[i] = newValue return } - // 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 - } } - 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) } + // Direct write fast path for ObjMap + ObjString + if (base is ObjMap && idx is ObjString) { + base.map[idx] = newValue 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) } @@ -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 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 { val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH val methodPic = net.sergeych.lyng.PerfFlags.METHOD_PIC @@ -763,14 +1055,17 @@ class MethodCallRef( val (key, ver) = receiverKeyAndVersion(base) mInvoker1?.let { inv -> if (key == mKey1 && ver == mVer1) { if (picCounters) net.sergeych.lyng.PerfStats.methodPicHit++ + noteMethodHit() return inv(base, scope, callArgs).asReadonly } } mInvoker2?.let { inv -> if (key == mKey2 && ver == mVer2) { if (picCounters) net.sergeych.lyng.PerfStats.methodPicHit++ + noteMethodHit() 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++ + noteMethodHit() // move-to-front: promote 3→1 val tK = mKey3; val tV = mVer3; val tI = mInvoker3 mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2 @@ -778,8 +1073,9 @@ class MethodCallRef( mKey1 = tK; mVer1 = tV; mInvoker1 = tI 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++ + noteMethodHit() // move-to-front: promote 4→1 val tK = mKey4; val tV = mVer4; val tI = mInvoker4 mKey4 = mKey3; mVer4 = mVer3; mInvoker4 = mInvoker3 @@ -790,6 +1086,7 @@ class MethodCallRef( } } // Slow path if (picCounters) net.sergeych.lyng.PerfStats.methodPicMiss++ + noteMethodMiss() val result = try { base.invokeInstanceMethod(scope, name, callArgs) } catch (e: ExecutionError) { @@ -800,9 +1097,11 @@ class MethodCallRef( mKey1 = key; mVer1 = ver; mInvoker1 = { _, sc, _ -> sc.raiseError(e.message ?: "method not found: $name") } throw e } - // Install move-to-front with a handle-aware invoker: shift 1→2→3→4, put new at 1 - mKey4 = mKey3; mVer4 = mVer3; mInvoker4 = mInvoker3 - mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2 + // Install move-to-front with a handle-aware invoker; honor PIC size flag + if (size4MethodsEnabled()) { + mKey4 = mKey3; mVer4 = mVer3; mInvoker4 = mInvoker3 + mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2 + } mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1 when (base) { 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'") } + 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) { scope.pos = atPos if (!PerfFlags.LOCAL_SLOT_PIC) { @@ -929,6 +1240,11 @@ class BoundLocalVarRef( 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) { scope.pos = atPos val rec = scope.getSlotRecord(slot) @@ -989,6 +1305,15 @@ class FastLocalVarRef( 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) { scope.pos = atPos val owner = if (isOwnerValidFor(scope)) cachedOwnerScope else null diff --git a/lynglib/src/jsMain/kotlin/net/sergeych/lyng/PerfDefaults.js.kt b/lynglib/src/jsMain/kotlin/net/sergeych/lyng/PerfDefaults.js.kt index 5e6dbaa..907fec7 100644 --- a/lynglib/src/jsMain/kotlin/net/sergeych/lyng/PerfDefaults.js.kt +++ b/lynglib/src/jsMain/kotlin/net/sergeych/lyng/PerfDefaults.js.kt @@ -27,6 +27,11 @@ actual object PerfDefaults { actual val FIELD_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 @@ -35,4 +40,7 @@ actual object PerfDefaults { actual val RVAL_FASTPATH: Boolean = false // Regex caching: disabled by default on JS until validated 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 } \ No newline at end of file diff --git a/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/PerfDefaults.jvm.kt b/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/PerfDefaults.jvm.kt index 12db4a6..f9e6156 100644 --- a/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/PerfDefaults.jvm.kt +++ b/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/PerfDefaults.jvm.kt @@ -27,6 +27,11 @@ actual object PerfDefaults { actual val FIELD_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 @@ -35,4 +40,13 @@ actual object PerfDefaults { // Regex caching (JVM-first): enabled by default on JVM 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 } \ No newline at end of file diff --git a/lynglib/src/nativeMain/kotlin/net/sergeych/lyng/PerfDefaults.native.kt b/lynglib/src/nativeMain/kotlin/net/sergeych/lyng/PerfDefaults.native.kt index 2a86d33..85ed428 100644 --- a/lynglib/src/nativeMain/kotlin/net/sergeych/lyng/PerfDefaults.native.kt +++ b/lynglib/src/nativeMain/kotlin/net/sergeych/lyng/PerfDefaults.native.kt @@ -27,6 +27,11 @@ actual object PerfDefaults { actual val FIELD_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 @@ -35,4 +40,7 @@ actual object PerfDefaults { actual val RVAL_FASTPATH: Boolean = false // Regex caching: keep OFF by default on Native until benchmarks validate it 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 } \ No newline at end of file diff --git a/lynglib/src/wasmJsMain/kotlin/net/sergeych/lyng/PerfDefaults.wasmJs.kt b/lynglib/src/wasmJsMain/kotlin/net/sergeych/lyng/PerfDefaults.wasmJs.kt index b81f11c..ace1e23 100644 --- a/lynglib/src/wasmJsMain/kotlin/net/sergeych/lyng/PerfDefaults.wasmJs.kt +++ b/lynglib/src/wasmJsMain/kotlin/net/sergeych/lyng/PerfDefaults.wasmJs.kt @@ -27,6 +27,11 @@ actual object PerfDefaults { actual val FIELD_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 @@ -35,4 +40,7 @@ actual object PerfDefaults { actual val RVAL_FASTPATH: Boolean = false // Regex caching: disabled by default on WasmJS until validated 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 } \ No newline at end of file