Building a Fast Math app for Performance Gains with Android NDK

In this recipe, we will compare performance between Kotlin and C++ by implementing a Fibonacci calculator in both languages. This will introduce you to NDK performance optimizations and teach you how to pass values between Kotlin and C++.

Pre-requisites

This recipe builds upon the last one’s code. This saves time, and skips all the setup work needed for a new project. If you haven’t completed the previous recipe in this series on Android NDK, I recommend going through Building a Hello World App using Android NDK and C++ first, and then coming here once you’re through.

πŸ›  What You’ll Learn

βœ… How to pass primitive data types (int) between Kotlin and C++

βœ… How to perform fast mathematical calculations in native code

βœ… How to measure performance differences between Kotlin and C++

πŸ“Œ Step 1: Modify Native Code to Implement Fibonacci in C++

1. Open native-lib.cpp (inside app/src/main/cpp/).

2. Modify it to include an iterative Fibonacci function:

#include <jni.h>

extern "C" JNIEXPORT jint JNICALL
Java_com_example_ndkhelloworld_MainActivity_fibonacciNative(
        JNIEnv *env,
        jobject,
        jint n
) {
    if (n <= 1) return n;
    int a = 0, b = 1, temp;
    for (int i = 2; i <= n; i++) {
        temp = a + b;
        a = b;
        b = temp;
    }
    return b;
}

πŸš€ What This Code Does

β€’ fibonacciNative() β†’ Uses iteration (faster and better for large n) to figure out the Fibonacci value for a given limit.

πŸ“Œ Step 2: Update CMakeLists.txt

Make sure your CMakeLists.txt includes:

add_library(native-lib SHARED native-lib.cpp)

No changes needed if you’ve set it up from Recipe 1.

πŸ“Œ Step 3: Expose Native Methods in Kotlin

1. Open MainActivity.kt.

2. Add one new native function declaration:

external fun fibonacciNative(n: Int): Int

πŸ“Œ Step 4: Implement Fibonacci in Kotlin

Now, let’s implement the same Fibonacci logic in Kotlin for comparison.

1. Add the following Kotlin iterative Fibonacci function to the bottom of MainActivity.kt.

private fun fibonacciKotlin(n: Int): Int {
        if (n <= 1) return n
        var a = 0
        var b = 1
        for (i in 2..n) {
            val temp = a + b
            a = b
            b = temp
        }
        return b
    }

πŸ“Œ Step 5: Benchmark Performance

1. Modify onCreate() in MainActivity.kt to measure execution time.
Start by adding a benchmarking function to measure average time for a number of iterations.

// Benchmarking utility to measure average execution time
private fun benchmark(iterations: Int, function: () -> Int): Long {
    var totalTime = 0L
    repeat(iterations) {
        val start = System.nanoTime()
        function()
        totalTime += (System.nanoTime() - start)
    }
    return totalTime / iterations  // Return the average time
}

2. Now update the onCreate function to run our tests on both Kotlin and Native C++ executions.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val n = 40  // Change this value to test different scenarios
        val iterations = 1000  // Running multiple times for better benchmarking
        val resultText = findViewById<TextView>(R.id.textView1)

        // Measure Kotlin Iterative Fibonacci
        val kotlinIterTime = benchmark(iterations) { fibonacciKotlin(n) }

        // Measure Native Iterative Fibonacci
        val nativeIterTime = benchmark(iterations) { fibonacciNative(n) }

        resultText.text = """
        Kotlin Iterative: Processed in ${kotlinIterTime} ns
        
        Native Iterative: Processed in ${nativeIterTime} ns
    """.trimIndent()
}

πŸ“Œ Step 6: Run the App & Compare

1. Run the app on a real device for accurate timing.

2. Observe the execution times for different Fibonacci implementations.

3. Increase n (e.g., 50-100) and notice the difference!

πŸ“Œ Expected Output

On a mid-range phone, you might see something like:

Kotlin Iterative: Processed in 134346 ns
        
Native Iterative: Processed in 34146 ns

πŸ“Œ There’s A Caveat

I will admit, I cheated a bit.

When calling the native function just once, Kotlin often appeared faster. However, when I ran it multiple times (100+ iterations), native code consistently outperformed Kotlin. This happens because the first JNI call has high overhead, including library loading, method lookup, and Java-Native context switching, making native execution seem slow. Meanwhile, ART’s Just-In-Time (JIT) compilation optimizes Kotlin functions over time, improving their performance. But when I repeated the calls, the JNI overhead became negligible, and native code’s direct execution and CPU caching advantages became apparent. This explains why single-call benchmarks can be misleading – NDK only shines when running intensive computations repeatedly. ⚑️

πŸš€ Key Takeaways

βœ… Kotlin code is much faster than native C++ if the number of calls to a function are few

βœ… Native C++ is significantly faster than Kotlin if the number of calls to a function are many or if the computation is complex

πŸ›  Bonus Challenge

βœ… Challenge 1: Write a new native C++ function that calculates factorial and calls it from Kotlin to measure performance with factorial compute.

πŸš€ What’s Next?

Now that we’ve understood NDK performance benefits, we’re ready for a juicy, real-world use case. Let’s dive into a scenario where computation is intense and repetitive: image and audio processing! This is where NDK is often used in real-world Android apps, so let’s dive into a sample app that processes images in real-time from a camera feed. This is a scenario where both speed and performance is critical to success. Let’s go!

Next Lesson: “Native Image Processing” (OpenCV + NDK)

We’ll be using C++ in the NDK to apply real-time image filters using OpenCV!

Closing Remarks

The source code for this recipe can be found at https://github.com/advaitsaravade/Android-NDK-2-Fast-Math. The source contains the answers to the Bonus Challenges above as well!

Happy coding!

πŸ’Œ Get updates about my life

I like to write & vlog about my time on this planet, usually 1-2 times a month. Enter your email here to get a summary of everything I did once a month.