How to speed up your Android app native calls up to 24x
Discover how two peculiar annotations can drastically accelerate native calls and maximize your app’s performance.
In Android 8.0, Google added a new ART feature: faster native calls with the @FastNative
and @CriticalNative
annotations. According to Google, “these built-in ART runtime optimizations speed up Java Native Interface (JNI) transitions and replace the now deprecated !bang JNI notation.” Google provides a chart with example execution time differences between the different native invocation types, showing a ~4.6x improvement in execution time—but how accurate is it?
On a Google Pixel 2 on Android 11, the results from 10,000,000 innovations each of empty C functions are shown below. Note that the !bang JNI notation no longer shows any performance benefit, so it is excluded from testing on Android 8+. The source code for this example project that these results were generated from can be found here.
JNI invocation type | Execution time (in ns) |
---|---|
Regular JNI | 774 |
@FastNative | 278 |
@CriticalNative | 32 |
Here are also the results from running on an arm64 Android 13 emulator on my Apple MacBook Air (Late 2020):
JNI invocation type | Execution time (in ns) |
---|---|
Regular JNI | 100 |
@FastNative | 40 |
@CriticalNative | 23 |
Although these results are not too scientific (i.e., I did not use a proper benchmarking tool such as JMH), they show an impressive result: ~24x and ~4x improvement in execution time respectively! I did utilize a “warmup” run, and the results were consistent between runs.
So, how do I speed up my Android app?
Unfortunately, Google does not provide access to the @FastNative
and @CriticalNative
annotations in the Android 13 SDK and below; in the public Android SDK, these classes (in the dalvik.annotation.optimization
package) are not included at all. Luckily, there is a very easy solution—simply adding these classes to your project is all that is needed to trick ART into optimizing your app’s native calls! The example project that I used to create the above-mentioned benchmark does exactly this.
However, there are several considerations to be made before using these annotations. While the @FastNative
annotation does not require altering your existing JNI interface code, the @CriticalNative
annotation does; using the @CriticalNative
annotation requires you to remove the JNIEnv
and jclass
parameters in your native code, thus giving up access to Java object functionality. Additionally, Google’s documentation recommends no long-running or unbounded functions, as well as not acquiring locks that aren’t also released in that same function (excluding some logging and native allocation calls); these actions risk creating deadlocks or freezing the garbage collector for too long. Despite this, many typical C/C++ functions are eligible for these annotations.
Lastly, in the JavaDoc for the @CriticalNative
annotation, Google warns that these annotations are not CTS-tested prior to Android 14, and that one should only use the RegisterNatives
API to use these prior to Android 12 (like in my example project). Although lack of CTS-testing could prove to be a potentially fatal compatibility issue with functions making use of the @CriticalNative
annotation, I question if there is any real-world precedent of a standard OEM modifying or removing support for these annotations in such a compatibility-breaking way.
Regarding Android 14, I just became aware that these annotations seem to be promoted to the public API, with the @SystemApi
annotations and @hide
JavaDoc annotations being removed! So, in cases where you have a critical production app and don’t want to risk running into compatibility issues, you can still take advantage of these two amazing annotations in the future.
If you take a look at the AOSP source, you may notice there are other annotations in the same package… those don’t look too interesting for typical usage, but I may explore them in a future blog.