Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

flatten: use loop and ArrayDeque instead of recursion #1801

Merged
merged 16 commits into from
Mar 29, 2024
Merged
6 changes: 4 additions & 2 deletions examples/jvm-perfs/benchmark.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
Benchmark Mode Cnt Score Error Units
BenchmarkClass.emptyStart avgt 10 ≈ 10⁻³ ms/op
BenchmarkClass.start400 avgt 10 0,272 ± 0,045 ms/op
BenchmarkClass.start400AndInject avgt 10 0,265 ± 0,022 ms/op
BenchmarkClass.flattenIterative avgt 10 0.643 ± 0.112 ms/op
BenchmarkClass.flattenRecursive avgt 10 1.297 ± 0.043 ms/op
BenchmarkClass.start400 avgt 10 0.297 ± 0.053 ms/op
BenchmarkClass.start400AndInject avgt 10 0.283 ± 0.007 ms/op
2 changes: 1 addition & 1 deletion examples/jvm-perfs/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ tasks.getByName<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>("compileKotlin"

val jmhVersion = "1.36"
//TODO get from existing version.gradle file
val koin_version = "3.5.2-RC1"
val koin_version = "3.5.3"
val coroutines_version = "1.7.3"

dependencies {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@ package org.koin.benchmark

import org.koin.benchmark.PerfRunner.koinScenario
import org.koin.dsl.koinApplication
import org.openjdk.jmh.annotations.*
import org.openjdk.jmh.annotations.Benchmark
import org.openjdk.jmh.annotations.BenchmarkMode
import org.openjdk.jmh.annotations.Fork
import org.openjdk.jmh.annotations.Measurement
import org.openjdk.jmh.annotations.Mode
import org.openjdk.jmh.annotations.OutputTimeUnit
import org.openjdk.jmh.annotations.Scope
import org.openjdk.jmh.annotations.Setup
import org.openjdk.jmh.annotations.State
import org.openjdk.jmh.annotations.Warmup
import java.util.concurrent.TimeUnit


@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Fork(value = 2, warmups = 0)
Expand Down Expand Up @@ -33,4 +41,31 @@ open class BenchmarkClass {
}.koin
koinScenario(koin)
}

@Benchmark
fun flattenRecursive(state: BenchmarkState) {
org.koin.core.module.flatten(state.nestedModules)
}

@Benchmark
fun flattenIterative(state: BenchmarkState) {
flattenIterative(state.nestedModules)
}
}

private val nestedModulesLazy = lazy {
buildNestedModule(
depth = 10,
width = 100,
)
}

@State(Scope.Benchmark)
open class BenchmarkState {
val nestedModules get() = nestedModulesLazy.value

@Setup
fun prepare() {
nestedModulesLazy.value
}
}
hoc081098 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.koin.benchmark

import org.koin.core.annotation.KoinInternalApi
import org.koin.core.module.Module
import org.koin.dsl.module

internal fun buildNestedModule(
depth: Int,
width: Int,
): List<Module> {
val matrix = List(depth) { List(width) { module {} } }

val level0Index = matrix.indices.random()
val level0 = matrix[level0Index]

matrix.forEachIndexed { r, modules ->
modules.forEachIndexed { c, module ->
// Let each module include the one above it.
matrix
.getOrNull(r + 1)
?.get(c)
?.let { module.includes(it) }

if (r != level0Index) {
module.includes(level0)
}
}
}

return matrix.first()
}

@OptIn(KoinInternalApi::class)
internal fun flattenIterative(modules: List<Module>): Set<Module> {
// This is actually a DFS traversal of the module graph,
// but we're using a stack instead of recursion to avoid stack overflows and performance overhead.

val flatten = HashSet<Module>()
val stack = ArrayDeque(modules)

while (stack.isNotEmpty()) {
val current = stack.removeLast()

// If the module is already in the set, that means we've already visited it, so we can skip it.
if (!flatten.add(current)) {
continue
}

// Add all the included modules to the stack if they haven't been visited yet.
for (module in current.includedModules) {
if (module !in flatten) {
stack += module
}
}
}

return flatten
}
Original file line number Diff line number Diff line change
Expand Up @@ -236,11 +236,27 @@ operator fun List<Module>.plus(module: Module): List<Module> = this + listOf(mod
*/
@OptIn(KoinInternalApi::class)
fun flatten(modules: List<Module>): Set<Module> {
fun flat(modules: List<Module>, newModules: MutableSet<Module>){
modules.forEach{
newModules += it
flat(it.includedModules,newModules)
// This is actually a DFS traversal of the module graph,
// but we're using a stack instead of recursion to avoid stack overflows and performance overhead.

val flatten = HashSet<Module>()
val stack = ArrayDeque(modules)

while (stack.isNotEmpty()) {
val current = stack.removeLast()

// If the module is already in the set, that means we've already visited it, so we can skip it.
if (!flatten.add(current)) {
continue
}

// Add all the included modules to the stack if they haven't been visited yet.
for (module in current.includedModules) {
if (module !in flatten) {
stack += module
}
}
}
return mutableSetOf<Module>().apply { flat(modules,this) }

return flatten
}