Embedded code performance
Hi! I am trying to use GraalVM from Kotlin to execute a Python script. I need to pass some input parameters, evaluate a script and read the result as a json.
I did a simple comparison with Jython:
package org.example.test
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.graalvm.polyglot.Context
import org.graalvm.polyglot.Engine
import org.graalvm.polyglot.Source
import org.python.util.PythonInterpreter
import kotlin.time.measureTime
fun main() {
graalvm()
jython()
}
val mapper = jacksonObjectMapper()
private fun graalvm() {
val duration = measureTime {
val engine = Engine.newBuilder()
.option("engine.WarnInterpreterOnly", "false")
.build()
val script = Source.create("python", "{'name':'John', 'id': id}")
repeat(100) {
val ctx = Context.newBuilder().engine(engine).build()
ctx.polyglotBindings.putMember("id", it)
val result = ctx.eval(script)
mapper.valueToTree<JsonNode>(result.`as`(Map::class.java))
}
}
println("GraalVM: $duration")
}
private fun jython() {
val duration = measureTime {
val script = PythonInterpreter().compile("{'name':'John', 'id': id}")
repeat(100) {
PythonInterpreter().use { interpreter ->
interpreter["id"] = it
val pyResult = interpreter.eval(script)
mapper.valueToTree<JsonNode>(pyResult)
}
}
}
println("Jython: $duration")
}
But the Jython implementation is ~4x faster for this example. I tried to compile and reuse the script with both implementations and just inject the necessary value.
I noticed I can reuse my val ctx = Context.newBuilder().engine(engine).build() by moving it outside the repeat block to make this example fast, but that is simply mutating the same context object in a loop and would retain old parameters in case they weren't reset after every execution and that seems fiddly.
Am I doing something wrong? Is there a better way to evaluate scripts from Java/Kotlin code?
After revisiting this issue, I added some simple logging to track the performance and it seems that most of the Jython time is spent on script parsing:
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.graalvm.polyglot.Context
import org.graalvm.polyglot.Engine
import org.graalvm.polyglot.Source
import org.python.util.PythonInterpreter
import kotlin.time.measureTime
import kotlin.time.measureTimedValue
fun main() {
graalvm()
jython()
}
val mapper = jacksonObjectMapper()
private fun graalvm() {
val duration = measureTime {
val engine = Engine.newBuilder()
.option("engine.WarnInterpreterOnly", "false")
.build()
val builder = Context.newBuilder("python").engine(engine)
val source = Source.create("python", "{'name':'John', 'id': id}")
repeat(1000) {
val executionTime = measureTime {
builder.build().use { ctx ->
ctx.polyglotBindings.putMember("id", it)
val result = ctx.eval(source)
mapper.valueToTree<JsonNode>(result.`as`(Map::class.java))
}
}
println(executionTime)
}
}
println("GraalVM: $duration")
}
private fun jython() {
val duration = measureTime {
val (script, duration) = measureTimedValue { PythonInterpreter().compile("{'name':'John', 'id': id}") }
println("Init: $duration")
repeat(1000) {
val executionTime = measureTime {
PythonInterpreter().use { interpreter ->
interpreter["id"] = it
val pyResult = interpreter.eval(script)
mapper.valueToTree<JsonNode>(pyResult)
}
}
println(executionTime)
}
}
println("Jython: $duration")
}
This simple example yields almost the same amount of time on Jython with 100 or 1000 records, but GraalVM implementation scaled very poorly. Average Jython execution time is around 10 microseconds after warmup, whereas GraalVM seems to be around 10 ms after warmup, so it is actually a 1000x difference after warmup?
I must be doing something wrong with the GraalVM implementation, but I cannot figure out what.