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,
|
defaultVisibility: Visibility = Visibility.Public,
|
||||||
declaringClass: net.sergeych.lyng.obj.ObjClass? = scope.currentClassCtx
|
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) {
|
fun assign(a: Item, value: Obj) {
|
||||||
scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable,
|
scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable,
|
||||||
value.byValueCopy(),
|
value.byValueCopy(),
|
||||||
|
|||||||
@ -70,9 +70,8 @@ open class Scope(
|
|||||||
|
|
||||||
internal fun findExtension(receiverClass: ObjClass, name: String): ObjRecord? {
|
internal fun findExtension(receiverClass: ObjClass, name: String): ObjRecord? {
|
||||||
var s: Scope? = this
|
var s: Scope? = this
|
||||||
val visited = HashSet<Long>(4)
|
var hops = 0
|
||||||
while (s != null) {
|
while (s != null && hops++ < 1024) {
|
||||||
if (!visited.add(s.frameId)) break
|
|
||||||
// Proximity rule: check all extensions in the current scope before going to parent.
|
// Proximity rule: check all extensions in the current scope before going to parent.
|
||||||
// Priority within scope: more specific class in MRO wins.
|
// Priority within scope: more specific class in MRO wins.
|
||||||
for (cls in receiverClass.mro) {
|
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? {
|
internal fun tryGetLocalRecord(s: Scope, name: String, caller: net.sergeych.lyng.obj.ObjClass?): ObjRecord? {
|
||||||
caller?.let { ctx ->
|
caller?.let { ctx ->
|
||||||
s.objects["${ctx.className}::$name"]?.let { rec ->
|
s.objects[ctx.mangledName(name)]?.let { rec ->
|
||||||
if (rec.visibility == Visibility.Private) return 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
|
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec
|
||||||
}
|
}
|
||||||
caller?.let { ctx ->
|
caller?.let { ctx ->
|
||||||
s.localBindings["${ctx.className}::$name"]?.let { rec ->
|
s.localBindings[ctx.mangledName(name)]?.let { rec ->
|
||||||
if (rec.visibility == Visibility.Private) return 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? {
|
internal fun chainLookupIgnoreClosure(name: String, followClosure: Boolean = false, caller: net.sergeych.lyng.obj.ObjClass? = null): ObjRecord? {
|
||||||
var s: Scope? = this
|
var s: Scope? = this
|
||||||
// use frameId to detect unexpected structural cycles in the parent chain
|
// use hop counter to detect unexpected structural cycles in the parent chain
|
||||||
val visited = HashSet<Long>(4)
|
var hops = 0
|
||||||
val effectiveCaller = caller ?: currentClassCtx
|
val effectiveCaller = caller ?: currentClassCtx
|
||||||
while (s != null) {
|
while (s != null && hops++ < 1024) {
|
||||||
if (!visited.add(s.frameId)) return null
|
|
||||||
tryGetLocalRecord(s, name, effectiveCaller)?.let { return it }
|
tryGetLocalRecord(s, name, effectiveCaller)?.let { return it }
|
||||||
s = if (followClosure && s is ClosureScope) s.closureScope else s.parent
|
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 }
|
tryGetLocalRecord(this, name, currentClassCtx)?.let { return it }
|
||||||
// 2) walk parents for plain locals/bindings only
|
// 2) walk parents for plain locals/bindings only
|
||||||
var s = parent
|
var s = parent
|
||||||
val visited = HashSet<Long>(4)
|
var hops = 0
|
||||||
while (s != null) {
|
while (s != null && hops++ < 1024) {
|
||||||
if (!visited.add(s.frameId)) return null
|
|
||||||
tryGetLocalRecord(s, name, currentClassCtx)?.let { return it }
|
tryGetLocalRecord(s, name, currentClassCtx)?.let { return it }
|
||||||
s = s.parent
|
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? {
|
internal fun chainLookupWithMembers(name: String, caller: net.sergeych.lyng.obj.ObjClass? = currentClassCtx, followClosure: Boolean = false): ObjRecord? {
|
||||||
var s: Scope? = this
|
var s: Scope? = this
|
||||||
val visited = HashSet<Long>(4)
|
var hops = 0
|
||||||
while (s != null) {
|
while (s != null && hops++ < 1024) {
|
||||||
if (!visited.add(s.frameId)) return null
|
|
||||||
tryGetLocalRecord(s, name, caller)?.let { return it }
|
tryGetLocalRecord(s, name, caller)?.let { return it }
|
||||||
for (cls in s.thisObj.objClass.mro) {
|
for (cls in s.thisObj.objClass.mro) {
|
||||||
s.extensions[cls]?.get(name)?.let { return it }
|
s.extensions[cls]?.get(name)?.let { return it }
|
||||||
@ -396,6 +392,20 @@ open class Scope(
|
|||||||
nameToSlot[name]?.let { slots[it] = record }
|
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.
|
* 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.
|
* Clears locals and slots, assigns new frameId, and sets parent/args/pos/thisObj.
|
||||||
@ -414,7 +424,6 @@ open class Scope(
|
|||||||
nameToSlot.clear()
|
nameToSlot.clear()
|
||||||
localBindings.clear()
|
localBindings.clear()
|
||||||
extensions.clear()
|
extensions.clear()
|
||||||
this.currentClassCtx = parent?.currentClassCtx
|
|
||||||
// Now safe to validate and re-parent
|
// Now safe to validate and re-parent
|
||||||
ensureNoCycle(parent)
|
ensureNoCycle(parent)
|
||||||
this.parent = parent
|
this.parent = parent
|
||||||
@ -534,11 +543,15 @@ open class Scope(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Map to a slot for fast local access (ensure consistency)
|
// Map to a slot for fast local access (ensure consistency)
|
||||||
val idx = getSlotIndexOf(name)
|
if (nameToSlot.isEmpty()) {
|
||||||
if (idx == null) {
|
|
||||||
allocateSlotFor(name, rec)
|
allocateSlotFor(name, rec)
|
||||||
} else {
|
} else {
|
||||||
slots[idx] = rec
|
val idx = nameToSlot[name]
|
||||||
|
if (idx == null) {
|
||||||
|
allocateSlotFor(name, rec)
|
||||||
|
} else {
|
||||||
|
slots[idx] = rec
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return 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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -18,25 +18,25 @@
|
|||||||
package net.sergeych.lyng
|
package net.sergeych.lyng
|
||||||
|
|
||||||
actual object PerfDefaults {
|
actual object PerfDefaults {
|
||||||
actual val LOCAL_SLOT_PIC: Boolean = false
|
actual val LOCAL_SLOT_PIC: Boolean = true
|
||||||
actual val EMIT_FAST_LOCAL_REFS: Boolean = false
|
actual val EMIT_FAST_LOCAL_REFS: Boolean = true
|
||||||
|
|
||||||
actual val ARG_BUILDER: Boolean = false
|
actual val ARG_BUILDER: Boolean = true
|
||||||
actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = false
|
actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = true
|
||||||
actual val SCOPE_POOL: Boolean = false
|
actual val SCOPE_POOL: Boolean = false
|
||||||
|
|
||||||
actual val FIELD_PIC: Boolean = false
|
actual val FIELD_PIC: Boolean = true
|
||||||
actual val METHOD_PIC: Boolean = false
|
actual val METHOD_PIC: Boolean = true
|
||||||
actual val FIELD_PIC_SIZE_4: Boolean = false
|
actual val FIELD_PIC_SIZE_4: Boolean = true
|
||||||
actual val METHOD_PIC_SIZE_4: Boolean = false
|
actual val METHOD_PIC_SIZE_4: Boolean = true
|
||||||
actual val PIC_ADAPTIVE_2_TO_4: Boolean = false
|
actual val PIC_ADAPTIVE_2_TO_4: Boolean = true
|
||||||
actual val PIC_ADAPTIVE_METHODS_ONLY: Boolean = false
|
actual val PIC_ADAPTIVE_METHODS_ONLY: Boolean = true
|
||||||
actual val PIC_ADAPTIVE_HEURISTIC: Boolean = false
|
actual val PIC_ADAPTIVE_HEURISTIC: Boolean = true
|
||||||
|
|
||||||
actual val PIC_DEBUG_COUNTERS: Boolean = false
|
actual val PIC_DEBUG_COUNTERS: Boolean = false
|
||||||
|
|
||||||
actual val PRIMITIVE_FASTOPS: Boolean = false
|
actual val PRIMITIVE_FASTOPS: Boolean = true
|
||||||
actual val RVAL_FASTPATH: Boolean = false
|
actual val RVAL_FASTPATH: Boolean = true
|
||||||
|
|
||||||
// Regex caching (JVM-first): enabled by default on JVM
|
// Regex caching (JVM-first): enabled by default on JVM
|
||||||
actual val REGEX_CACHE: Boolean = true
|
actual val REGEX_CACHE: Boolean = true
|
||||||
|
|||||||
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -18,7 +18,6 @@
|
|||||||
package net.sergeych.lyng
|
package net.sergeych.lyng
|
||||||
|
|
||||||
import net.sergeych.lyng.obj.Obj
|
import net.sergeych.lyng.obj.Obj
|
||||||
import net.sergeych.lyng.obj.ObjVoid
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JVM actual: per-thread scope frame pool backed by ThreadLocal.
|
* 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 {
|
actual fun borrow(parent: Scope, args: Arguments, pos: Pos, thisObj: Obj): Scope {
|
||||||
val pool = threadLocalPool.get()
|
val pool = threadLocalPool.get()
|
||||||
val s = if (pool.isNotEmpty()) pool.removeLast() else Scope(parent, args, pos, thisObj)
|
if (pool.isNotEmpty()) {
|
||||||
return try {
|
val s = pool.removeLast()
|
||||||
// Always reset state on borrow to guarantee fresh-frame semantics
|
try {
|
||||||
s.resetForReuse(parent, args, pos, thisObj)
|
// Re-initialize pooled instance
|
||||||
s
|
s.resetForReuse(parent, args, pos, thisObj)
|
||||||
} catch (e: IllegalStateException) {
|
return s
|
||||||
// Defensive fallback: if a cycle in scope parent chain is detected during reuse,
|
} catch (e: IllegalStateException) {
|
||||||
// discard pooled instance for this call frame and allocate a fresh scope instead.
|
// Defensive fallback: if a cycle in scope parent chain is detected during reuse,
|
||||||
if (e.message?.contains("cycle") == true && e.message?.contains("scope parent chain") == true) {
|
// discard pooled instance for this call frame and allocate a fresh scope instead.
|
||||||
Scope(parent, args, pos, thisObj)
|
if (e.message?.contains("cycle") == true && e.message?.contains("scope parent chain") == true) {
|
||||||
} else {
|
return Scope(parent, args, pos, thisObj)
|
||||||
throw e
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return Scope(parent, args, pos, thisObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun release(scope: Scope) {
|
actual fun release(scope: Scope) {
|
||||||
val pool = threadLocalPool.get()
|
val pool = threadLocalPool.get()
|
||||||
// Scrub sensitive references to avoid accidental retention
|
if (pool.size < MAX_POOL_SIZE) {
|
||||||
scope.resetForReuse(parent = null, args = Arguments.EMPTY, pos = Pos.builtIn, thisObj = ObjVoid)
|
// Scrub sensitive references to avoid accidental retention before returning to pool
|
||||||
if (pool.size < MAX_POOL_SIZE) pool.addLast(scope)
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -88,20 +88,57 @@ class PicBenchmarkTest {
|
|||||||
|
|
||||||
// PIC OFF
|
// PIC OFF
|
||||||
PerfFlags.METHOD_PIC = false
|
PerfFlags.METHOD_PIC = false
|
||||||
|
PerfFlags.SCOPE_POOL = false
|
||||||
val scope1 = Scope()
|
val scope1 = Scope()
|
||||||
val t0 = System.nanoTime()
|
val t0 = System.nanoTime()
|
||||||
val r1 = (scope1.eval(script) as ObjInt).value
|
val r1 = (scope1.eval(script) as ObjInt).value
|
||||||
val t1 = System.nanoTime()
|
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)
|
assertEquals(iterations.toLong(), r1)
|
||||||
|
|
||||||
// PIC ON
|
// PIC ON
|
||||||
PerfFlags.METHOD_PIC = true
|
PerfFlags.METHOD_PIC = true
|
||||||
|
PerfFlags.SCOPE_POOL = true
|
||||||
val scope2 = Scope()
|
val scope2 = Scope()
|
||||||
val t2 = System.nanoTime()
|
val t2 = System.nanoTime()
|
||||||
val r2 = (scope2.eval(script) as ObjInt).value
|
val r2 = (scope2.eval(script) as ObjInt).value
|
||||||
val t3 = System.nanoTime()
|
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)
|
assertEquals(iterations.toLong(), r2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user