I totally forgot to make a braindump about another feature I stumbled upon lately: companion vals.
There's this really interesting feature proposal for the Kotlin language.
In essence, the proposed feature allows to write the following code:
and the following code:
From my point of view the feature has two aspects.
The first one is making members of companion members part of the surrounding instance. That means we can have properties and their members are automatically exposed, hence you don't need to access them with dot notation.
The second aspect is that other scopes are treated as well: If something is marked as companion, it is automatically available as a receiver in the corresponding scope. The proposal only talks about class properties, which are available in the class body automatically. This enables having Kotlin's scoped extension functions available without the need to use with(AdressPrinter) }{} everywhere.
I extended the Kotlin compiler with these two features and widened the application of the second aspect to all (?) possible scopes. This means if the companion val is top level, it's automatically available in the whole file. If a function parameter is marked as companion, the argument is going to be available as a receiver in the function body and so on. The implementation can be found here and examples can be found in the working tests.
Since the compiler has no simple and no stable API, I also implemented an annotation processor, that fulfils the first aspect. Repository can be found here. This would make the above code compile (with the right imports). Works by generating extension functions for all members, just as you would do in Kotlin by hand anyway if you would want to have this functionality.
Freitag, 16. August 2019
Sonntag, 24. Februar 2019
Multivolume voxel cone tracing
Another feature experiment I nearly forgot to show you is voxel cone tracing with multiple voxel volumes. There are different approaches to support large scenes with voxel cone tracing:
* Use a single volume texture and scale it to the scene's bounds. This increases a single voxel's world extents, which leads to coarser lighting and even more light leaking.
* Use sparse octree voxelization with a buffer instead of a volume texture. This is a way more complicated implementation. Additionally, the performance hit for lighting evaluation is quite big, compared to hardware-filtered volume texel fetching.
* Use cascaded voxel cone tracing, a similar approach to cascaded shadow maps. Revoxelization of the whole scene (or what's visible for the player) is quite demanding - implementations that only revoxelize objects on the border of the cascades are way more complex than the traditional, non-cascaded approach. Not using such an approach and revoxelizing everything every frame leads to flickering in the voxelization step, which can't be eliminated complettely due to the "binary" nature" of voxels (or at least I didn't manage to achieve it).
My implementation goes a different way, that doesn't suffer from the above problems, by introducing world space voxel volumes. Instead of a single big one, there are many smaller ones. There are many advantages now:
* Not all voxel volumes have to be updated every frame - one can update the nearest n volumes per frame, depending on the given hardware specs.
* There can be higher resolutions where needed and less resolution where coarse illumination is sufficient.
* Since everything is in world space, no flickering on revoxelization - at least when materials change. For dynamic objects, one still has to do some tricks or use temporal filtering with multiple bounces or sth.
* Theoretically, the voxel data could be precalculated and streamed in.
I put a sized list of VoxelGrid entries into a generic buffer that my avaluation compute shaders can read. My VoxelGrid data structure is as simple as the following.
As mentioned in an earlier post, my volumes use a kind of deferred rendering to cache geometry properties and onyl recaclulate lighting information when necessary, hence the need for 4 texture attachments - one for albedo, one for normals, and two for multiple bounces of gi.
The resolution (and the helper resolutionHalf) determine the resolution of the volume texture. This is needed, because the size of a volume can be arbitrary, while the resolution is fixed at some time, leading to arbitrary world space sizes for a single voxel.
Besides a little bit of padding, I also save the projection matrix that is used to voxelize objects into this volume. This isn't needed during evaluation, but I wanted to use a single data structure for both steps of the pipeline.
Since the texture ids don't give you much when using multiple volumes any more (you don't want to bind anything anymore...), those can be erased by now. What I use is bindless handles for everything, hence the long texture handles for the said four textures, passed in as uvec2 data types.
When implementing the tracing, I realized, that I want to favour higher resolution volumes when volumes overlap. Besides that, the tracing is quite simple: Take the gbuffer's world space position and trace diffuse or/and specular lighting in as many directions as you like. The sampling diameter increases with distance and determines the mipmap level to sample from.
The results are quite nice, with mixed resolutions and sizes of volumes. Here's an example of a transition between a fine and a coarse volume:
Unfortunately, the performance of my tracing is not that good. It's remarkably slower than the single volume tracing, and I'm not too certain why this is the case. My test scene contained 4 volumes and decreased performance below 30 fps on my GTX 1060, to it's not capable of being realtime anymore. And I'm talking about avaluation only - no voxelization done here.
This again leads me to the conclusion, that voxel cone tracing is just too heavy on resources to be practical. I got the idea of using binary voxelization and only use voxels for occlusion and soft shadows, but evaluate global illumination from precomputed volumes. Much cheaper, no synchronization between voxelization threads and in general just a lot cheaper.
* Use a single volume texture and scale it to the scene's bounds. This increases a single voxel's world extents, which leads to coarser lighting and even more light leaking.
* Use sparse octree voxelization with a buffer instead of a volume texture. This is a way more complicated implementation. Additionally, the performance hit for lighting evaluation is quite big, compared to hardware-filtered volume texel fetching.
* Use cascaded voxel cone tracing, a similar approach to cascaded shadow maps. Revoxelization of the whole scene (or what's visible for the player) is quite demanding - implementations that only revoxelize objects on the border of the cascades are way more complex than the traditional, non-cascaded approach. Not using such an approach and revoxelizing everything every frame leads to flickering in the voxelization step, which can't be eliminated complettely due to the "binary" nature" of voxels (or at least I didn't manage to achieve it).
My implementation goes a different way, that doesn't suffer from the above problems, by introducing world space voxel volumes. Instead of a single big one, there are many smaller ones. There are many advantages now:
* Not all voxel volumes have to be updated every frame - one can update the nearest n volumes per frame, depending on the given hardware specs.
* There can be higher resolutions where needed and less resolution where coarse illumination is sufficient.
* Since everything is in world space, no flickering on revoxelization - at least when materials change. For dynamic objects, one still has to do some tricks or use temporal filtering with multiple bounces or sth.
* Theoretically, the voxel data could be precalculated and streamed in.
I put a sized list of VoxelGrid entries into a generic buffer that my avaluation compute shaders can read. My VoxelGrid data structure is as simple as the following.
struct VoxelGrid { int albedoGrid; int normalGrid; int grid; int grid2; int resolution; int resolutionHalf; int dummy2; int dummy3; mat4 projectionMatrix; vec3 position; float scale; uvec2 albedoGridHandle; uvec2 normalGridHandle; uvec2 gridHandle; uvec2 grid2Handle; };
As mentioned in an earlier post, my volumes use a kind of deferred rendering to cache geometry properties and onyl recaclulate lighting information when necessary, hence the need for 4 texture attachments - one for albedo, one for normals, and two for multiple bounces of gi.
The resolution (and the helper resolutionHalf) determine the resolution of the volume texture. This is needed, because the size of a volume can be arbitrary, while the resolution is fixed at some time, leading to arbitrary world space sizes for a single voxel.
Besides a little bit of padding, I also save the projection matrix that is used to voxelize objects into this volume. This isn't needed during evaluation, but I wanted to use a single data structure for both steps of the pipeline.
Since the texture ids don't give you much when using multiple volumes any more (you don't want to bind anything anymore...), those can be erased by now. What I use is bindless handles for everything, hence the long texture handles for the said four textures, passed in as uvec2 data types.
Tracing
Now the interesting part. When only a few volumes are used, let's say 5-10 or something, the tracing can easily be implemented as brute force iteration over an array. I don't think more volumes are practical, as each volume needs a lot of memory, and there comes the point where classic sparse voxel octrees are simply more efficient.When implementing the tracing, I realized, that I want to favour higher resolution volumes when volumes overlap. Besides that, the tracing is quite simple: Take the gbuffer's world space position and trace diffuse or/and specular lighting in as many directions as you like. The sampling diameter increases with distance and determines the mipmap level to sample from.
vec4 accum = vec4(0.0); float alpha = 0; float dist = 0; vec3 samplePos = origin;// + dir; while (dist <= maxDist && alpha < 1.0) { float minScale = 100000.0; int canditateIndex = -1; VoxelGrid voxelGrid; for(int voxelGridIndex = 0; voxelGridIndex < voxelGridArray.size; voxelGridIndex++) { VoxelGrid candidate = voxelGridArray.voxelGrids[voxelGridIndex]; if(isInsideVoxelGrid(samplePos, candidate) && candidate.scale < minScale) { canditateIndex = voxelGridIndex; minScale = candidate.scale; voxelGrid = candidate; } } float minVoxelDiameter = 0.25f*voxelGrid.scale; float minVoxelDiameterInv = 1.0/minVoxelDiameter; vec4 ambientLightColor = vec4(0.); float diameter = max(minVoxelDiameter, 2 * coneRatio * (1+dist)); float increment = diameter; if(canditateIndex != -1) { sampler3D grid; // sample grid here } dist += increment; samplePos = origin + dir * dist; increment *= 1.25f; } return vec4(accum.rgb, alpha);
The results are quite nice, with mixed resolutions and sizes of volumes. Here's an example of a transition between a fine and a coarse volume:
| coarse and fine voxel volume side by side |
This again leads me to the conclusion, that voxel cone tracing is just too heavy on resources to be practical. I got the idea of using binary voxelization and only use voxels for occlusion and soft shadows, but evaluate global illumination from precomputed volumes. Much cheaper, no synchronization between voxelization threads and in general just a lot cheaper.
Samstag, 23. Februar 2019
Multibounce voxel cone tracing
It has been quite a while since I implemented a derivative of the classic voxel cone tracing with volume textures in my graphics engine that kind of applies deferred rendering with voxels in order to achieve multi bounce global illumination. The idea is to voxelize the whole scene to a voxel texture and save all parameters that are needed for lighting. Similar to the regular gbuffer in deferred rendering, positions (implicitly given by world space voxels..) normals and albedo can be sufficient. Additionally, my renderer writes a flag if the object is dynamic or static, in order to be able to cache voxel data for static objects, which massively speeds up voxelization proccess and just brings the whole thing closer to realtime capable.
Decoupling lighting from voxelization also frees enough frame time to implement multiple light bounces. Therefore, for n bounces, I added n light accumulation voxel texture targets. During voxel lighting, these are traced against. Although a second bounce can significantly enhance the scene's overall lighting. I struggled getting this to work with ping-ponging textures. I also struggled with parameters like samples on the hemisphere or tracing distance, cone aperture, etc. because in the voxel world, my default parameters for gbuffer tracing didn't lead to great results. Nonetheless, I wanted to share my results with you strangers, although I tend to discard this feature, because it doesn't make voxel cone tracing's light leaking problem less appearent....
Decoupling lighting from voxelization also frees enough frame time to implement multiple light bounces. Therefore, for n bounces, I added n light accumulation voxel texture targets. During voxel lighting, these are traced against. Although a second bounce can significantly enhance the scene's overall lighting. I struggled getting this to work with ping-ponging textures. I also struggled with parameters like samples on the hemisphere or tracing distance, cone aperture, etc. because in the voxel world, my default parameters for gbuffer tracing didn't lead to great results. Nonetheless, I wanted to share my results with you strangers, although I tend to discard this feature, because it doesn't make voxel cone tracing's light leaking problem less appearent....
![]() |
| two bounces (first), one bounce (second) |
Montag, 17. September 2018
Kind-of-structs on the JVM using Kotlin's delegated properties
Project Valhalla is on everyone's lips nowadays, but the problem is: It is for years now and there's no concrete schedule when we can expect value types to be part of the JVM.
In games, there is a desperate need for value types or at least control about the object layout. Why? Because one has to share memory with the native side. For example OpenGL lets you use a persistent mapped buffer - combined with multibuffering and your own synchronization gives you a blazing fast multithreading approach for your engine. But OpenGL doesn't want to read your Java object's headers, that's why you can't use regular serialzation mechanisms and instead you have to put your objects into a ByteBuffer float by float or int by int.
Using standard Java/JVM heap objects, one has to update all the objects and afterwards extract them to a ByteBuffer. This means two iterations. Better would be to have objects that use a ByteBuffer directly, in order to be able to skip the buffer extraction completely.
Now there's Kotlin with its delegated properties. All the basic examples show how to use a hash map instance as a backing storage for arbitrary properties of an object (https://kotlinlang.org/docs/reference/delegated-properties.html). This led me to the idea to use delegated properties to access a ByteBuffer object as a backing storage for objects and structures of objects - just like structs in C do it.
Benchmark
Mode Cnt Score Error Units
iterAndMutBufferDirect
thrpt 12 90626,796 ± 303,407 ops/s
iterAndMutKotlinDelegatedPropertySlidingWindowBuffer
thrpt 12 23695,594 ± 82,291 ops/s
iterAndMutKotlinDelegatedPropertyUnsafeSimpleSlidingWindowBuffer
thrpt 12 27906,315 ± 52,382 ops/s
iterAndMutKotlinDelegatedPropertyUnsafeSlidingWindowBuffer
thrpt 12 25736,322 ± 904,017 ops/s
iterAndMutKotlinSimpleSlidingWindowBuffer
thrpt 12 27416,212 ± 959,016 ops/s
iterAndMutResizableStruct
thrpt 12 10204,870 ± 189,237 ops/s
iterAndMutSimpleSlidingWindowBuffer
thrpt 12 27627,217 ± 122,119 ops/s
iterAndMutStructArray
thrpt 12 12714,642 ± 51,275 ops/s
iterAndMutStructArrayIndexed
thrpt 12 11110,882 ± 26,910 ops/s
iterAndMutVanilla
thrpt 12 27111,335 ± 661,822 ops/s
iterStruct
thrpt 12 13240,723 ± 40,612 ops/s
iterVanilla
thrpt 12 21452,188 ± 46,380 ops/s
All benchmarks iterate over a collection of 5000 Vector3f instances. iterAndMutVanilla is just a regular ArrayList iteration with forEach, setting the three components of each vector. iterAndMutStruct is my current implementation of a tight StructArray of Vector3fs with a sliding window iteration.
Vanilla Java iteration with mutation yields the baseline results with 27k operations. It's very intersting, that a non-abstracted simple version with a direct bytebuffer is three times as fast as the baseline, reaching 90k operations. Simple non-abstracted implementations with Kotlin's delegates brings us down to the baseline performance again. My struct abstraction in the current implementation with a struct array class implementation can only reach 50% of the baseline - quite a difference between the simple delegate approach and only a rough sixth of the simple direct bytebuffer approache's performance.
I have to figure out why my abstractions degrade performance by such amounts - the generated bytecode looks pretty similar for all the versions. At the time of writing, Kotlin's inline classes are not stable enough for delegate usage, so delegates cause some class overhead here.
But even though there are some performance differences in this very micro benchmark, it doesn't necessarily mean that other use cases show such dramatic differences as well. Additionally, the largest benefit my struct-alike implementation offers is, that now large and complex datastructures can be memcopied like this:
This means no iteration over nested arrays, complex copy constructors and even more complex nested invocation of them. Super handy for renderstate constructs in game engines - your whole renderstate instance can be mapped to a OpenGL struct and mapped as a shader storage buffer :)
In games, there is a desperate need for value types or at least control about the object layout. Why? Because one has to share memory with the native side. For example OpenGL lets you use a persistent mapped buffer - combined with multibuffering and your own synchronization gives you a blazing fast multithreading approach for your engine. But OpenGL doesn't want to read your Java object's headers, that's why you can't use regular serialzation mechanisms and instead you have to put your objects into a ByteBuffer float by float or int by int.
Using standard Java/JVM heap objects, one has to update all the objects and afterwards extract them to a ByteBuffer. This means two iterations. Better would be to have objects that use a ByteBuffer directly, in order to be able to skip the buffer extraction completely.
Now there's Kotlin with its delegated properties. All the basic examples show how to use a hash map instance as a backing storage for arbitrary properties of an object (https://kotlinlang.org/docs/reference/delegated-properties.html). This led me to the idea to use delegated properties to access a ByteBuffer object as a backing storage for objects and structures of objects - just like structs in C do it.
interface Struct {
byteOffset: Int
buffer: ByteBuffer
}
// some missing magic here for property registration and local offset calculation
class FloatProperty(val localOffset) {
inline operator fun getValue(thisRef: Struct, KProperty<*,*>): Float {
thisRef.buffer.getFloat(thisRef.byteOffset+localOffset)
}
}
class MyStruct: Struct {
// .. missing magic here
val myFloat by FloatProperty()
val mySecondFloat by FloatProperty()
}
This will result a flat Layout for a MyStruct instance. This can be used directly by native APIs. So what I'm interested in is, how well does this perform compared to Vanilla Java approach? How much overhead is there for all those methods and delegate instances?
I tested several different implementations that differ in convenience for the user and overall performance. One surprise for me was, that none of my implementations (neither ByteBuffer nor Unsafe backed) is as fast as vanilla Java. There are some benchmarks on the internet that tell a different story, for example this one. I can't really tell you how it was achieved. Just that I wasn't able to achieve similar results.
Benchmark
Mode Cnt Score Error Units
iterAndMutBufferDirect
thrpt 12 90626,796 ± 303,407 ops/s
iterAndMutKotlinDelegatedPropertySlidingWindowBuffer
thrpt 12 23695,594 ± 82,291 ops/s
iterAndMutKotlinDelegatedPropertyUnsafeSimpleSlidingWindowBuffer
thrpt 12 27906,315 ± 52,382 ops/s
iterAndMutKotlinDelegatedPropertyUnsafeSlidingWindowBuffer
thrpt 12 25736,322 ± 904,017 ops/s
iterAndMutKotlinSimpleSlidingWindowBuffer
thrpt 12 27416,212 ± 959,016 ops/s
iterAndMutResizableStruct
thrpt 12 10204,870 ± 189,237 ops/s
iterAndMutSimpleSlidingWindowBuffer
thrpt 12 27627,217 ± 122,119 ops/s
iterAndMutStructArray
thrpt 12 12714,642 ± 51,275 ops/s
iterAndMutStructArrayIndexed
thrpt 12 11110,882 ± 26,910 ops/s
iterAndMutVanilla
thrpt 12 27111,335 ± 661,822 ops/s
iterStruct
thrpt 12 13240,723 ± 40,612 ops/s
iterVanilla
thrpt 12 21452,188 ± 46,380 ops/s
All benchmarks iterate over a collection of 5000 Vector3f instances. iterAndMutVanilla is just a regular ArrayList iteration with forEach, setting the three components of each vector. iterAndMutStruct is my current implementation of a tight StructArray of Vector3fs with a sliding window iteration.
Vanilla Java iteration with mutation yields the baseline results with 27k operations. It's very intersting, that a non-abstracted simple version with a direct bytebuffer is three times as fast as the baseline, reaching 90k operations. Simple non-abstracted implementations with Kotlin's delegates brings us down to the baseline performance again. My struct abstraction in the current implementation with a struct array class implementation can only reach 50% of the baseline - quite a difference between the simple delegate approach and only a rough sixth of the simple direct bytebuffer approache's performance.
I have to figure out why my abstractions degrade performance by such amounts - the generated bytecode looks pretty similar for all the versions. At the time of writing, Kotlin's inline classes are not stable enough for delegate usage, so delegates cause some class overhead here.
But even though there are some performance differences in this very micro benchmark, it doesn't necessarily mean that other use cases show such dramatic differences as well. Additionally, the largest benefit my struct-alike implementation offers is, that now large and complex datastructures can be memcopied like this:
class MyStruct: Struct {
// .. missing magic here
val myFloat by FloatProperty()
val mySecondFloat by FloatProperty()
}
val source = MyStruct().apply {
myFloat = 5
}
val target = MyStruct()
source.copyTo(target) // Simple extension method that copies a bytebuffer
println(target.myFloat) // prints 5
This means no iteration over nested arrays, complex copy constructors and even more complex nested invocation of them. Super handy for renderstate constructs in game engines - your whole renderstate instance can be mapped to a OpenGL struct and mapped as a shader storage buffer :)
Dienstag, 27. März 2018
Programmatically compile Kotlin code
Here's a small braindump again, because I don't want to forget how I managed to compile Kotlin files programmatically. There are some very small discussions online, for example this one, but I find existing usages too complex and there are too many things that didn't work for me at the first few tries.
So there's a very convenient artifact that seem to contain everything one needs in order to call a Kotlin compiler from within code and that's published for every Kotlin release. Use it with
The following snippet instantiates a compiler, passes a file and some other properties to it and executes compilation. The resulting class file can be found in the given output directory.
The usage depends on the project where it is used, for example it coult be necessary to add a different classpath or add the classpath and a stdlib or something else.
EDIT: I'm sure I found this snippet, or a similar one in the internet, but I can't find it anymore. If you found it, let me know and I will link it as source.
So there's a very convenient artifact that seem to contain everything one needs in order to call a Kotlin compiler from within code and that's published for every Kotlin release. Use it with
compile group: 'org.jetbrains.kotlin', name: 'kotlin-compiler-embeddable', version: "$kotlin_version"
The following snippet instantiates a compiler, passes a file and some other properties to it and executes compilation. The resulting class file can be found in the given output directory.
val output = File("/home/myuser/out") K2JVMCompiler().run { val args = K2JVMCompilerArguments().apply { freeArgs = listOf(File("/home/myuser/KotlinFile.kt").absolutePath) loadBuiltInsFromDependencies = true destination = output.absolutePath classpath = System.getProperty("java.class.path") .split(System.getProperty("path.separator")) .filter { File(it).exists() && File(it).canRead() }.joinToString(":") noStdlib = true noReflect = true skipRuntimeVersionCheck = true reportPerf = true } // output.deleteOnExit() execImpl( PrintingMessageCollector( System.out, MessageRenderer.WITHOUT_PATHS, true), Services.EMPTY, args) }
The usage depends on the project where it is used, for example it coult be necessary to add a different classpath or add the classpath and a stdlib or something else.
EDIT: I'm sure I found this snippet, or a similar one in the internet, but I can't find it anymore. If you found it, let me know and I will link it as source.
Dienstag, 13. Februar 2018
Koin vs Vanilla Kotlin
Here we go again: I'm searching for a performant, convenient solution for dependency injection in Kotlin. Koin got my attention, because it seems to be simple and straightforward. Since it uses a lot of small inline functions and seems to have only a few hotspots where performance suckers could lurk, it seemed very promising. But I wouldn't want to use it in my game engine project until I can be sure that the performance impact would be negligible. So I did a ... probably totally flawed microbenchmark, that doesn't show anything, but I want to dump it here nonetheless.
So we have a simple service class and another service class that depends on the first service. The main module depends on the seconds service, so the chain has to be fulfilled.
Using Koin, one can simply write a few lines and everything is wired together automatically. Note that the Koin context has to be started and stopped, which has to be excluded from the benchmark later.
The benchmark is executed as follows, to ensure all object creation happens and context creation is done outside of the benchmarked code:
The result is somehow sobering
Even though I'm aware that this is an artificial benchmark that may be flawed, it's pretty much clear that using Koin will have a huge impact on performance, that could make program infrastrucutre slower by a factor of 100. Of course, we're talking about dependency injection at object creation time, which should be a rare case in a game engine. Nonetheless, not too good from my sight.
So we have a simple service class and another service class that depends on the first service. The main module depends on the seconds service, so the chain has to be fulfilled.
class MainModuleKoin : Module() { override fun context(): Context = applicationContext { provide { ServiceA() } provide { ServiceB(get()) } } } class MainModuleVanilla(val serviceA: ServiceA, val serviceB: ServiceB) class MainComponentKoin : KoinComponent { val bla by inject<ServiceB>() } class MainComponentVanilla(val bla: ServiceB) class ServiceA class ServiceB(val serviceA: ServiceA) { }
Using Koin, one can simply write a few lines and everything is wired together automatically. Note that the Koin context has to be started and stopped, which has to be excluded from the benchmark later.
@JvmStatic fun benchmarkKoin(): ServiceB { return MainComponentKoin().bla } @JvmStatic fun stopKoin() { closeKoin() } @JvmStatic fun startKoin() { startKoin(listOf(MainModuleKoin())) } @JvmStatic fun benchmarkVanilla(): ServiceB { return MainComponentVanilla(ServiceB(ServiceA())).bla }
The benchmark is executed as follows, to ensure all object creation happens and context creation is done outside of the benchmarked code:
@State(Scope.Thread) public static class MyState { @Setup(Level.Trial) public void doSetup() { KoinBenchmarkRunner.startKoin(); } @TearDown(Level.Trial) public void doTearDown() { KoinBenchmarkRunner.stopKoin(); } } @Benchmark public void benchmarkKoin(Blackhole hole, MyState state) { hole.consume(KoinBenchmarkRunner.benchmarkKoin()); hole.consume(state); } @Benchmark public void benchmarkVanilla(Blackhole hole, MyState state) { hole.consume(KoinBenchmarkRunner.benchmarkVanilla()); hole.consume(state); }
The result is somehow sobering
Benchmark Mode Cnt Score Error Units BenchmarkRunner.benchmarkKoin thrpt 200 1425585.082 ± 31179.345 ops/s BenchmarkRunner.benchmarkVanilla thrpt 200 106484919.110 ± 1121927.712 ops/s
Even though I'm aware that this is an artificial benchmark that may be flawed, it's pretty much clear that using Koin will have a huge impact on performance, that could make program infrastrucutre slower by a factor of 100. Of course, we're talking about dependency injection at object creation time, which should be a rare case in a game engine. Nonetheless, not too good from my sight.
Mittwoch, 7. Februar 2018
Kotlin's scoped extensions micro-benchmarked
My last post was about an approach to use Kotlin's scoped extension methods to implement an application with data oriented design paradigm. Yes, I'm still coding that game engine, that's why I had to do a simple benchmark, just to get a feeling how performance could get better or worse. See it as a brain dump. Very unprofessional benchmark with the println statement, but I wanted to get the relation between the simple baseline implementation and the extension method version, like this:
Surprising results again:
Once again, the opposite of my expectations.
import org.openjdk.jmh.annotations.Benchmark interface Extension<ELEMENT>{ fun ELEMENT.prettyPrint() { println("Default pretty " + this) } } object StringListExtension : Extension<String> fun <T> someFrameWorkFunction(parameter : T, extensionProvider: Extension<T>) { with(extensionProvider) { parameter.prettyPrint() } } @Benchmark fun extension() { someFrameWorkFunction("asd", StringListExtension) } fun String.prettyPrint() { println("Default pretty " + this) } @Benchmark fun baseline() { "asd".prettyPrint() }
Surprising results again:
Benchmark Mode Cnt Score Error Units BenchmarkRunner.benchmarkBaseline thrpt 200 269087.160 ± 17915.393 ops/s BenchmarkRunner.benchmarkExtension thrpt 200 329648.131 ± 19646.005 ops/s
Once again, the opposite of my expectations.
Abonnieren
Posts (Atom)
