Refactor scope pooling logic for robustness and efficiency, optimize argument assignment, and enhance benchmarks with pool-specific scenarios.
This commit is contained in:
parent
6b957ae6a3
commit
8f04b25fcb
@ -51,6 +51,32 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
|
||||
defaultVisibility: Visibility = Visibility.Public,
|
||||
declaringClass: net.sergeych.lyng.obj.ObjClass? = scope.currentClassCtx
|
||||
) {
|
||||
// Fast path for simple positional-only calls with no ellipsis and no defaults
|
||||
if (arguments.named.isEmpty() && !arguments.tailBlockMode) {
|
||||
var hasComplex = false
|
||||
for (p in params) {
|
||||
if (p.isEllipsis || p.defaultValue != null) {
|
||||
hasComplex = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!hasComplex) {
|
||||
if (arguments.list.size != params.size)
|
||||
scope.raiseIllegalArgument("expected ${params.size} arguments, got ${arguments.list.size}")
|
||||
|
||||
for (i in params.indices) {
|
||||
val a = params[i]
|
||||
val value = arguments.list[i]
|
||||
scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable,
|
||||
value.byValueCopy(),
|
||||
a.visibility ?: defaultVisibility,
|
||||
recordType = ObjRecord.Type.Argument,
|
||||
declaringClass = declaringClass)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fun assign(a: Item, value: Obj) {
|
||||
scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable,
|
||||
value.byValueCopy(),
|
||||
|
||||
@ -70,9 +70,8 @@ open class Scope(
|
||||
|
||||
internal fun findExtension(receiverClass: ObjClass, name: String): ObjRecord? {
|
||||
var s: Scope? = this
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) break
|
||||
var hops = 0
|
||||
while (s != null && hops++ < 1024) {
|
||||
// Proximity rule: check all extensions in the current scope before going to parent.
|
||||
// Priority within scope: more specific class in MRO wins.
|
||||
for (cls in receiverClass.mro) {
|
||||
@ -108,7 +107,7 @@ open class Scope(
|
||||
*/
|
||||
internal fun tryGetLocalRecord(s: Scope, name: String, caller: net.sergeych.lyng.obj.ObjClass?): ObjRecord? {
|
||||
caller?.let { ctx ->
|
||||
s.objects["${ctx.className}::$name"]?.let { rec ->
|
||||
s.objects[ctx.mangledName(name)]?.let { rec ->
|
||||
if (rec.visibility == Visibility.Private) return rec
|
||||
}
|
||||
}
|
||||
@ -116,7 +115,7 @@ open class Scope(
|
||||
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec
|
||||
}
|
||||
caller?.let { ctx ->
|
||||
s.localBindings["${ctx.className}::$name"]?.let { rec ->
|
||||
s.localBindings[ctx.mangledName(name)]?.let { rec ->
|
||||
if (rec.visibility == Visibility.Private) return rec
|
||||
}
|
||||
}
|
||||
@ -132,11 +131,10 @@ open class Scope(
|
||||
|
||||
internal fun chainLookupIgnoreClosure(name: String, followClosure: Boolean = false, caller: net.sergeych.lyng.obj.ObjClass? = null): ObjRecord? {
|
||||
var s: Scope? = this
|
||||
// use frameId to detect unexpected structural cycles in the parent chain
|
||||
val visited = HashSet<Long>(4)
|
||||
// use hop counter to detect unexpected structural cycles in the parent chain
|
||||
var hops = 0
|
||||
val effectiveCaller = caller ?: currentClassCtx
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) return null
|
||||
while (s != null && hops++ < 1024) {
|
||||
tryGetLocalRecord(s, name, effectiveCaller)?.let { return it }
|
||||
s = if (followClosure && s is ClosureScope) s.closureScope else s.parent
|
||||
}
|
||||
@ -155,9 +153,8 @@ open class Scope(
|
||||
tryGetLocalRecord(this, name, currentClassCtx)?.let { return it }
|
||||
// 2) walk parents for plain locals/bindings only
|
||||
var s = parent
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) return null
|
||||
var hops = 0
|
||||
while (s != null && hops++ < 1024) {
|
||||
tryGetLocalRecord(s, name, currentClassCtx)?.let { return it }
|
||||
s = s.parent
|
||||
}
|
||||
@ -182,9 +179,8 @@ open class Scope(
|
||||
*/
|
||||
internal fun chainLookupWithMembers(name: String, caller: net.sergeych.lyng.obj.ObjClass? = currentClassCtx, followClosure: Boolean = false): ObjRecord? {
|
||||
var s: Scope? = this
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) return null
|
||||
var hops = 0
|
||||
while (s != null && hops++ < 1024) {
|
||||
tryGetLocalRecord(s, name, caller)?.let { return it }
|
||||
for (cls in s.thisObj.objClass.mro) {
|
||||
s.extensions[cls]?.get(name)?.let { return it }
|
||||
@ -396,6 +392,20 @@ open class Scope(
|
||||
nameToSlot[name]?.let { slots[it] = record }
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all references and maps to prevent memory leaks when pooled.
|
||||
*/
|
||||
fun scrub() {
|
||||
this.parent = null
|
||||
this.skipScopeCreation = false
|
||||
this.currentClassCtx = null
|
||||
objects.clear()
|
||||
slots.clear()
|
||||
nameToSlot.clear()
|
||||
localBindings.clear()
|
||||
extensions.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset this scope instance so it can be safely reused as a fresh child frame.
|
||||
* Clears locals and slots, assigns new frameId, and sets parent/args/pos/thisObj.
|
||||
@ -414,7 +424,6 @@ open class Scope(
|
||||
nameToSlot.clear()
|
||||
localBindings.clear()
|
||||
extensions.clear()
|
||||
this.currentClassCtx = parent?.currentClassCtx
|
||||
// Now safe to validate and re-parent
|
||||
ensureNoCycle(parent)
|
||||
this.parent = parent
|
||||
@ -534,11 +543,15 @@ open class Scope(
|
||||
}
|
||||
}
|
||||
// Map to a slot for fast local access (ensure consistency)
|
||||
val idx = getSlotIndexOf(name)
|
||||
if (idx == null) {
|
||||
if (nameToSlot.isEmpty()) {
|
||||
allocateSlotFor(name, rec)
|
||||
} else {
|
||||
slots[idx] = rec
|
||||
val idx = nameToSlot[name]
|
||||
if (idx == null) {
|
||||
allocateSlotFor(name, rec)
|
||||
} else {
|
||||
slots[idx] = rec
|
||||
}
|
||||
}
|
||||
return rec
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -18,25 +18,25 @@
|
||||
package net.sergeych.lyng
|
||||
|
||||
actual object PerfDefaults {
|
||||
actual val LOCAL_SLOT_PIC: Boolean = false
|
||||
actual val EMIT_FAST_LOCAL_REFS: Boolean = false
|
||||
actual val LOCAL_SLOT_PIC: Boolean = true
|
||||
actual val EMIT_FAST_LOCAL_REFS: Boolean = true
|
||||
|
||||
actual val ARG_BUILDER: Boolean = false
|
||||
actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = false
|
||||
actual val ARG_BUILDER: Boolean = true
|
||||
actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = true
|
||||
actual val SCOPE_POOL: Boolean = false
|
||||
|
||||
actual val FIELD_PIC: Boolean = false
|
||||
actual val METHOD_PIC: Boolean = false
|
||||
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 FIELD_PIC: Boolean = true
|
||||
actual val METHOD_PIC: Boolean = true
|
||||
actual val FIELD_PIC_SIZE_4: Boolean = true
|
||||
actual val METHOD_PIC_SIZE_4: Boolean = true
|
||||
actual val PIC_ADAPTIVE_2_TO_4: Boolean = true
|
||||
actual val PIC_ADAPTIVE_METHODS_ONLY: Boolean = true
|
||||
actual val PIC_ADAPTIVE_HEURISTIC: Boolean = true
|
||||
|
||||
actual val PIC_DEBUG_COUNTERS: Boolean = false
|
||||
|
||||
actual val PRIMITIVE_FASTOPS: Boolean = false
|
||||
actual val RVAL_FASTPATH: Boolean = false
|
||||
actual val PRIMITIVE_FASTOPS: Boolean = true
|
||||
actual val RVAL_FASTPATH: Boolean = true
|
||||
|
||||
// Regex caching (JVM-first): enabled by default on JVM
|
||||
actual val REGEX_CACHE: Boolean = true
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -18,7 +18,6 @@
|
||||
package net.sergeych.lyng
|
||||
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjVoid
|
||||
|
||||
/**
|
||||
* JVM actual: per-thread scope frame pool backed by ThreadLocal.
|
||||
@ -32,26 +31,31 @@ actual object ScopePool {
|
||||
|
||||
actual fun borrow(parent: Scope, args: Arguments, pos: Pos, thisObj: Obj): Scope {
|
||||
val pool = threadLocalPool.get()
|
||||
val s = if (pool.isNotEmpty()) pool.removeLast() else Scope(parent, args, pos, thisObj)
|
||||
return try {
|
||||
// Always reset state on borrow to guarantee fresh-frame semantics
|
||||
s.resetForReuse(parent, args, pos, thisObj)
|
||||
s
|
||||
} catch (e: IllegalStateException) {
|
||||
// Defensive fallback: if a cycle in scope parent chain is detected during reuse,
|
||||
// discard pooled instance for this call frame and allocate a fresh scope instead.
|
||||
if (e.message?.contains("cycle") == true && e.message?.contains("scope parent chain") == true) {
|
||||
Scope(parent, args, pos, thisObj)
|
||||
} else {
|
||||
throw e
|
||||
if (pool.isNotEmpty()) {
|
||||
val s = pool.removeLast()
|
||||
try {
|
||||
// Re-initialize pooled instance
|
||||
s.resetForReuse(parent, args, pos, thisObj)
|
||||
return s
|
||||
} catch (e: IllegalStateException) {
|
||||
// Defensive fallback: if a cycle in scope parent chain is detected during reuse,
|
||||
// discard pooled instance for this call frame and allocate a fresh scope instead.
|
||||
if (e.message?.contains("cycle") == true && e.message?.contains("scope parent chain") == true) {
|
||||
return Scope(parent, args, pos, thisObj)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
return Scope(parent, args, pos, thisObj)
|
||||
}
|
||||
|
||||
actual fun release(scope: Scope) {
|
||||
val pool = threadLocalPool.get()
|
||||
// Scrub sensitive references to avoid accidental retention
|
||||
scope.resetForReuse(parent = null, args = Arguments.EMPTY, pos = Pos.builtIn, thisObj = ObjVoid)
|
||||
if (pool.size < MAX_POOL_SIZE) pool.addLast(scope)
|
||||
if (pool.size < MAX_POOL_SIZE) {
|
||||
// Scrub sensitive references to avoid accidental retention before returning to pool
|
||||
scope.scrub()
|
||||
pool.addLast(scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -88,20 +88,57 @@ class PicBenchmarkTest {
|
||||
|
||||
// PIC OFF
|
||||
PerfFlags.METHOD_PIC = false
|
||||
PerfFlags.SCOPE_POOL = 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")
|
||||
println("[DEBUG_LOG] [BENCH] Method PIC=OFF, POOL=OFF: ${(t1 - t0) / 1_000_000.0} ms")
|
||||
assertEquals(iterations.toLong(), r1)
|
||||
|
||||
// PIC ON
|
||||
PerfFlags.METHOD_PIC = true
|
||||
PerfFlags.SCOPE_POOL = 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")
|
||||
println("[DEBUG_LOG] [BENCH] Method PIC=ON, POOL=ON: ${(t3 - t2) / 1_000_000.0} ms")
|
||||
assertEquals(iterations.toLong(), r2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun benchmarkLoopScopePooling() = runBlocking {
|
||||
val iterations = 500_000
|
||||
val script = """
|
||||
var x = 0
|
||||
var i = 0
|
||||
while(i < $iterations) {
|
||||
if(true) {
|
||||
var y = 1
|
||||
x = x + y
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
x
|
||||
""".trimIndent()
|
||||
|
||||
// POOL OFF
|
||||
PerfFlags.SCOPE_POOL = false
|
||||
val scope1 = Scope()
|
||||
val t0 = System.nanoTime()
|
||||
val r1 = (scope1.eval(script) as ObjInt).value
|
||||
val t1 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] Loop Pool=OFF: ${(t1 - t0) / 1_000_000.0} ms")
|
||||
assertEquals(iterations.toLong(), r1)
|
||||
|
||||
// POOL ON
|
||||
PerfFlags.SCOPE_POOL = true
|
||||
val scope2 = Scope()
|
||||
val t2 = System.nanoTime()
|
||||
val r2 = (scope2.eval(script) as ObjInt).value
|
||||
val t3 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] Loop Pool=ON: ${(t3 - t2) / 1_000_000.0} ms")
|
||||
assertEquals(iterations.toLong(), r2)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user