more optimizations

This commit is contained in:
Sergey Chernov 2025-11-10 02:14:18 +01:00
parent 1498140892
commit 029fde2883
7 changed files with 475 additions and 50 deletions

View File

@ -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<ParsedArgument>.toArguments(scope: Scope, tailBlockMode: Boolean): Arguments {
val list = mutableListOf<Obj>()
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<Obj>, val tailBlockMode: Boolean = false) : List<Obj> 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<Any?> {
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<Obj>) = Arguments(values.toList())
}
}
data class ParsedArgument(val value: Statement, val pos: Pos, val isSplat: Boolean = false)
suspend fun Collection<ParsedArgument>.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<Obj> = 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<Obj>, val tailBlockMode: Boolean = false) : List<Obj> 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<Any?> {
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<Obj>) = Arguments(values.toList())
}
}

View File

@ -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
}

View File

@ -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) {

View File

@ -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<Long, Int> = 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<Long, Int> = when (obj) {
is ObjInstance -> obj.objClass.classId to obj.objClass.layoutVersion
is ObjClass -> obj.classId to obj.layoutVersion
else -> 0L to -1
}
}
/**

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}