added Multiple Inheritance

This commit is contained in:
sergeych committed 2025-11-17 00:28:07 +01:00
1 parent 882df67909
commit aeeec2d417
33 files changed
+3302 -130

No files matched your search

@@ -0,0 +1,336 @@
/*
* Copyright 2025 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import kotlinx.coroutines.runBlocking
import net.sergeych.lyng.PerfFlags
import java.io.File
import java.lang.management.GarbageCollectorMXBean
import java.lang.management.ManagementFactory
import java.nio.file.Files
import java.nio.file.Paths
import kotlin.io.path.extension
import kotlin.random.Random
import kotlin.system.measureNanoTime
import kotlin.test.Test
class BookAllocationProfileTest {
private fun outFile(): File = File("lynglib/build/book_alloc_profile.txt")
private fun writeHeader(f: File) {
if (!f.parentFile.exists()) f.parentFile.mkdirs()
f.writeText("[DEBUG_LOG] Book allocation/time profiling (JVM)\n")
f.appendText("[DEBUG_LOG] All sizes in bytes; time in ns (lower is better).\n")
}
private fun appendLine(f: File, s: String) { f.appendText(s + "\n") }
// Optional STDERR filter to hide benign warnings during profiling runs
private inline fun <T> withFilteredStderr(vararg suppressContains: String, block: () -> T): T {
val orig = System.err
val filtering = java.io.PrintStream(object : java.io.OutputStream() {
private val buf = StringBuilder()
override fun write(b: Int) {
if (b == '\n'.code) {
val line = buf.toString()
val suppress = suppressContains.any { line.contains(it) }
if (!suppress) orig.println(line)
buf.setLength(0)
} else buf.append(b.toChar())
}
})
return try {
System.setErr(filtering)
block()
} finally {
System.setErr(orig)
}
}
private fun forceGc() {
// Best-effort GC to stabilize measurements
repeat(3) {
System.gc()
try { Thread.sleep(25) } catch (_: InterruptedException) {}
}
}
private fun usedHeap(): Long {
val mem = ManagementFactory.getMemoryMXBean().heapMemoryUsage
return mem.used
}
private suspend fun runBooksOnce(): Unit = runBlocking {
// Mirror BookTest set
runDocTests("../docs/tutorial.md")
runDocTests("../docs/math.md")
runDocTests("../docs/advanced_topics.md")
runDocTests("../docs/OOP.md")
runDocTests("../docs/Real.md")
runDocTests("../docs/List.md")
runDocTests("../docs/Range.md")
runDocTests("../docs/Set.md")
runDocTests("../docs/Map.md")
runDocTests("../docs/Buffer.md")
// Samples folder, bookMode=true
for (bt in Files.list(Paths.get("../docs/samples")).toList()) {
if (bt.extension == "md") runDocTests(bt.toString(), bookMode = true)
}
runDocTests("../docs/declaring_arguments.md")
runDocTests("../docs/exceptions_handling.md")
runDocTests("../docs/time.md")
runDocTests("../docs/parallelism.md")
runDocTests("../docs/RingBuffer.md")
runDocTests("../docs/Iterable.md")
}
private data class ProfileResult(val timeNs: Long, val allocBytes: Long)
private suspend fun profileRun(): ProfileResult {
forceGc()
val before = usedHeap()
val elapsed = measureNanoTime {
withFilteredStderr("ScriptFlowIsNoMoreCollected") {
runBooksOnce()
}
}
forceGc()
val after = usedHeap()
val alloc = (after - before).coerceAtLeast(0)
return ProfileResult(elapsed, alloc)
}
private data class GcSnapshot(val count: Long, val timeMs: Long)
private fun gcSnapshot(): GcSnapshot {
var c = 0L
var t = 0L
for (gc: GarbageCollectorMXBean in ManagementFactory.getGarbageCollectorMXBeans()) {
c += (gc.collectionCount.takeIf { it >= 0 } ?: 0)
t += (gc.collectionTime.takeIf { it >= 0 } ?: 0)
}
return GcSnapshot(c, t)
}
// --- Optional JFR support via reflection (works only on JDKs with Flight Recorder) ---
private class JfrHandle(val rec: Any, val dump: (File) -> Unit, val stop: () -> Unit)
private fun jfrStartIfRequested(name: String): JfrHandle? {
val enabled = System.getProperty("lyng.jfr")?.toBoolean() == true
if (!enabled) return null
return try {
val recCl = Class.forName("jdk.jfr.Recording")
val ctor = recCl.getDeclaredConstructor()
val rec = ctor.newInstance()
val setName = recCl.methods.firstOrNull { it.name == "setName" && it.parameterTypes.size == 1 }
setName?.invoke(rec, "Lyng-$name")
val start = recCl.methods.first { it.name == "start" && it.parameterTypes.isEmpty() }
start.invoke(rec)
val stop = recCl.methods.first { it.name == "stop" && it.parameterTypes.isEmpty() }
val dump = recCl.methods.firstOrNull { it.name == "dump" && it.parameterTypes.size == 1 }
val dumper: (File) -> Unit = if (dump != null) {
{ f -> dump.invoke(rec, f.toPath()) }
} else {
{ _ -> }
}
JfrHandle(rec, dumper) { stop.invoke(rec) }
} catch (e: Throwable) {
// JFR requested but not available; note once via stdout and proceed without it
try {
println("[DEBUG_LOG] JFR not available on this JVM; run with Oracle/OpenJDK 11+ to enable -Dlyng.jfr=true")
} catch (_: Throwable) {}
null
}
}
private fun intProp(name: String, def: Int): Int =
System.getProperty(name)?.toIntOrNull() ?: def
private fun boolProp(name: String, def: Boolean): Boolean =
System.getProperty(name)?.toBoolean() ?: def
private data class FlagSnapshot(
val RVAL_FASTPATH: Boolean,
val PRIMITIVE_FASTOPS: Boolean,
val ARG_BUILDER: Boolean,
val ARG_SMALL_ARITY_12: Boolean,
val FIELD_PIC: Boolean,
val METHOD_PIC: Boolean,
val FIELD_PIC_SIZE_4: Boolean,
val METHOD_PIC_SIZE_4: Boolean,
val INDEX_PIC: Boolean,
val INDEX_PIC_SIZE_4: Boolean,
val SCOPE_POOL: Boolean,
val PIC_DEBUG_COUNTERS: Boolean,
) {
fun restore() {
PerfFlags.RVAL_FASTPATH = RVAL_FASTPATH
PerfFlags.PRIMITIVE_FASTOPS = PRIMITIVE_FASTOPS
PerfFlags.ARG_BUILDER = ARG_BUILDER
PerfFlags.ARG_SMALL_ARITY_12 = ARG_SMALL_ARITY_12
PerfFlags.FIELD_PIC = FIELD_PIC
PerfFlags.METHOD_PIC = METHOD_PIC
PerfFlags.FIELD_PIC_SIZE_4 = FIELD_PIC_SIZE_4
PerfFlags.METHOD_PIC_SIZE_4 = METHOD_PIC_SIZE_4
PerfFlags.INDEX_PIC = INDEX_PIC
PerfFlags.INDEX_PIC_SIZE_4 = INDEX_PIC_SIZE_4
PerfFlags.SCOPE_POOL = SCOPE_POOL
PerfFlags.PIC_DEBUG_COUNTERS = PIC_DEBUG_COUNTERS
}
}
private fun snapshotFlags() = FlagSnapshot(
RVAL_FASTPATH = PerfFlags.RVAL_FASTPATH,
PRIMITIVE_FASTOPS = PerfFlags.PRIMITIVE_FASTOPS,
ARG_BUILDER = PerfFlags.ARG_BUILDER,
ARG_SMALL_ARITY_12 = PerfFlags.ARG_SMALL_ARITY_12,
FIELD_PIC = PerfFlags.FIELD_PIC,
METHOD_PIC = PerfFlags.METHOD_PIC,
FIELD_PIC_SIZE_4 = PerfFlags.FIELD_PIC_SIZE_4,
METHOD_PIC_SIZE_4 = PerfFlags.METHOD_PIC_SIZE_4,
INDEX_PIC = PerfFlags.INDEX_PIC,
INDEX_PIC_SIZE_4 = PerfFlags.INDEX_PIC_SIZE_4,
SCOPE_POOL = PerfFlags.SCOPE_POOL,
PIC_DEBUG_COUNTERS = PerfFlags.PIC_DEBUG_COUNTERS,
)
private fun median(values: List<Long>): Long {
if (values.isEmpty()) return 0
val s = values.sorted()
val mid = s.size / 2
return if (s.size % 2 == 1) s[mid] else ((s[mid - 1] + s[mid]) / 2)
}
private suspend fun runScenario(
name: String,
prepare: () -> Unit,
repeats: Int = 3,
out: (String) -> Unit
): ProfileResult {
val warmup = intProp("lyng.profile.warmup", 1)
val reps = intProp("lyng.profile.repeats", repeats)
// JFR
val jfr = jfrStartIfRequested(name)
if (System.getProperty("lyng.jfr")?.toBoolean() == true && jfr == null) {
out("[DEBUG_LOG] JFR: requested but not available on this JVM")
}
// Warm-up before GC snapshot (some profilers prefer this)
prepare()
repeat(warmup) { profileRun() }
// GC baseline
val gc0 = gcSnapshot()
val times = ArrayList<Long>(repeats)
val allocs = ArrayList<Long>(repeats)
repeat(reps) {
val r = profileRun()
times += r.timeNs
allocs += r.allocBytes
}
val pr = ProfileResult(median(times), median(allocs))
val gc1 = gcSnapshot()
val gcCountDelta = (gc1.count - gc0.count).coerceAtLeast(0)
val gcTimeDelta = (gc1.timeMs - gc0.timeMs).coerceAtLeast(0)
out("[DEBUG_LOG] time=${pr.timeNs} ns, alloc=${pr.allocBytes} B (median of ${reps}), GC(count=${gcCountDelta}, timeMs=${gcTimeDelta})")
// Stop and dump JFR if enabled
if (jfr != null) {
try {
jfr.stop()
val dumpFile = File("lynglib/build/jfr_${name}.jfr")
jfr.dump(dumpFile)
out("[DEBUG_LOG] JFR dumped: ${dumpFile.path}")
} catch (_: Throwable) {}
}
return pr
}
@Test
fun profile_books_allocations_and_time() = runTestBlocking {
val f = outFile()
writeHeader(f)
fun log(s: String) = appendLine(f, s)
val saved = snapshotFlags()
try {
data class Scenario(val label: String, val title: String, val prep: () -> Unit)
val scenarios = mutableListOf<Scenario>()
// Baseline A
scenarios += Scenario("A", "JVM defaults") {
saved.restore(); PerfFlags.PIC_DEBUG_COUNTERS = false
}
// Most flags OFF B
scenarios += Scenario("B", "most perf flags OFF") {
saved.restore(); PerfFlags.PIC_DEBUG_COUNTERS = false
PerfFlags.RVAL_FASTPATH = false
PerfFlags.PRIMITIVE_FASTOPS = false
PerfFlags.ARG_BUILDER = false
PerfFlags.ARG_SMALL_ARITY_12 = false
PerfFlags.FIELD_PIC = false
PerfFlags.METHOD_PIC = false
PerfFlags.FIELD_PIC_SIZE_4 = false
PerfFlags.METHOD_PIC_SIZE_4 = false
PerfFlags.INDEX_PIC = false
PerfFlags.INDEX_PIC_SIZE_4 = false
PerfFlags.SCOPE_POOL = false
}
// Defaults with INDEX_PIC size 2 C
scenarios += Scenario("C", "defaults except INDEX_PIC_SIZE_4=false") {
saved.restore(); PerfFlags.PIC_DEBUG_COUNTERS = false
PerfFlags.INDEX_PIC = true; PerfFlags.INDEX_PIC_SIZE_4 = false
}
// One-flag toggles relative to A
scenarios += Scenario("D", "A with RVAL_FASTPATH=false") {
saved.restore(); PerfFlags.PIC_DEBUG_COUNTERS = false; PerfFlags.RVAL_FASTPATH = false
}
scenarios += Scenario("E", "A with PRIMITIVE_FASTOPS=false") {
saved.restore(); PerfFlags.PIC_DEBUG_COUNTERS = false; PerfFlags.PRIMITIVE_FASTOPS = false
}
scenarios += Scenario("F", "A with INDEX_PIC=false") {
saved.restore(); PerfFlags.PIC_DEBUG_COUNTERS = false; PerfFlags.INDEX_PIC = false
}
scenarios += Scenario("G", "A with SCOPE_POOL=false") {
saved.restore(); PerfFlags.PIC_DEBUG_COUNTERS = false; PerfFlags.SCOPE_POOL = false
}
val shuffle = boolProp("lyng.profile.shuffle", true)
val order = if (shuffle) scenarios.shuffled(Random(System.nanoTime())) else scenarios
val results = mutableMapOf<String, ProfileResult>()
for (sc in order) {
log("[DEBUG_LOG] Scenario ${sc.label}: ${sc.title}")
results[sc.label] = runScenario(sc.label, prepare = sc.prep, out = ::log)
}
// Summary vs A if measured
val a = results["A"]
if (a != null) {
log("[DEBUG_LOG] Summary deltas vs A (medians):")
fun deltaLine(name: String, r: ProfileResult) = "[DEBUG_LOG] ${name} - A: time=${r.timeNs - a.timeNs} ns, alloc=${r.allocBytes - a.allocBytes} B"
listOf("B","C","D","E","F","G").forEach { k ->
results[k]?.let { r -> log(deltaLine(k, r)) }
}
}
} finally {
saved.restore()
}
}
}
// Minimal runBlocking bridge to avoid extra test deps here
private fun runTestBlocking(block: suspend () -> Unit) {
kotlinx.coroutines.runBlocking { block() }
}
@@ -0,0 +1,156 @@
/*
* Copyright 2025 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjInt
import java.io.File
import kotlin.system.measureNanoTime
import kotlin.test.Test
class CallArgPipelineABTest {
private fun outFile(): File = File("lynglib/build/call_ab_results.txt")
private fun writeHeader(f: File) {
if (!f.parentFile.exists()) f.parentFile.mkdirs()
f.writeText("[DEBUG_LOG] Call/Arg pipeline A/B results\n")
}
private fun appendLine(f: File, s: String) {
f.appendText(s + "\n")
}
private suspend fun buildScriptForCalls(arity: Int, iters: Int): Script {
val argsDecl = (0 until arity).joinToString(",") { "a$it" }
val argsUse = (0 until arity).joinToString(" + ") { "a$it" }.ifEmpty { "0" }
val callArgs = (0 until arity).joinToString(",") { (it + 1).toString() }
val src = """
var sum = 0
fun f($argsDecl) { $argsUse }
for(i in 0..${iters - 1}) {
sum += f($callArgs)
}
sum
""".trimIndent()
return Compiler.compile(Source("<calls-$arity>", src), Script.defaultImportManager)
}
private suspend fun benchCallsOnce(arity: Int, iters: Int): Long {
val script = buildScriptForCalls(arity, iters)
val scope = Script.newScope()
var result: Obj? = null
val t = measureNanoTime {
result = script.execute(scope)
}
// Basic correctness check so JIT doesn’t elide
val expected = (0 until iters).fold(0L) { acc, _ ->
(acc + (1L + 2L + 3L + 4L + 5L + 6L + 7L + 8L).let { if (arity <= 8) it - (8 - arity) * 0L else it })
}
// We only rely that it runs; avoid strict equals as function may compute differently for arities < 8
if (result !is ObjInt) {
// ensure use to prevent DCE
println("[DEBUG_LOG] Result class=${result?.javaClass?.simpleName}")
}
return t
}
private suspend fun benchOptionalCallShortCircuit(iters: Int): Long {
val src = """
var side = 0
fun inc() { side += 1 }
var o = null
for(i in 0..${iters - 1}) {
o?.foo(inc())
}
side
""".trimIndent()
val script = Compiler.compile(Source("<opt-call>", src), Script.defaultImportManager)
val scope = Script.newScope()
var result: Obj? = null
val t = measureNanoTime { result = script.execute(scope) }
// Ensure short-circuit actually happened
require((result as? ObjInt)?.value == 0L) { "optional-call short-circuit failed; side=${(result as? ObjInt)?.value}" }
return t
}
@Test
fun ab_call_pipeline() = runTestBlocking {
val f = outFile()
writeHeader(f)
val savedArgBuilder = PerfFlags.ARG_BUILDER
val savedScopePool = PerfFlags.SCOPE_POOL
try {
val iters = 50_000
val aritiesBase = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8)
// A/B for ARG_BUILDER (0..8)
PerfFlags.ARG_BUILDER = false
val offTimes = mutableListOf<Long>()
for (a in aritiesBase) offTimes += benchCallsOnce(a, iters)
PerfFlags.ARG_BUILDER = true
val onTimes = mutableListOf<Long>()
for (a in aritiesBase) onTimes += benchCallsOnce(a, iters)
appendLine(f, "[DEBUG_LOG] ARG_BUILDER A/B (iters=$iters):")
aritiesBase.forEachIndexed { idx, a ->
appendLine(f, "[DEBUG_LOG] arity=$a OFF=${offTimes[idx]} ns, ON=${onTimes[idx]} ns, delta=${offTimes[idx] - onTimes[idx]} ns")
}
// A/B for ARG_SMALL_ARITY_12 (9..12)
val aritiesExtended = listOf(9, 10, 11, 12)
val savedSmall = PerfFlags.ARG_SMALL_ARITY_12
try {
PerfFlags.ARG_BUILDER = true // base builder on
PerfFlags.ARG_SMALL_ARITY_12 = false
val offExt = mutableListOf<Long>()
for (a in aritiesExtended) offExt += benchCallsOnce(a, iters)
PerfFlags.ARG_SMALL_ARITY_12 = true
val onExt = mutableListOf<Long>()
for (a in aritiesExtended) onExt += benchCallsOnce(a, iters)
appendLine(f, "[DEBUG_LOG] ARG_SMALL_ARITY_12 A/B (iters=$iters):")
aritiesExtended.forEachIndexed { idx, a ->
appendLine(f, "[DEBUG_LOG] arity=$a OFF=${offExt[idx]} ns, ON=${onExt[idx]} ns, delta=${offExt[idx] - onExt[idx]} ns")
}
} finally {
PerfFlags.ARG_SMALL_ARITY_12 = savedSmall
}
// Optional call short-circuit sanity timing (does not A/B a flag currently; implementation short-circuits before args)
val tOpt = benchOptionalCallShortCircuit(100_000)
appendLine(f, "[DEBUG_LOG] Optional-call short-circuit sanity: ${tOpt} ns for 100k iterations (side-effect arg not evaluated).")
// A/B for SCOPE_POOL
PerfFlags.SCOPE_POOL = false
val tPoolOff = benchCallsOnce(5, iters)
PerfFlags.SCOPE_POOL = true
val tPoolOn = benchCallsOnce(5, iters)
appendLine(f, "[DEBUG_LOG] SCOPE_POOL A/B (arity=5, iters=$iters): OFF=${tPoolOff} ns, ON=${tPoolOn} ns, delta=${tPoolOff - tPoolOn} ns")
} finally {
PerfFlags.ARG_BUILDER = savedArgBuilder
PerfFlags.SCOPE_POOL = savedScopePool
}
}
}
// Minimal runBlocking for common jvmTest without depending on kotlinx.coroutines test artifacts here
private fun runTestBlocking(block: suspend () -> Unit) {
kotlinx.coroutines.runBlocking { block() }
}
@@ -0,0 +1,137 @@
/*
* Copyright 2025 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjInt
import java.io.File
import kotlin.system.measureNanoTime
import kotlin.test.Test
class IndexPicABTest {
private fun outFile(): File = File("lynglib/build/index_pic_ab_results.txt")
private fun writeHeader(f: File) {
if (!f.parentFile.exists()) f.parentFile.mkdirs()
f.writeText("[DEBUG_LOG] Index PIC A/B results\n")
}
private fun appendLine(f: File, s: String) { f.appendText(s + "\n") }
private suspend fun buildStringIndexScript(len: Int, iters: Int): Script {
// Build a long string and index it by cycling positions
val content = (0 until len).joinToString("") { i ->
val ch = 'a' + (i % 26)
ch.toString()
}
val src = """
val s = "$content"
var acc = 0
for(i in 0..${iters - 1}) {
val j = i % ${len}
// Compare to a 1-char string to avoid needing Char.toInt(); still exercises indexing path
if (s[j] == "a") { acc += 1 } else { acc += 0 }
}
acc
""".trimIndent()
return Compiler.compile(Source("<idx-string>", src), Script.defaultImportManager)
}
private suspend fun buildMapIndexScript(keys: Int, iters: Int): Script {
// Build a map of ("kX" -> X) and repeatedly access by key cycling
val entries = (0 until keys).joinToString(", ") { i -> "\"k$i\" => $i" }
val src = """
// Build via Map(entry1, entry2, ...), not a list literal
val m = Map($entries)
var acc = 0
for(i in 0..${iters - 1}) {
val k = "k" + (i % ${keys})
acc += (m[k] ?: 0)
}
acc
""".trimIndent()
return Compiler.compile(Source("<idx-map>", src), Script.defaultImportManager)
}
private suspend fun runOnce(script: Script): Long {
val scope = Script.newScope()
var result: Obj? = null
val t = measureNanoTime { result = script.execute(scope) }
if (result !is ObjInt) println("[DEBUG_LOG] result=${result?.javaClass?.simpleName}")
return t
}
@Test
fun ab_index_pic_and_size() = runTestBlocking {
val f = outFile()
writeHeader(f)
val savedIndexPic = PerfFlags.INDEX_PIC
val savedIndexSize4 = PerfFlags.INDEX_PIC_SIZE_4
val savedCounters = PerfFlags.PIC_DEBUG_COUNTERS
try {
val iters = 300_000
val sLen = 512
val mapKeys = 256
val sScript = buildStringIndexScript(sLen, iters)
val mScript = buildMapIndexScript(mapKeys, iters)
fun header(which: String) { appendLine(f, "[DEBUG_LOG] A/B on $which (iters=$iters)") }
// Baseline OFF
PerfFlags.PIC_DEBUG_COUNTERS = true
PerfStats.resetAll()
PerfFlags.INDEX_PIC = false
PerfFlags.INDEX_PIC_SIZE_4 = false
header("String[Int], INDEX_PIC=OFF")
val tSOff = runOnce(sScript)
header("Map[String], INDEX_PIC=OFF")
val tMOff = runOnce(mScript)
appendLine(f, "[DEBUG_LOG] OFF counters: indexHit=${PerfStats.indexPicHit} indexMiss=${PerfStats.indexPicMiss}")
// PIC ON, size 2
PerfStats.resetAll()
PerfFlags.INDEX_PIC = true
PerfFlags.INDEX_PIC_SIZE_4 = false
val tSOn2 = runOnce(sScript)
val tMOn2 = runOnce(mScript)
appendLine(f, "[DEBUG_LOG] ON size=2 counters: indexHit=${PerfStats.indexPicHit} indexMiss=${PerfStats.indexPicMiss}")
// PIC ON, size 4
PerfStats.resetAll()
PerfFlags.INDEX_PIC = true
PerfFlags.INDEX_PIC_SIZE_4 = true
val tSOn4 = runOnce(sScript)
val tMOn4 = runOnce(mScript)
appendLine(f, "[DEBUG_LOG] ON size=4 counters: indexHit=${PerfStats.indexPicHit} indexMiss=${PerfStats.indexPicMiss}")
// Report
appendLine(f, "[DEBUG_LOG] String[Int] OFF=${tSOff} ns, ON(2)=${tSOn2} ns, ON(4)=${tSOn4} ns")
appendLine(f, "[DEBUG_LOG] Map[String] OFF=${tMOff} ns, ON(2)=${tMOn2} ns, ON(4)=${tMOn4} ns")
} finally {
PerfFlags.INDEX_PIC = savedIndexPic
PerfFlags.INDEX_PIC_SIZE_4 = savedIndexSize4
PerfFlags.PIC_DEBUG_COUNTERS = savedCounters
}
}
}
private fun runTestBlocking(block: suspend () -> Unit) {
kotlinx.coroutines.runBlocking { block() }
}
@@ -0,0 +1,139 @@
/*
* Copyright 2025 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjInt
import java.io.File
import kotlin.system.measureNanoTime
import kotlin.test.Test
/**
* A/B micro-benchmark for index WRITE paths (Map[String] put, List[Int] set).
* Measures OFF vs ON for INDEX_PIC and then size 2 vs 4 (INDEX_PIC_SIZE_4).
* Produces [DEBUG_LOG] output in lynglib/build/index_write_ab_results.txt
*/
class IndexWritePathABTest {
private fun outFile(): File = File("lynglib/build/index_write_ab_results.txt")
private fun writeHeader(f: File) {
if (!f.parentFile.exists()) f.parentFile.mkdirs()
f.writeText("[DEBUG_LOG] Index WRITE PIC A/B results\n")
}
private fun appendLine(f: File, s: String) { f.appendText(s + "\n") }
private suspend fun buildMapWriteScript(keys: Int, iters: Int): Script {
// Construct map with keys k0..k{keys-1} and then perform writes in a tight loop
val initEntries = (0 until keys).joinToString(", ") { i -> "\"k$i\" => $i" }
val src = """
var acc = 0
val m = Map($initEntries)
for(i in 0..${iters - 1}) {
val k = "k" + (i % $keys)
m[k] = i
acc += (m[k] ?: 0)
}
acc
""".trimIndent()
return Compiler.compile(Source("<idx-map-write>", src), Script.defaultImportManager)
}
private suspend fun buildListWriteScript(len: Int, iters: Int): Script {
val initList = (0 until len).joinToString(", ") { i -> i.toString() }
val src = """
var acc = 0
val a = [$initList]
for(i in 0..${iters - 1}) {
val j = i % $len
a[j] = i
acc += a[j]
}
acc
""".trimIndent()
return Compiler.compile(Source("<idx-list-write>", src), Script.defaultImportManager)
}
private suspend fun runOnce(script: Script): Long {
val scope = Script.newScope()
var result: Obj? = null
val t = measureNanoTime { result = script.execute(scope) }
if (result !is ObjInt) println("[DEBUG_LOG] result=${result?.javaClass?.simpleName}")
return t
}
@Test
fun ab_index_write_paths() = runTestBlocking {
val f = outFile()
writeHeader(f)
val savedIndexPic = PerfFlags.INDEX_PIC
val savedIndexSize4 = PerfFlags.INDEX_PIC_SIZE_4
val savedCounters = PerfFlags.PIC_DEBUG_COUNTERS
try {
val iters = 250_000
val mapKeys = 256
val listLen = 1024
val mScript = buildMapWriteScript(mapKeys, iters)
val lScript = buildListWriteScript(listLen, iters)
fun header(which: String) { appendLine(f, "[DEBUG_LOG] A/B on $which (iters=$iters)") }
// Baseline OFF
PerfFlags.PIC_DEBUG_COUNTERS = true
PerfStats.resetAll()
PerfFlags.INDEX_PIC = false
PerfFlags.INDEX_PIC_SIZE_4 = false
header("Map[String] write, INDEX_PIC=OFF")
val tMOff = runOnce(mScript)
header("List[Int] write, INDEX_PIC=OFF")
val tLOff = runOnce(lScript)
appendLine(f, "[DEBUG_LOG] OFF counters: indexHit=${PerfStats.indexPicHit} indexMiss=${PerfStats.indexPicMiss}")
// PIC ON, size 2
PerfStats.resetAll()
PerfFlags.INDEX_PIC = true
PerfFlags.INDEX_PIC_SIZE_4 = false
val tMOn2 = runOnce(mScript)
val tLOn2 = runOnce(lScript)
appendLine(f, "[DEBUG_LOG] ON size=2 counters: indexHit=${PerfStats.indexPicHit} indexMiss=${PerfStats.indexPicMiss}")
// PIC ON, size 4
PerfStats.resetAll()
PerfFlags.INDEX_PIC = true
PerfFlags.INDEX_PIC_SIZE_4 = true
val tMOn4 = runOnce(mScript)
val tLOn4 = runOnce(lScript)
appendLine(f, "[DEBUG_LOG] ON size=4 counters: indexHit=${PerfStats.indexPicHit} indexMiss=${PerfStats.indexPicMiss}")
// Report
appendLine(f, "[DEBUG_LOG] Map[String] WRITE OFF=$tMOff ns, ON(2)=$tMOn2 ns, ON(4)=$tMOn4 ns")
appendLine(f, "[DEBUG_LOG] List[Int] WRITE OFF=$tLOff ns, ON(2)=$tLOn2 ns, ON(4)=$tLOn4 ns")
} finally {
PerfFlags.INDEX_PIC = savedIndexPic
PerfFlags.INDEX_PIC_SIZE_4 = savedIndexSize4
PerfFlags.PIC_DEBUG_COUNTERS = savedCounters
}
}
}
private fun runTestBlocking(block: suspend () -> Unit) {
kotlinx.coroutines.runBlocking { block() }
}
@@ -0,0 +1,76 @@
/*
* Copyright 2025 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class PerfProfilesTest {
@Test
fun apply_and_restore_presets() {
val before = PerfProfiles.snapshot()
try {
// BENCH preset expectations
val snapAfterBench = PerfProfiles.apply(PerfProfiles.Preset.BENCH)
// Expect some key flags ON for benches
assertTrue(PerfFlags.ARG_BUILDER)
assertTrue(PerfFlags.SCOPE_POOL)
assertTrue(PerfFlags.FIELD_PIC)
assertTrue(PerfFlags.METHOD_PIC)
assertTrue(PerfFlags.INDEX_PIC)
assertTrue(PerfFlags.INDEX_PIC_SIZE_4)
assertTrue(PerfFlags.PRIMITIVE_FASTOPS)
assertTrue(PerfFlags.RVAL_FASTPATH)
// Restore via snapshot returned by apply
PerfProfiles.restore(snapAfterBench)
// BOOKS preset expectations
val snapAfterBooks = PerfProfiles.apply(PerfProfiles.Preset.BOOKS)
// Expect simpler paths enabled/disabled accordingly
assertEquals(false, PerfFlags.ARG_BUILDER)
assertEquals(false, PerfFlags.SCOPE_POOL)
assertEquals(false, PerfFlags.FIELD_PIC)
assertEquals(false, PerfFlags.METHOD_PIC)
assertEquals(false, PerfFlags.INDEX_PIC)
assertEquals(false, PerfFlags.INDEX_PIC_SIZE_4)
assertEquals(false, PerfFlags.PRIMITIVE_FASTOPS)
assertEquals(false, PerfFlags.RVAL_FASTPATH)
// Restore via snapshot returned by apply
PerfProfiles.restore(snapAfterBooks)
// BASELINE preset should match PerfDefaults
val snapAfterBaseline = PerfProfiles.apply(PerfProfiles.Preset.BASELINE)
assertEquals(PerfDefaults.ARG_BUILDER, PerfFlags.ARG_BUILDER)
assertEquals(PerfDefaults.SCOPE_POOL, PerfFlags.SCOPE_POOL)
assertEquals(PerfDefaults.FIELD_PIC, PerfFlags.FIELD_PIC)
assertEquals(PerfDefaults.METHOD_PIC, PerfFlags.METHOD_PIC)
assertEquals(PerfDefaults.INDEX_PIC_SIZE_4, PerfFlags.INDEX_PIC_SIZE_4)
assertEquals(PerfDefaults.PRIMITIVE_FASTOPS, PerfFlags.PRIMITIVE_FASTOPS)
assertEquals(PerfDefaults.RVAL_FASTPATH, PerfFlags.RVAL_FASTPATH)
// Restore baseline snapshot
PerfProfiles.restore(snapAfterBaseline)
} finally {
// Finally, restore very original snapshot
PerfProfiles.restore(before)
}
}
}
@@ -0,0 +1,154 @@
/*
* Copyright 2025 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjInt
import java.io.File
import kotlin.system.measureNanoTime
import kotlin.test.Test
class PicAdaptiveABTest {
private fun outFile(): File = File("lynglib/build/pic_adaptive_ab_results.txt")
private fun writeHeader(f: File) {
if (!f.parentFile.exists()) f.parentFile.mkdirs()
f.writeText("[DEBUG_LOG] PIC Adaptive 2→4 A/B results\n")
}
private fun appendLine(f: File, s: String) { f.appendText(s + "\n") }
private suspend fun buildScriptForMethodShapes(shapes: Int, iters: Int): Script {
// Define N classes C0..C{shapes-1} each with method f() { 1 }
val classes = (0 until shapes).joinToString("\n") { i ->
"class C$i { fun f() { $i } var x = 0 }"
}
val inits = (0 until shapes).joinToString(", ") { i -> "C$i()" }
val calls = buildString {
append("var s = 0\n")
append("val a = [${inits}]\n")
append("for(i in 0..${iters - 1}) {\n")
append(" val o = a[i % ${shapes}]\n")
append(" s += o.f()\n")
append("}\n")
append("s\n")
}
val src = classes + "\n" + calls
return Compiler.compile(Source("<pic-method-shapes>", src), Script.defaultImportManager)
}
private suspend fun buildScriptForFieldShapes(shapes: Int, iters: Int): Script {
// Each class has a mutable field x initialized to 0; read and write it
val classes = (0 until shapes).joinToString("\n") { i ->
"class F$i { var x = 0 }"
}
val inits = (0 until shapes).joinToString(", ") { i -> "F$i()" }
val body = buildString {
append("var s = 0\n")
append("val a = [${inits}]\n")
append("for(i in 0..${iters - 1}) {\n")
append(" val o = a[i % ${shapes}]\n")
append(" s += o.x\n")
append(" o.x = o.x + 1\n")
append("}\n")
append("s\n")
}
val src = classes + "\n" + body
return Compiler.compile(Source("<pic-field-shapes>", src), Script.defaultImportManager)
}
private suspend fun runOnce(script: Script): Long {
val scope = Script.newScope()
var result: Obj? = null
val t = measureNanoTime { result = script.execute(scope) }
if (result !is ObjInt) println("[DEBUG_LOG] result=${result?.javaClass?.simpleName}")
return t
}
@Test
fun ab_adaptive_pic() = runTestBlocking {
val f = outFile()
writeHeader(f)
val savedAdaptive = PerfFlags.PIC_ADAPTIVE_2_TO_4
val savedCounters = PerfFlags.PIC_DEBUG_COUNTERS
val savedFieldPic = PerfFlags.FIELD_PIC
val savedMethodPic = PerfFlags.METHOD_PIC
val savedFieldPicSize4 = PerfFlags.FIELD_PIC_SIZE_4
val savedMethodPicSize4 = PerfFlags.METHOD_PIC_SIZE_4
try {
// Ensure baseline PICs are enabled and fixed-size flags OFF to isolate adaptivity
PerfFlags.FIELD_PIC = true
PerfFlags.METHOD_PIC = true
PerfFlags.FIELD_PIC_SIZE_4 = false
PerfFlags.METHOD_PIC_SIZE_4 = false
// Prepare workloads with 3 and 4 receiver shapes
val iters = 200_000
val meth3 = buildScriptForMethodShapes(3, iters)
val meth4 = buildScriptForMethodShapes(4, iters)
val fld3 = buildScriptForFieldShapes(3, iters)
val fld4 = buildScriptForFieldShapes(4, iters)
fun header(which: String) {
appendLine(f, "[DEBUG_LOG] A/B Adaptive PIC on $which (iters=$iters)")
}
// OFF pass
PerfFlags.PIC_DEBUG_COUNTERS = true
PerfStats.resetAll()
PerfFlags.PIC_ADAPTIVE_2_TO_4 = false
header("methods-3")
val tM3Off = runOnce(meth3)
header("methods-4")
val tM4Off = runOnce(meth4)
header("fields-3")
val tF3Off = runOnce(fld3)
header("fields-4")
val tF4Off = runOnce(fld4)
appendLine(f, "[DEBUG_LOG] OFF counters: methodHit=${PerfStats.methodPicHit} methodMiss=${PerfStats.methodPicMiss} fieldHit=${PerfStats.fieldPicHit} fieldMiss=${PerfStats.fieldPicMiss}")
// ON pass
PerfStats.resetAll()
PerfFlags.PIC_ADAPTIVE_2_TO_4 = true
val tM3On = runOnce(meth3)
val tM4On = runOnce(meth4)
val tF3On = runOnce(fld3)
val tF4On = runOnce(fld4)
appendLine(f, "[DEBUG_LOG] ON counters: methodHit=${PerfStats.methodPicHit} methodMiss=${PerfStats.methodPicMiss} fieldHit=${PerfStats.fieldPicHit} fieldMiss=${PerfStats.fieldPicMiss}")
// Report
appendLine(f, "[DEBUG_LOG] methods-3 OFF=${tM3Off} ns, ON=${tM3On} ns, delta=${tM3Off - tM3On} ns")
appendLine(f, "[DEBUG_LOG] methods-4 OFF=${tM4Off} ns, ON=${tM4On} ns, delta=${tM4Off - tM4On} ns")
appendLine(f, "[DEBUG_LOG] fields-3 OFF=${tF3Off} ns, ON=${tF3On} ns, delta=${tF3Off - tF3On} ns")
appendLine(f, "[DEBUG_LOG] fields-4 OFF=${tF4Off} ns, ON=${tF4On} ns, delta=${tF4Off - tF4On} ns")
} finally {
PerfFlags.PIC_ADAPTIVE_2_TO_4 = savedAdaptive
PerfFlags.PIC_DEBUG_COUNTERS = savedCounters
PerfFlags.FIELD_PIC = savedFieldPic
PerfFlags.METHOD_PIC = savedMethodPic
PerfFlags.FIELD_PIC_SIZE_4 = savedFieldPicSize4
PerfFlags.METHOD_PIC_SIZE_4 = savedMethodPicSize4
}
}
}
private fun runTestBlocking(block: suspend () -> Unit) {
kotlinx.coroutines.runBlocking { block() }
}
@@ -0,0 +1,132 @@
/*
* Copyright 2025 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjInt
import java.io.File
import kotlin.system.measureNanoTime
import kotlin.test.Test
/**
* A/B micro-benchmark to compare methods-only adaptive PIC OFF vs ON.
* Ensures fixed PIC sizes (2-entry) and only toggles PIC_ADAPTIVE_METHODS_ONLY.
* Writes a summary to lynglib/build/pic_methods_only_adaptive_ab_results.txt
*/
class PicMethodsOnlyAdaptiveABTest {
private fun outFile(): File = File("lynglib/build/pic_methods_only_adaptive_ab_results.txt")
private fun writeHeader(f: File) {
if (!f.parentFile.exists()) f.parentFile.mkdirs()
f.writeText("[DEBUG_LOG] PIC Adaptive (methods-only) 2→4 A/B results\n")
}
private fun appendLine(f: File, s: String) { f.appendText(s + "\n") }
private suspend fun buildScriptForMethodShapes(shapes: Int, iters: Int): Script {
// Define N classes C0..C{shapes-1} each with method f() { i }
val classes = (0 until shapes).joinToString("\n") { i ->
"class MC$i { fun f() { $i } }"
}
val inits = (0 until shapes).joinToString(", ") { i -> "MC$i()" }
val body = buildString {
append("var s = 0\n")
append("val a = [${inits}]\n")
append("for(i in 0..${iters - 1}) {\n")
append(" val o = a[i % ${shapes}]\n")
append(" s += o.f()\n")
append("}\n")
append("s\n")
}
val src = classes + "\n" + body
return Compiler.compile(Source("<pic-meth-only-shapes>", src), Script.defaultImportManager)
}
private suspend fun runOnce(script: Script): Long {
val scope = Script.newScope()
var result: Obj? = null
val t = measureNanoTime { result = script.execute(scope) }
if (result !is ObjInt) println("[DEBUG_LOG] result=${result?.javaClass?.simpleName}")
return t
}
@Test
fun ab_methods_only_adaptive_pic() = runTestBlocking {
val f = outFile()
writeHeader(f)
// Save flags
val savedAdaptive2To4 = PerfFlags.PIC_ADAPTIVE_2_TO_4
val savedAdaptiveMethodsOnly = PerfFlags.PIC_ADAPTIVE_METHODS_ONLY
val savedFieldPic = PerfFlags.FIELD_PIC
val savedMethodPic = PerfFlags.METHOD_PIC
val savedFieldSize4 = PerfFlags.FIELD_PIC_SIZE_4
val savedMethodSize4 = PerfFlags.METHOD_PIC_SIZE_4
val savedCounters = PerfFlags.PIC_DEBUG_COUNTERS
try {
// Fixed-size 2-entry PICs, enable PICs, disable global adaptivity
PerfFlags.FIELD_PIC = true
PerfFlags.METHOD_PIC = true
PerfFlags.FIELD_PIC_SIZE_4 = false
PerfFlags.METHOD_PIC_SIZE_4 = false
PerfFlags.PIC_ADAPTIVE_2_TO_4 = false
val iters = 200_000
val meth3 = buildScriptForMethodShapes(3, iters)
val meth4 = buildScriptForMethodShapes(4, iters)
fun header(which: String) { appendLine(f, "[DEBUG_LOG] A/B Methods-only adaptive on $which (iters=$iters)") }
// OFF pass
PerfFlags.PIC_DEBUG_COUNTERS = true
PerfStats.resetAll()
PerfFlags.PIC_ADAPTIVE_METHODS_ONLY = false
header("methods-3")
val tM3Off = runOnce(meth3)
header("methods-4")
val tM4Off = runOnce(meth4)
appendLine(f, "[DEBUG_LOG] OFF counters: methodHit=${PerfStats.methodPicHit} methodMiss=${PerfStats.methodPicMiss}")
// ON pass
PerfStats.resetAll()
PerfFlags.PIC_ADAPTIVE_METHODS_ONLY = true
val tM3On = runOnce(meth3)
val tM4On = runOnce(meth4)
appendLine(f, "[DEBUG_LOG] ON counters: methodHit=${PerfStats.methodPicHit} methodMiss=${PerfStats.methodPicMiss}")
// Report
appendLine(f, "[DEBUG_LOG] methods-3 OFF=${tM3Off} ns, ON=${tM3On} ns, delta=${tM3Off - tM3On} ns")
appendLine(f, "[DEBUG_LOG] methods-4 OFF=${tM4Off} ns, ON=${tM4On} ns, delta=${tM4Off - tM4On} ns")
} finally {
// Restore
PerfFlags.PIC_ADAPTIVE_2_TO_4 = savedAdaptive2To4
PerfFlags.PIC_ADAPTIVE_METHODS_ONLY = savedAdaptiveMethodsOnly
PerfFlags.FIELD_PIC = savedFieldPic
PerfFlags.METHOD_PIC = savedMethodPic
PerfFlags.FIELD_PIC_SIZE_4 = savedFieldSize4
PerfFlags.METHOD_PIC_SIZE_4 = savedMethodSize4
PerfFlags.PIC_DEBUG_COUNTERS = savedCounters
}
}
}
private fun runTestBlocking(block: suspend () -> Unit) {
kotlinx.coroutines.runBlocking { block() }
}
@@ -0,0 +1,112 @@
/*
* Copyright 2025 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
/*
* A/B micro-benchmark to compare PerfFlags.PRIMITIVE_FASTOPS OFF vs ON.
* JVM-only quick check using simple arithmetic/logic loops.
*/
package net.sergeych.lyng
import java.io.File
import kotlin.system.measureNanoTime
import kotlin.test.Test
class PrimitiveFastOpsABTest {
private fun outFile(): File = File("lynglib/build/primitive_ab_results.txt")
private fun writeHeader(f: File) {
if (!f.parentFile.exists()) f.parentFile.mkdirs()
f.writeText("[DEBUG_LOG] Primitive FastOps A/B results\n")
}
private fun appendLine(f: File, s: String) {
f.appendText(s + "\n")
}
private fun benchIntArithmeticIters(iters: Int): Long {
var acc = 0L
val t = measureNanoTime {
var a = 1L
var b = 2L
var c = 3L
repeat(iters) {
// mimic mix of +, -, *, /, %, shifts and comparisons
a = (a + b) xor c
b = (b * 3L + a) and 0x7FFF_FFFFL
if ((b and 1L) == 0L) c = c + 1L else c = c - 1L
acc = acc + (a and b) + (c or a)
}
}
// use acc to prevent DCE
if (acc == 42L) println("[DEBUG_LOG] impossible")
return t
}
private fun benchBoolLogicIters(iters: Int): Long {
var acc = 0
val t = measureNanoTime {
var a = true
var b = false
repeat(iters) {
a = a || b
b = !b && a
if (a == b) acc++ else acc--
}
}
if (acc == Int.MIN_VALUE) println("[DEBUG_LOG] impossible2")
return t
}
@Test
fun ab_compare_primitive_fastops() {
// Save current settings
val savedFast = PerfFlags.PRIMITIVE_FASTOPS
val savedCounters = PerfFlags.PIC_DEBUG_COUNTERS
val f = outFile()
writeHeader(f)
try {
val iters = 500_000
// OFF pass
PerfFlags.PIC_DEBUG_COUNTERS = true
PerfStats.resetAll()
PerfFlags.PRIMITIVE_FASTOPS = false
val tArithOff = benchIntArithmeticIters(iters)
val tLogicOff = benchBoolLogicIters(iters)
// ON pass
PerfStats.resetAll()
PerfFlags.PRIMITIVE_FASTOPS = true
val tArithOn = benchIntArithmeticIters(iters)
val tLogicOn = benchBoolLogicIters(iters)
println("[DEBUG_LOG] A/B PrimitiveFastOps (iters=$iters):")
println("[DEBUG_LOG] Arithmetic OFF: ${'$'}tArithOff ns, ON: ${'$'}tArithOn ns, delta: ${'$'}{tArithOff - tArithOn} ns")
println("[DEBUG_LOG] Bool logic OFF: ${'$'}tLogicOff ns, ON: ${'$'}tLogicOn ns, delta: ${'$'}{tLogicOff - tLogicOn} ns")
appendLine(f, "[DEBUG_LOG] A/B PrimitiveFastOps (iters=$iters):")
appendLine(f, "[DEBUG_LOG] Arithmetic OFF: ${'$'}tArithOff ns, ON: ${'$'}tArithOn ns, delta: ${'$'}{tArithOff - tArithOn} ns")
appendLine(f, "[DEBUG_LOG] Bool logic OFF: ${'$'}tLogicOff ns, ON: ${'$'}tLogicOn ns, delta: ${'$'}{tLogicOff - tLogicOn} ns")
} finally {
// restore
PerfFlags.PRIMITIVE_FASTOPS = savedFast
PerfFlags.PIC_DEBUG_COUNTERS = savedCounters
}
}
}
@@ -0,0 +1,171 @@
/*
* Copyright 2025 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjInt
import java.io.File
import kotlin.system.measureNanoTime
import kotlin.test.Test
/**
* Baseline range iteration benchmark. It measures for-loops over integer ranges under
* current implementation and records timings. When RANGE_FAST_ITER is implemented,
* this test will also serve for OFF vs ON A/B.
*/
class RangeIterationBenchmarkTest {
private fun outFile(): File = File("lynglib/build/range_iter_bench.txt")
private fun writeHeader(f: File) {
if (!f.parentFile.exists()) f.parentFile.mkdirs()
f.writeText("[DEBUG_LOG] Range iteration benchmark results\n")
}
private fun appendLine(f: File, s: String) { f.appendText(s + "\n") }
private suspend fun buildSumScriptInclusive(n: Int, iters: Int): Script {
// Sum 0..n repeatedly to stress iteration
val src = """
var total = 0
for (k in 0..${iters - 1}) {
var s = 0
for (i in 0..$n) { s += i }
total += s
}
total
""".trimIndent()
return Compiler.compile(Source("<range-inc>", src), Script.defaultImportManager)
}
private suspend fun buildSumScriptExclusive(n: Int, iters: Int): Script {
val src = """
var total = 0
for (k in 0..${iters - 1}) {
var s = 0
for (i in 0..<$n) { s += i }
total += s
}
total
""".trimIndent()
return Compiler.compile(Source("<range-exc>", src), Script.defaultImportManager)
}
private suspend fun buildSumScriptReversed(n: Int, iters: Int): Script {
val src = """
var total = 0
for (k in 0..${iters - 1}) {
var s = 0
// reversed-like loop using countdown range (n..0)
for (i in $n..0) { s += i }
total += s
}
total
""".trimIndent()
return Compiler.compile(Source("<range-rev>", src), Script.defaultImportManager)
}
private suspend fun buildSumScriptNegative(n: Int, iters: Int): Script {
// Sum -n..n repeatedly
val src = """
var total = 0
for (k in 0..${iters - 1}) {
var s = 0
for (i in -$n..$n) { s += (i < 0 ? -i : i) }
total += s
}
total
""".trimIndent()
return Compiler.compile(Source("<range-neg>", src), Script.defaultImportManager)
}
private suspend fun buildSumScriptEmpty(iters: Int): Script {
// Empty range 1..0 should not iterate
val src = """
var total = 0
for (k in 0..${iters - 1}) {
var s = 0
for (i in 1..0) { s += 1 }
total += s
}
total
""".trimIndent()
return Compiler.compile(Source("<range-empty>", src), Script.defaultImportManager)
}
private suspend fun runOnce(script: Script): Long {
val scope = Script.newScope()
var result: Obj? = null
val t = measureNanoTime { result = script.execute(scope) }
if (result !is ObjInt) println("[DEBUG_LOG] result=${result?.javaClass?.simpleName}")
return t
}
@Test
fun bench_range_iteration_baseline() = runTestBlocking {
val f = outFile()
writeHeader(f)
val savedFlag = PerfFlags.RANGE_FAST_ITER
try {
val n = 1000
val iters = 500
// Baseline with current flag (OFF by default)
PerfFlags.RANGE_FAST_ITER = false
val sIncOff = buildSumScriptInclusive(n, iters)
val tIncOff = runOnce(sIncOff)
val sExcOff = buildSumScriptExclusive(n, iters)
val tExcOff = runOnce(sExcOff)
appendLine(f, "[DEBUG_LOG] OFF inclusive=${tIncOff} ns, exclusive=${tExcOff} ns")
// Also record ON times
PerfFlags.RANGE_FAST_ITER = true
val sIncOn = buildSumScriptInclusive(n, iters)
val tIncOn = runOnce(sIncOn)
val sExcOn = buildSumScriptExclusive(n, iters)
val tExcOn = runOnce(sExcOn)
appendLine(f, "[DEBUG_LOG] ON inclusive=${tIncOn} ns, exclusive=${tExcOn} ns")
// Additional scenarios: reversed, negative, empty
PerfFlags.RANGE_FAST_ITER = false
val sRevOff = buildSumScriptReversed(n, iters)
val tRevOff = runOnce(sRevOff)
val sNegOff = buildSumScriptNegative(n, iters)
val tNegOff = runOnce(sNegOff)
val sEmptyOff = buildSumScriptEmpty(iters)
val tEmptyOff = runOnce(sEmptyOff)
appendLine(f, "[DEBUG_LOG] OFF reversed=${tRevOff} ns, negative=${tNegOff} ns, empty=${tEmptyOff} ns")
PerfFlags.RANGE_FAST_ITER = true
val sRevOn = buildSumScriptReversed(n, iters)
val tRevOn = runOnce(sRevOn)
val sNegOn = buildSumScriptNegative(n, iters)
val tNegOn = runOnce(sNegOn)
val sEmptyOn = buildSumScriptEmpty(iters)
val tEmptyOn = runOnce(sEmptyOn)
appendLine(f, "[DEBUG_LOG] ON reversed=${tRevOn} ns, negative=${tNegOn} ns, empty=${tEmptyOn} ns")
} finally {
PerfFlags.RANGE_FAST_ITER = savedFlag
}
}
}
private fun runTestBlocking(block: suspend () -> Unit) {
kotlinx.coroutines.runBlocking { block() }
}