From 029fde288356e5e5fe7a959a644f199062fbf313 Mon Sep 17 00:00:00 2001 From: sergeych Date: Mon, 10 Nov 2025 02:14:18 +0100 Subject: [PATCH] more optimizations --- .../kotlin/net/sergeych/lyng/Arguments.kt | 102 +++++----- .../kotlin/net/sergeych/lyng/PerfFlags.kt | 14 ++ .../kotlin/net/sergeych/lyng/obj/ObjClass.kt | 11 ++ .../kotlin/net/sergeych/lyng/obj/ObjRef.kt | 181 +++++++++++++++++- .../jvmTest/kotlin/ArithmeticBenchmarkTest.kt | 76 ++++++++ .../src/jvmTest/kotlin/CallBenchmarkTest.kt | 55 ++++++ .../src/jvmTest/kotlin/PicBenchmarkTest.kt | 86 +++++++++ 7 files changed, 475 insertions(+), 50 deletions(-) create mode 100644 lynglib/src/jvmTest/kotlin/ArithmeticBenchmarkTest.kt create mode 100644 lynglib/src/jvmTest/kotlin/CallBenchmarkTest.kt create mode 100644 lynglib/src/jvmTest/kotlin/PicBenchmarkTest.kt diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt index 5e8a30c..50be76f 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt @@ -21,53 +21,57 @@ import net.sergeych.lyng.obj.Obj import net.sergeych.lyng.obj.ObjIterable import net.sergeych.lyng.obj.ObjList -data class ParsedArgument(val value: Statement, val pos: Pos, val isSplat: Boolean = false) - -suspend fun Collection.toArguments(scope: Scope, tailBlockMode: Boolean): Arguments { - val list = mutableListOf() - - for (x in this) { - val value = x.value.execute(scope) - if (x.isSplat) { - when { - value is ObjList -> { - for (subitem in value.list) list.add(subitem) - } - - value.isInstanceOf(ObjIterable) -> { - val i = (value.invokeInstanceMethod(scope, "toList") as ObjList).list - i.forEach { list.add(it) } - } - - else -> scope.raiseClassCastError("expected list of objects for splat argument") - } - } else - list.add(value) - } - return Arguments(list,tailBlockMode) -} - -data class Arguments(val list: List, val tailBlockMode: Boolean = false) : List by list { - - constructor(vararg values: Obj) : this(values.toList()) - - fun firstAndOnly(pos: Pos = Pos.UNKNOWN): Obj { - if (list.size != 1) throw ScriptError(pos, "expected one argument, got ${list.size}") - return list.first().byValueCopy() - } - - /** - * Convert to list of kotlin objects, see [Obj.toKotlin]. - */ - suspend fun toKotlinList(scope: Scope): List { - return list.map { it.toKotlin(scope) } - } - - suspend fun inspect(scope: Scope): String = list.map{ it.inspect(scope)}.joinToString(",") - - companion object { - val EMPTY = Arguments(emptyList()) - fun from(values: Collection) = Arguments(values.toList()) - } -} + data class ParsedArgument(val value: Statement, val pos: Pos, val isSplat: Boolean = false) + + suspend fun Collection.toArguments(scope: Scope, tailBlockMode: Boolean): Arguments { + // If ARG_BUILDER is enabled, try to reuse a pre-sized ArrayList and do bulk-adds + val list: MutableList = if (PerfFlags.ARG_BUILDER) ArrayList(this.size) else mutableListOf() + + for (x in this) { + val value = x.value.execute(scope) + if (x.isSplat) { + when { + value is ObjList -> { + // Bulk add elements from an ObjList + list.addAll(value.list) + } + + value.isInstanceOf(ObjIterable) -> { + // Convert to list once and bulk add + val i = (value.invokeInstanceMethod(scope, "toList") as ObjList).list + list.addAll(i) + } + + else -> scope.raiseClassCastError("expected list of objects for splat argument") + } + } else { + list.add(value) + } + } + return Arguments(list, tailBlockMode) + } + + data class Arguments(val list: List, val tailBlockMode: Boolean = false) : List by list { + + constructor(vararg values: Obj) : this(values.toList()) + + fun firstAndOnly(pos: Pos = Pos.UNKNOWN): Obj { + if (list.size != 1) throw ScriptError(pos, "expected one argument, got ${list.size}") + return list.first().byValueCopy() + } + + /** + * Convert to list of kotlin objects, see [Obj.toKotlin]. + */ + suspend fun toKotlinList(scope: Scope): List { + return list.map { it.toKotlin(scope) } + } + + suspend fun inspect(scope: Scope): String = list.map{ it.inspect(scope)}.joinToString(",") + + companion object { + val EMPTY = Arguments(emptyList()) + fun from(values: Collection) = Arguments(values.toList()) + } + } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfFlags.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfFlags.kt index e257554..d3bf0be 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfFlags.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfFlags.kt @@ -9,4 +9,18 @@ object PerfFlags { var LOCAL_SLOT_PIC: Boolean = true // Make the compiler emit fast local refs for identifiers known to be function locals/params var EMIT_FAST_LOCAL_REFS: Boolean = true + + // Enable more efficient argument building and bulk-copy for splats + var ARG_BUILDER: Boolean = true + // Allow early-return in optional calls before building args (semantics-compatible). Present for A/B only. + var SKIP_ARGS_ON_NULL_RECEIVER: Boolean = true + // Enable pooling of Scope frames for calls (planned; JVM-only) + var SCOPE_POOL: Boolean = false + + // Step 2: PICs for fields and methods + var FIELD_PIC: Boolean = true + var METHOD_PIC: Boolean = true + + // Step 3: Primitive arithmetic and comparison fast paths + var PRIMITIVE_FASTOPS: Boolean = true } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt index 9fdf781..fa6a36f 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt @@ -21,6 +21,9 @@ import net.sergeych.lyng.* import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonType +// Simple id generator for class identities (not thread-safe; fine for scripts) +private object ClassIdGen { var c: Long = 1L; fun nextId(): Long = c++ } + val ObjClassType by lazy { ObjClass("Class") } open class ObjClass( @@ -28,6 +31,10 @@ open class ObjClass( vararg parents: ObjClass, ) : Obj() { + // Stable identity and simple structural version for PICs + val classId: Long = ClassIdGen.nextId() + var layoutVersion: Int = 0 + val classNameObj by lazy { ObjString(className) } var constructorMeta: ArgsDeclaration? = null @@ -84,6 +91,8 @@ open class ObjClass( if (existing?.isMutable == false) throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes") members[name] = ObjRecord(initialValue, isMutable, visibility) + // Structural change: bump layout version for PIC invalidation + layoutVersion += 1 } private fun initClassScope(): Scope { @@ -103,6 +112,8 @@ open class ObjClass( if (existing != null) throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes") classScope!!.addItem(name, isMutable, initialValue, visibility) + // Structural change: bump layout version for PIC invalidation + layoutVersion += 1 } fun addFn(name: String, isOpen: Boolean = false, code: suspend Scope.() -> Obj) { 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 3b6afa8..e40b6a1 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt @@ -65,6 +65,42 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r override suspend fun get(scope: Scope): ObjRecord { val a = left.get(scope).value val b = right.get(scope).value + + // Primitive fast paths for common cases (guarded by PerfFlags.PRIMITIVE_FASTOPS) + if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) { + // Fast boolean ops when both operands are ObjBool + if (a is ObjBool && b is ObjBool) { + val r: Obj? = when (op) { + BinOp.OR -> if (a.value || b.value) ObjTrue else ObjFalse + BinOp.AND -> if (a.value && b.value) ObjTrue else ObjFalse + BinOp.EQ -> if (a.value == b.value) ObjTrue else ObjFalse + BinOp.NEQ -> if (a.value != b.value) ObjTrue else ObjFalse + else -> null + } + if (r != null) return r.asReadonly + } + // Fast integer ops when both operands are ObjInt + if (a is ObjInt && b is ObjInt) { + val av = a.value + val bv = b.value + val r: Obj? = when (op) { + BinOp.PLUS -> ObjInt(av + bv) + BinOp.MINUS -> ObjInt(av - bv) + BinOp.STAR -> ObjInt(av * bv) + BinOp.SLASH -> if (bv != 0L) ObjInt(av / bv) else null + BinOp.PERCENT -> if (bv != 0L) ObjInt(av % bv) else null + BinOp.EQ -> if (av == bv) ObjTrue else ObjFalse + BinOp.NEQ -> if (av != bv) ObjTrue else ObjFalse + BinOp.LT -> if (av < bv) ObjTrue else ObjFalse + BinOp.LTE -> if (av <= bv) ObjTrue else ObjFalse + BinOp.GT -> if (av > bv) ObjTrue else ObjFalse + BinOp.GTE -> if (av >= bv) ObjTrue else ObjFalse + else -> null + } + if (r != null) return r.asReadonly + } + } + val r: Obj = when (op) { BinOp.OR -> a.logicalOr(scope, b) BinOp.AND -> a.logicalAnd(scope, b) @@ -172,6 +208,11 @@ class LogicalOrRef(private val left: ObjRef, private val right: ObjRef) : ObjRef val a = left.get(scope).value if ((a as? ObjBool)?.value == true) return ObjTrue.asReadonly val b = right.get(scope).value + if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) { + if (a is ObjBool && b is ObjBool) { + return if (a.value || b.value) ObjTrue.asReadonly else ObjFalse.asReadonly + } + } return a.logicalOr(scope, b).asReadonly } } @@ -182,6 +223,11 @@ class LogicalAndRef(private val left: ObjRef, private val right: ObjRef) : ObjRe val a = left.get(scope).value if ((a as? ObjBool)?.value == false) return ObjFalse.asReadonly val b = right.get(scope).value + if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) { + if (a is ObjBool && b is ObjBool) { + return if (a.value && b.value) ObjTrue.asReadonly else ObjFalse.asReadonly + } + } return a.logicalAnd(scope, b).asReadonly } } @@ -201,9 +247,54 @@ class FieldRef( private val name: String, private val isOptional: Boolean, ) : ObjRef { + // 2-entry PIC for reads/writes (guarded by PerfFlags.FIELD_PIC) + private var rKey1: Long = 0L; private var rVer1: Int = -1; private var rGetter1: (suspend (Obj, Scope) -> ObjRecord)? = null + private var rKey2: Long = 0L; private var rVer2: Int = -1; private var rGetter2: (suspend (Obj, Scope) -> ObjRecord)? = null + + private var wKey1: Long = 0L; private var wVer1: Int = -1; private var wSetter1: (suspend (Obj, Scope, Obj) -> Unit)? = null + private var wKey2: Long = 0L; private var wVer2: Int = -1; private var wSetter2: (suspend (Obj, Scope, Obj) -> Unit)? = null + override suspend fun get(scope: Scope): ObjRecord { val base = target.get(scope).value - return if (base == ObjNull && isOptional) ObjNull.asMutable else base.readField(scope, name) + if (base == ObjNull && isOptional) return ObjNull.asMutable + if (net.sergeych.lyng.PerfFlags.FIELD_PIC) { + val (key, ver) = receiverKeyAndVersion(base) + rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) return g(base, scope) } + rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) return g(base, scope) } + // Slow path + val rec = base.readField(scope, name) + // Install move-to-front with a handle-aware getter + rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 + rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc -> + when (obj) { + is ObjInstance -> { + val instScope = obj.instanceScope + val idx = instScope.getSlotIndexOf(name) + if (idx != null) { + val r = instScope.getSlotRecord(idx) + if (!r.visibility.isPublic) + sc.raiseError(ObjAccessException(sc, "can't access non-public field $name")) + r + } else obj.readField(sc, name) + } + is ObjClass -> { + val clsScope = obj.classScope + if (clsScope != null) { + val idx = clsScope.getSlotIndexOf(name) + if (idx != null) { + val r = clsScope.getSlotRecord(idx) + if (!r.visibility.isPublic) + sc.raiseError(ObjAccessException(sc, "can't access non-public field $name")) + r + } else obj.readField(sc, name) + } else obj.readField(sc, name) + } + else -> obj.readField(sc, name) + } + } + return rec + } + return base.readField(scope, name) } override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) { @@ -212,8 +303,53 @@ class FieldRef( // no-op on null receiver for optional chaining assignment return } + if (net.sergeych.lyng.PerfFlags.FIELD_PIC) { + val (key, ver) = receiverKeyAndVersion(base) + wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) return s(base, scope, newValue) } + wSetter2?.let { s -> if (key == wKey2 && ver == wVer2) return s(base, scope, newValue) } + // Slow path + base.writeField(scope, name, newValue) + // Install move-to-front with a handle-aware setter + wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1 + wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, v -> + when (obj) { + is ObjInstance -> { + val instScope = obj.instanceScope + val idx = instScope.getSlotIndexOf(name) + if (idx != null) { + val r = instScope.getSlotRecord(idx) + if (!r.visibility.isPublic) + sc.raiseError(ObjAccessException(sc, "can't assign to non-public field $name")) + if (!r.isMutable) + sc.raiseError(ObjIllegalAssignmentException(sc, "can't reassign val $name")) + if (r.value.assign(sc, v) == null) r.value = v + } else obj.writeField(sc, name, v) + } + is ObjClass -> { + val clsScope = obj.classScope + if (clsScope != null) { + val idx = clsScope.getSlotIndexOf(name) + if (idx != null) { + val r = clsScope.getSlotRecord(idx) + if (!r.isMutable) + sc.raiseError(ObjIllegalAssignmentException(sc, "can't reassign val $name")) + r.value = v + } else obj.writeField(sc, name, v) + } else obj.writeField(sc, name, v) + } + else -> obj.writeField(sc, name, v) + } + } + return + } base.writeField(scope, name, newValue) } + + private fun receiverKeyAndVersion(obj: Obj): Pair = when (obj) { + is ObjInstance -> obj.objClass.classId to obj.objClass.layoutVersion + is ObjClass -> obj.classId to obj.layoutVersion + else -> 0L to -1 // no caching for primitives/dynamics without stable shape + } } /** @@ -277,13 +413,56 @@ class MethodCallRef( private val tailBlock: Boolean, private val isOptional: Boolean, ) : ObjRef { + // 2-entry PIC for method invocations (guarded by PerfFlags.METHOD_PIC) + private var mKey1: Long = 0L; private var mVer1: Int = -1; private var mInvoker1: (suspend (Obj, Scope, Arguments) -> Obj)? = null + private var mKey2: Long = 0L; private var mVer2: Int = -1; private var mInvoker2: (suspend (Obj, Scope, Arguments) -> Obj)? = null + override suspend fun get(scope: Scope): ObjRecord { val base = receiver.get(scope).value if (base == ObjNull && isOptional) return ObjNull.asReadonly val callArgs = args.toArguments(scope, tailBlock) + if (net.sergeych.lyng.PerfFlags.METHOD_PIC) { + val (key, ver) = receiverKeyAndVersion(base) + mInvoker1?.let { inv -> if (key == mKey1 && ver == mVer1) return inv(base, scope, callArgs).asReadonly } + mInvoker2?.let { inv -> if (key == mKey2 && ver == mVer2) return inv(base, scope, callArgs).asReadonly } + // Slow path + val result = base.invokeInstanceMethod(scope, name, callArgs) + // Install move-to-front with a handle-aware invoker + mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1 + mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> + when (obj) { + is ObjInstance -> { + val instScope = obj.instanceScope + val rec = instScope.get(name) + if (rec != null) { + if (!rec.visibility.isPublic) + sc.raiseError(ObjAccessException(sc, "can't invoke non-public method $name")) + rec.value.invoke(instScope, obj, a) + } else obj.invokeInstanceMethod(sc, name, a) + } + is ObjClass -> { + val clsScope = obj.classScope + if (clsScope != null) { + val rec = clsScope.get(name) + if (rec != null) { + rec.value.invoke(sc, obj, a) + } else obj.invokeInstanceMethod(sc, name, a) + } else obj.invokeInstanceMethod(sc, name, a) + } + else -> obj.invokeInstanceMethod(sc, name, a) + } + } + return result.asReadonly + } val result = base.invokeInstanceMethod(scope, name, callArgs) return result.asReadonly } + + private fun receiverKeyAndVersion(obj: Obj): Pair = when (obj) { + is ObjInstance -> obj.objClass.classId to obj.objClass.layoutVersion + is ObjClass -> obj.classId to obj.layoutVersion + else -> 0L to -1 + } } /** diff --git a/lynglib/src/jvmTest/kotlin/ArithmeticBenchmarkTest.kt b/lynglib/src/jvmTest/kotlin/ArithmeticBenchmarkTest.kt new file mode 100644 index 0000000..249790f --- /dev/null +++ b/lynglib/src/jvmTest/kotlin/ArithmeticBenchmarkTest.kt @@ -0,0 +1,76 @@ +/* + * JVM micro-benchmarks for primitive arithmetic and comparison fast paths. + */ + +import kotlinx.coroutines.runBlocking +import net.sergeych.lyng.PerfFlags +import net.sergeych.lyng.Scope +import net.sergeych.lyng.obj.ObjInt +import kotlin.test.Test +import kotlin.test.assertEquals + +class ArithmeticBenchmarkTest { + @Test + fun benchmarkIntArithmeticAndComparisons() = runBlocking { + val n = 400_000 + val sumScript = """ + var s = 0 + var i = 0 + while (i < $n) { + s = s + i + i = i + 1 + } + s + """.trimIndent() + + // Baseline: disable primitive fast ops + PerfFlags.PRIMITIVE_FASTOPS = false + val scope1 = Scope() + val t0 = System.nanoTime() + val r1 = (scope1.eval(sumScript) as ObjInt).value + val t1 = System.nanoTime() + println("[DEBUG_LOG] [BENCH] int-sum x$n [PRIMITIVE_FASTOPS=OFF]: ${(t1 - t0)/1_000_000.0} ms") + + // Optimized + PerfFlags.PRIMITIVE_FASTOPS = true + val scope2 = Scope() + val t2 = System.nanoTime() + val r2 = (scope2.eval(sumScript) as ObjInt).value + val t3 = System.nanoTime() + println("[DEBUG_LOG] [BENCH] int-sum x$n [PRIMITIVE_FASTOPS=ON]: ${(t3 - t2)/1_000_000.0} ms") + + val expected = (n.toLong() - 1L) * n / 2L + assertEquals(expected, r1) + assertEquals(expected, r2) + + // Comparison heavy (branchy) loop + val cmpScript = """ + var s = 0 + var i = 0 + while (i < $n) { + if (i % 2 == 0) s = s + 1 else s = s + 2 + i = i + 1 + } + s + """.trimIndent() + + PerfFlags.PRIMITIVE_FASTOPS = false + val scope3 = Scope() + val t4 = System.nanoTime() + val c1 = (scope3.eval(cmpScript) as ObjInt).value + val t5 = System.nanoTime() + println("[DEBUG_LOG] [BENCH] int-cmp x$n [PRIMITIVE_FASTOPS=OFF]: ${(t5 - t4)/1_000_000.0} ms") + + PerfFlags.PRIMITIVE_FASTOPS = true + val scope4 = Scope() + val t6 = System.nanoTime() + val c2 = (scope4.eval(cmpScript) as ObjInt).value + val t7 = System.nanoTime() + println("[DEBUG_LOG] [BENCH] int-cmp x$n [PRIMITIVE_FASTOPS=ON]: ${(t7 - t6)/1_000_000.0} ms") + + // Expected: half of n even add 1, half odd add 2 (n even assumed) + val expectedCmp = (n / 2) * 1L + (n - n / 2) * 2L + assertEquals(expectedCmp, c1) + assertEquals(expectedCmp, c2) + } +} diff --git a/lynglib/src/jvmTest/kotlin/CallBenchmarkTest.kt b/lynglib/src/jvmTest/kotlin/CallBenchmarkTest.kt new file mode 100644 index 0000000..4cad648 --- /dev/null +++ b/lynglib/src/jvmTest/kotlin/CallBenchmarkTest.kt @@ -0,0 +1,55 @@ +/* + * JVM micro-benchmarks for function/method call overhead and argument building. + */ + +import kotlinx.coroutines.runBlocking +import net.sergeych.lyng.PerfFlags +import net.sergeych.lyng.Scope +import net.sergeych.lyng.obj.ObjInt +import kotlin.test.Test +import kotlin.test.assertEquals + +class CallBenchmarkTest { + @Test + fun benchmarkSimpleFunctionCalls() = runBlocking { + val n = 300_000 // keep it fast for CI + + // A tiny script with 0, 1, 2 arg functions and a loop using them + val script = """ + fun f0() { 1 } + fun f1(a) { a } + fun f2(a,b) { a + b } + + var s = 0 + var i = 0 + while (i < $n) { + s = s + f0() + s = s + f1(1) + s = s + f2(1, 1) + i = i + 1 + } + s + """.trimIndent() + + // Disable ARG_BUILDER for baseline + PerfFlags.ARG_BUILDER = false + val scope1 = Scope() + val t0 = System.nanoTime() + val r1 = (scope1.eval(script) as ObjInt).value + val t1 = System.nanoTime() + println("[DEBUG_LOG] [BENCH] calls x$n [ARG_BUILDER=OFF]: ${(t1 - t0)/1_000_000.0} ms") + + // Enable ARG_BUILDER for optimized run + PerfFlags.ARG_BUILDER = true + val scope2 = Scope() + val t2 = System.nanoTime() + val r2 = (scope2.eval(script) as ObjInt).value + val t3 = System.nanoTime() + println("[DEBUG_LOG] [BENCH] calls x$n [ARG_BUILDER=ON]: ${(t3 - t2)/1_000_000.0} ms") + + // Correctness: each loop adds 1 + 1 + (1+1) = 4 + val expected = 4L * n + assertEquals(expected, r1) + assertEquals(expected, r2) + } +} diff --git a/lynglib/src/jvmTest/kotlin/PicBenchmarkTest.kt b/lynglib/src/jvmTest/kotlin/PicBenchmarkTest.kt new file mode 100644 index 0000000..93ad01e --- /dev/null +++ b/lynglib/src/jvmTest/kotlin/PicBenchmarkTest.kt @@ -0,0 +1,86 @@ +/* + * JVM micro-benchmarks for FieldRef and MethodCallRef PICs. + */ + +import kotlinx.coroutines.runBlocking +import net.sergeych.lyng.PerfFlags +import net.sergeych.lyng.Scope +import net.sergeych.lyng.obj.ObjInt +import kotlin.test.Test +import kotlin.test.assertEquals + +class PicBenchmarkTest { + @Test + fun benchmarkFieldGetSetPic() = runBlocking { + val iterations = 300_000 + val script = """ + class C() { + var x = 0 + fun add1() { x = x + 1 } + fun getX() { x } + } + val c = C() + var i = 0 + while(i < $iterations) { + c.x = c.x + 1 + i = i + 1 + } + c.x + """.trimIndent() + + // PIC OFF + PerfFlags.FIELD_PIC = false + val scope1 = Scope() + val t0 = System.nanoTime() + val r1 = (scope1.eval(script) as ObjInt).value + val t1 = System.nanoTime() + println("[DEBUG_LOG] [BENCH] Field PIC=OFF: ${(t1 - t0) / 1_000_000.0} ms") + assertEquals(iterations.toLong(), r1) + + // PIC ON + PerfFlags.FIELD_PIC = true + val scope2 = Scope() + val t2 = System.nanoTime() + val r2 = (scope2.eval(script) as ObjInt).value + val t3 = System.nanoTime() + println("[DEBUG_LOG] [BENCH] Field PIC=ON: ${(t3 - t2) / 1_000_000.0} ms") + assertEquals(iterations.toLong(), r2) + } + + @Test + fun benchmarkMethodPic() = runBlocking { + val iterations = 200_000 + val script = """ + class C() { + var x = 0 + fun add(v) { x = x + v } + fun get() { x } + } + val c = C() + var i = 0 + while(i < $iterations) { + c.add(1) + i = i + 1 + } + c.get() + """.trimIndent() + + // PIC OFF + PerfFlags.METHOD_PIC = false + val scope1 = Scope() + val t0 = System.nanoTime() + val r1 = (scope1.eval(script) as ObjInt).value + val t1 = System.nanoTime() + println("[DEBUG_LOG] [BENCH] Method PIC=OFF: ${(t1 - t0) / 1_000_000.0} ms") + assertEquals(iterations.toLong(), r1) + + // PIC ON + PerfFlags.METHOD_PIC = true + val scope2 = Scope() + val t2 = System.nanoTime() + val r2 = (scope2.eval(script) as ObjInt).value + val t3 = System.nanoTime() + println("[DEBUG_LOG] [BENCH] Method PIC=ON: ${(t3 - t2) / 1_000_000.0} ms") + assertEquals(iterations.toLong(), r2) + } +}