Kotlin/Native and JNI
Are you interested in Kotlin, the official language of Android and a fantastic replacement for Java? What about JNI, that thing that OpenJDK is now attempting to restrict for safety reasons in upcoming JDK versions?
Kotlin already has Multiplatform, why would you want to use JNI?
Thank you for the question, undisclosed ephemeral person I created to ask this question. My answer, as with the various other projects I have undergone (see my other blog posts), is...
"no reason"
Setup
I created a proof-of-concept project as a way to mess around with Kotlin and see what happens.
The project is a Kotlin/Multiplatform project with a JVM target (the Java side, with .java
files
that actually have the native methods) and a native target (for the real implementations).
Ten JNI functions are in the proof-of-concept:
- Creating an empty array
- Creating a copy of a Java array
- Checking the equality of two arrays
- Getting the length of an array
- Getting an element at a specific index
- Setting an element at a specific index
- Zeroing an array
- Filling an array with a value
- Sorting an array
- Deleting an array
The Java Side
For the PoC, I created a rather simple class: NativeBackedIntArray
. This essentially reimplements
int[]
but uses a native array to store the data. The class implements
AutoCloseable
to allow for automatic cleanup of the native memory when the object is done being used. The native
methods have parameters and return types meant for referencing memory addresses, and user-facing
methods are provided as normal object methods to hide the implementation details.
The Native Side
The native side is written with Kotlin/Native and implements the native methods defined in the Java side.
Thanks to the help of people with more low-level experience, many methods are implemented in ways that
manage to outpace the Java implementation in terms of performance. Through heavy use of the
@CName
annotation, the
actual function names can be sensical while still conforming to how JNI functions are meant to be named.
Speed compared to the Java implementation varies, with some methods (those involving direct memory access) being significantly faster than the Java implementation, while others (those involving individual element access) take a big hit in performance:
- Creating an empty array is near instant if enough memory is unused on the computer, as
calloc
provides zeroed memory (which is miles faster thanmalloc
with zeroing), but I do not understand why the JVM is a significant amount slower than said native implementation. - Copying a Java array is okay, but not amazing. A call to
memcpy
would be ideal, but until I figure out how to sneak my way around Java's implementation, instead (the equivalent of)env->GetIntArrayRegion
is used to copy, which is far slower than the JVM'sArrays.copyOf
method. - Setting elements in an array is horrendously slow. Because of the overhead of JNI and Kotlin's own checks, the
native implementation is far slower than
Arrays.fill
. - Zeroing an array is reasonably fast, thanks to the ability to use
memset
to zero the entire array in one go. It completes in about half the time ofArrays.fill
. - Array equality is faster than the JVM implementation because it can use
memcmp
for a very fast comparison. Like zeroing, it completes in about half the time as the Java equivalent ofArrays.equals
. - Array length, reading, and removal were not tested for speed. For removal, the JVM just lets the garbage collector
handle it, while the native implementation simply calls
free
(then causes a Java exception on future attempts to access). As the length is stored as the first thing in the memory block, reading the length is about as fast as can be for a memory access in Kotlin. Array accesses are likely also fast due to the contiguous memory layout, but JNI overhead may eat into that speed. - Filling the array with one value is done with a loop, but because of Kotlin/Native's own overhead, Java's
Arrays.fill
is faster than the native implementation. The native implementation is about 5x slower than the JVM implementation, but on ten million elements, it still completes within 25ms. - Sorting is very much a doozy, but through a four-bit LSD radix sort,
the native implementation can sort equivalent arrays in about the same time as the JVM implementation.
- Over the few test runs I've done, sorting one billion random elements tends to take around a minute and a half on the JVM, while the native radix sort can do so in about a minute (plus or minus a dozen seconds).
Results
Writing JNI methods in Kotlin/Native for no reason but to use Kotlin/Native is in general a bad idea, but using Kotlin/Native as the bridge between a Java API face and some other native library does have some merit. Just maybe don't use native code in all the places where a pure Java or Kotlin/JVM implementation works, and also maybe keep the overall overhead of Kotlin/Native and JNI in general in mind when writing JNI methods.
This project is available on GitHub, but as with most of my projects, it may be messy and definitely isn't documented. Good luck in any future Kotlin endeavors!