Kotlin - Performance

Overview

Performance optimization in Kotlin involves understanding the language's compilation model, memory usage patterns, and runtime characteristics. This tutorial covers performance best practices, optimization techniques, and tools for building efficient Kotlin applications.

๐ŸŽฏ Learning Objectives:
  • Understand Kotlin performance characteristics
  • Learn inline functions and when to use them
  • Master memory allocation optimization techniques
  • Apply performance profiling and measurement
  • Optimize collections and data structures usage

Understanding Kotlin Performance

JVM Compilation and Runtime

// Kotlin compiles to JVM bytecode, similar performance to Java
class PerformanceBasics {
    // โœ… Value classes reduce object allocation
    @JvmInline
    value class UserId(val value: String)
    
    // โœ… Data classes are efficient for simple data containers
    data class User(val id: UserId, val name: String, val age: Int)
    
    // โœ… Object declarations are singletons (efficient)
    object UserRepository {
        private val users = mutableMapOf()
        
        fun addUser(user: User) {
            users[user.id] = user
        }
        
        fun getUser(id: UserId): User? = users[id]
    }
}

Memory Usage Patterns

class MemoryOptimization {
    // โœ… Primitive types are more efficient than wrapper types
    fun processNumbers(numbers: IntArray): Long {
        var sum = 0L // Long primitive, not Long wrapper
        for (i in numbers.indices) {
            sum += numbers[i] // No boxing/unboxing
        }
        return sum
    }
    
    // โŒ Inefficient: Creates wrapper objects
    fun inefficientSum(numbers: List): Long {
        return numbers.fold(0L) { acc, n -> acc + n } // Boxing occurs
    }
    
    // โœ… More efficient: Use specialized collections
    fun efficientSum(numbers: IntArray): Long {
        return numbers.fold(0L) { acc, n -> acc + n } // No boxing
    }
    
    // โœ… Use primitive collections when possible
    val primitiveMap = HashMap() // Better than Map
}

Inline Functions

When and How to Use Inline

// โœ… Inline functions eliminate lambda object creation
inline fun  measureTime(operation: () -> T): Pair {
    val start = System.nanoTime()
    val result = operation()
    val end = System.nanoTime()
    return result to (end - start)
}

// โœ… Higher-order functions benefit from inlining
inline fun  List.forEachFast(action: (T) -> Unit) {
    for (element in this) {
        action(element)
    }
}

// โœ… Reified type parameters with inline functions
inline fun  Any?.safeCast(): T? = this as? T

fun example() {
    val any: Any = "Hello"
    val string: String? = any.safeCast() // No Class parameter needed
}

// โŒ Don't inline large functions
// inline fun largeFunction() { /* 100+ lines of code */ }

// โœ… Use noinline for some parameters when needed
inline fun processData(
    data: List,
    noinline logger: (String) -> Unit, // Won't be inlined
    transform: (String) -> String // Will be inlined
) {
    data.forEach { item ->
        logger("Processing: $item")
        transform(item)
    }
}

Inline Function Performance Benefits

class InlinePerformanceDemo {
    // Regular higher-order function
    fun regularMap(list: List, transform: (Int) -> Int): List {
        return list.map(transform) // Creates lambda object
    }
    
    // Inline higher-order function
    inline fun inlineMap(list: List, transform: (Int) -> Int): List {
        return list.map(transform) // No lambda object created
    }
    
    fun performanceTest() {
        val numbers = (1..1_000_000).toList()
        
        // This creates a lambda object for each call
        val result1 = regularMap(numbers) { it * 2 }
        
        // This inlines the lambda, no object creation
        val result2 = inlineMap(numbers) { it * 2 }
    }
    
    // โœ… Crossinline prevents non-local returns
    inline fun processItems(
        items: List,
        crossinline processor: (T) -> Unit
    ) {
        items.forEach { item ->
            // processor can't contain return statements
            processor(item)
        }
    }
}

Collection Performance

Choosing the Right Collection

class CollectionPerformance {
    // โœ… Use appropriate collection types
    fun demonstrateCollectionChoice() {
        // ArrayList for random access and frequent appends
        val arrayList = ArrayList()
        
        // LinkedList for frequent insertions/deletions in middle
        val linkedList = LinkedList()
        
        // HashSet for fast lookups, no duplicates
        val hashSet = HashSet()
        
        // LinkedHashSet for ordered unique elements
        val linkedHashSet = LinkedHashSet()
        
        // HashMap for key-value lookups
        val hashMap = HashMap()
        
        // TreeMap for sorted key-value pairs
        val treeMap = TreeMap()
    }
    
    // โœ… Use sequences for large data processing
    fun processLargeDataset(data: List): List {
        return data.asSequence()
            .filter { it.isNotBlank() }
            .map { it.trim().uppercase() }
            .filter { it.length > 3 }
            .sorted()
            .take(1000)
            .toList()
    }
    
    // โŒ Inefficient: Multiple intermediate collections
    fun inefficientProcessing(data: List): List {
        return data
            .filter { it.isNotBlank() } // Creates new list
            .map { it.trim().uppercase() } // Creates new list
            .filter { it.length > 3 } // Creates new list
            .sorted() // Creates new list
            .take(1000) // Creates new list
    }
}

Array vs List Performance

import kotlin.system.measureTimeMillis

class ArrayVsList {
    fun performanceComparison() {
        val size = 10_000_000
        
        // Array creation and access
        val arrayTime = measureTimeMillis {
            val array = IntArray(size) { it }
            var sum = 0L
            for (i in array.indices) {
                sum += array[i]
            }
        }
        
        // List creation and access
        val listTime = measureTimeMillis {
            val list = List(size) { it }
            var sum = 0L
            for (i in list.indices) {
                sum += list[i]
            }
        }
        
        println("Array time: ${arrayTime}ms")
        println("List time: ${listTime}ms")
    }
    
    // โœ… Use specialized arrays for primitives
    fun useSpecializedArrays() {
        val intArray = IntArray(1000) // No boxing
        val doubleArray = DoubleArray(1000) // No boxing
        val booleanArray = BooleanArray(1000) // No boxing
        
        // โŒ Less efficient
        val intList = List(1000) { 0 } // Boxing occurs
    }
    
    // โœ… Pre-allocate collections when size is known
    fun preallocation() {
        val knownSize = 10000
        
        // Pre-allocate capacity
        val list = ArrayList(knownSize)
        val map = HashMap(knownSize)
        val set = HashSet(knownSize)
        
        // This avoids multiple resize operations
    }
}

String Performance

String Operations Optimization

class StringPerformance {
    // โœ… Use StringBuilder for multiple concatenations
    fun efficientStringBuilding(items: List): String {
        return buildString {
            items.forEach { item ->
                append(item)
                append(", ")
            }
        }
    }
    
    // โŒ Inefficient string concatenation
    fun inefficientStringBuilding(items: List): String {
        var result = ""
        items.forEach { item ->
            result += item + ", " // Creates new string each time
        }
        return result
    }
    
    // โœ… Use joinToString for collections
    fun joinStrings(items: List): String {
        return items.joinToString(separator = ", ")
    }
    
    // โœ… String templates are optimized
    fun useStringTemplates(name: String, age: Int): String {
        return "Name: $name, Age: $age" // Compiled efficiently
    }
    
    // โœ… Use string comparison efficiently
    fun stringComparisons() {
        val str1 = "hello"
        val str2 = "world"
        
        // Fast comparison
        val equal = str1 == str2
        
        // Case-insensitive comparison
        val equalIgnoreCase = str1.equals(str2, ignoreCase = true)
        
        // Use when for multiple comparisons
        when (str1) {
            "hello" -> println("Found hello")
            "world" -> println("Found world")
            else -> println("Other")
        }
    }
}

Memory Management

Object Creation and Garbage Collection

class MemoryManagement {
    // โœ… Object pooling for frequently created objects
    class ObjectPool(
        private val factory: () -> T,
        private val reset: (T) -> Unit,
        maxSize: Int = 10
    ) {
        private val pool = ArrayDeque(maxSize)
        
        fun acquire(): T {
            return if (pool.isNotEmpty()) {
                pool.removeFirst()
            } else {
                factory()
            }
        }
        
        fun release(obj: T) {
            reset(obj)
            if (pool.size < 10) {
                pool.addLast(obj)
            }
        }
    }
    
    // Example usage
    private val stringBuilderPool = ObjectPool(
        factory = { StringBuilder() },
        reset = { it.clear() }
    )
    
    fun buildString(): String {
        val sb = stringBuilderPool.acquire()
        try {
            sb.append("Hello ")
            sb.append("World")
            return sb.toString()
        } finally {
            stringBuilderPool.release(sb)
        }
    }
    
    // โœ… Use lazy initialization
    class ExpensiveResource {
        val expensiveData: Map by lazy {
            // Only computed when first accessed
            loadExpensiveData()
        }
        
        private fun loadExpensiveData(): Map {
            // Simulate expensive operation
            return (1..10000).associate { "key$it" to "value$it" }
        }
    }
    
    // โœ… Avoid creating unnecessary objects in loops
    fun processItems(items: List) {
        val regex = Regex("\\d+") // Create once, outside loop
        
        items.forEach { item ->
            if (regex.matches(item)) {
                // Process numeric item
            }
        }
    }
    
    // โŒ Inefficient: Creates new regex each iteration
    fun inefficientProcessing(items: List) {
        items.forEach { item ->
            val regex = Regex("\\d+") // Created repeatedly
            if (regex.matches(item)) {
                // Process numeric item
            }
        }
    }
}

Value Classes and Inline Classes

// โœ… Value classes eliminate wrapper object allocation
@JvmInline
value class UserId(val value: String) {
    fun isValid(): Boolean = value.isNotBlank()
}

@JvmInline
value class Money(val cents: Long) {
    val dollars: Double
        get() = cents / 100.0
    
    operator fun plus(other: Money) = Money(cents + other.cents)
    operator fun times(multiplier: Int) = Money(cents * multiplier)
}

class ValueClassBenefits {
    // No object allocation - value is inlined
    fun processUser(id: UserId) {
        if (id.isValid()) {
            println("Processing user: ${id.value}")
        }
    }
    
    // Calculations happen on primitives
    fun calculateTotal(items: List): Money {
        return items.fold(Money(0)) { acc, item -> acc + item }
    }
    
    // โŒ Without value classes, wrapper objects are created
    data class UserIdOld(val value: String) // Creates objects
}

Coroutines Performance

Efficient Asynchronous Programming

import kotlinx.coroutines.*

class CoroutinePerformance {
    // โœ… Use appropriate dispatchers
    suspend fun performIOOperation(): String = withContext(Dispatchers.IO) {
        // I/O operations don't block threads
        delay(100)
        "IO Result"
    }
    
    suspend fun performCPUIntensiveOperation(): Int = withContext(Dispatchers.Default) {
        // CPU-intensive work uses multiple threads
        (1..1_000_000).sum()
    }
    
    // โœ… Concurrent processing with async
    suspend fun processDataConcurrently(items: List): List {
        return items.map { item ->
            async(Dispatchers.IO) {
                processItem(item)
            }
        }.awaitAll()
    }
    
    private suspend fun processItem(item: String): String {
        delay(10) // Simulate I/O
        return item.uppercase()
    }
    
    // โœ… Use Channel for producer-consumer scenarios
    fun setupProducerConsumer() = runBlocking {
        val channel = Channel(capacity = 100) // Buffered channel
        
        launch(Dispatchers.Default) {
            // Producer
            repeat(1000) { i ->
                channel.send(i)
            }
            channel.close()
        }
        
        launch(Dispatchers.Default) {
            // Consumer
            for (item in channel) {
                // Process item
                println("Processed: $item")
            }
        }
    }
    
    // โœ… Avoid blocking operations in coroutines
    suspend fun goodAsyncOperation() {
        delay(1000) // Suspends coroutine, doesn't block thread
    }
    
    // โŒ Don't do this in coroutines
    suspend fun badAsyncOperation() {
        Thread.sleep(1000) // Blocks thread!
    }
}

Performance Measurement

Benchmarking and Profiling

import kotlin.system.measureTimeMillis
import kotlin.system.measureNanoTime

class PerformanceMeasurement {
    // โœ… Basic time measurement
    fun measureOperationTime() {
        val timeMillis = measureTimeMillis {
            (1..1_000_000).sum()
        }
        println("Operation took ${timeMillis}ms")
        
        val timeNanos = measureNanoTime {
            (1..1000).sum()
        }
        println("Operation took ${timeNanos}ns")
    }
    
    // โœ… Multiple runs for accurate measurement
    fun benchmarkOperation(operation: () -> Unit, runs: Int = 10): Double {
        // Warm up JVM
        repeat(5) { operation() }
        
        val times = mutableListOf()
        repeat(runs) {
            val time = measureNanoTime(operation)
            times.add(time)
        }
        
        return times.average()
    }
    
    // โœ… Memory usage measurement
    fun measureMemoryUsage(operation: () -> Unit): Long {
        val runtime = Runtime.getRuntime()
        
        // Force garbage collection
        System.gc()
        Thread.sleep(100)
        
        val beforeMemory = runtime.totalMemory() - runtime.freeMemory()
        
        operation()
        
        val afterMemory = runtime.totalMemory() - runtime.freeMemory()
        
        return afterMemory - beforeMemory
    }
    
    // โœ… Comprehensive performance test
    fun performanceTest() {
        data class TestResult(
            val name: String,
            val timeNs: Double,
            val memoryBytes: Long
        )
        
        val operations = mapOf(
            "Array sum" to { (1..100_000).toIntArray().sum() },
            "List sum" to { (1..100_000).toList().sum() },
            "Sequence sum" to { (1..100_000).asSequence().sum() }
        )
        
        val results = operations.map { (name, operation) ->
            val avgTime = benchmarkOperation(operation, 20)
            val memory = measureMemoryUsage(operation)
            TestResult(name, avgTime, memory)
        }
        
        println("Performance Results:")
        results.forEach { result ->
            println("${result.name}: ${result.timeNs.toLong()}ns, ${result.memoryBytes} bytes")
        }
    }
}

JVM Optimization Tips

class JVMOptimization {
    // โœ… Use @JvmStatic for better performance
    companion object {
        @JvmStatic
        fun staticMethod() {
            // No instance allocation needed
        }
        
        // โŒ Regular companion object method creates instance
        fun nonStaticMethod() {
            // Creates companion object instance
        }
    }
    
    // โœ… Use const for compile-time constants
    companion object {
        const val COMPILE_TIME_CONSTANT = "value" // Inlined by compiler
        val RUNTIME_CONSTANT = "value" // Property access
    }
    
    // โœ… Avoid reflection when possible
    fun directAccess(user: User) {
        println(user.name) // Direct property access
    }
    
    // โŒ Reflection is slower
    fun reflectionAccess(user: User) {
        val property = user::class.members.find { it.name == "name" }
        println(property?.call(user)) // Reflection call
    }
    
    // โœ… Use primitive-specialized functions
    fun sumInts(numbers: IntArray): Long {
        return numbers.fold(0L) { acc, n -> acc + n }
    }
    
    // โœ… Avoid autoboxing in hot paths
    fun hotPath(numbers: IntArray): Int {
        var max = Int.MIN_VALUE
        for (num in numbers) {
            if (num > max) {
                max = num
            }
        }
        return max
    }
}

Best Practices Summary

Performance Guidelines

  • Use inline functions for higher-order functions in hot paths
  • Choose appropriate collections based on access patterns
  • Prefer sequences for large data processing chains
  • Use primitive arrays instead of boxed collections when possible
  • Implement object pooling for frequently created objects
  • Use value classes to eliminate wrapper object allocation
  • Profile before optimizing to identify real bottlenecks

Common Performance Pitfalls

class PerformancePitfalls {
    // โŒ Creating objects in loops
    fun badLoop(items: List) {
        items.forEach { item ->
            val regex = Regex("\\d+") // New object each iteration
            regex.matches(item)
        }
    }
    
    // โœ… Create objects outside loops
    fun goodLoop(items: List) {
        val regex = Regex("\\d+") // Create once
        items.forEach { item ->
            regex.matches(item)
        }
    }
    
    // โŒ String concatenation in loops
    fun badStringConcatenation(items: List): String {
        var result = ""
        items.forEach { result += it } // Creates new string each time
        return result
    }
    
    // โœ… Use StringBuilder or joinToString
    fun goodStringConcatenation(items: List): String {
        return items.joinToString("")
    }
    
    // โŒ Unnecessary collection operations
    fun badCollectionOperations(numbers: List): List {
        return numbers
            .map { it } // Unnecessary identity mapping
            .filter { true } // Unnecessary filtering
            .toList() // Unnecessary conversion
    }
    
    // โœ… Minimize operations
    fun goodCollectionOperations(numbers: List): List {
        return numbers // Direct return when no transformation needed
    }
}

Key Takeaways

  • Kotlin performance is generally equivalent to Java performance
  • Inline functions eliminate lambda object creation overhead
  • Choose collections based on usage patterns and performance requirements
  • Use sequences for large data processing to avoid intermediate collections
  • Value classes eliminate wrapper object allocation while maintaining type safety
  • Measure performance before and after optimizations
  • Focus on algorithmic improvements before micro-optimizations

Practice Exercises

  1. Benchmark different collection operations and analyze the results
  2. Implement an object pool for a frequently used class in your application
  3. Profile a Kotlin application and identify performance bottlenecks
  4. Convert wrapper classes to value classes and measure the performance impact

Quiz

  1. When should you use inline functions in Kotlin?
  2. What are the benefits of using value classes?
  3. How do sequences improve performance over regular collection operations?
Show Answers
  1. Use inline functions for higher-order functions that are called frequently, especially in performance-critical code, to eliminate lambda object creation overhead.
  2. Value classes eliminate object allocation while maintaining type safety, reducing memory usage and improving performance for wrapper types.
  3. Sequences use lazy evaluation, processing elements one by one through the entire chain instead of creating intermediate collections, reducing memory usage and improving performance for large datasets.