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
- Benchmark different collection operations and analyze the results
- Implement an object pool for a frequently used class in your application
- Profile a Kotlin application and identify performance bottlenecks
- Convert wrapper classes to value classes and measure the performance impact
Quiz
- When should you use inline functions in Kotlin?
- What are the benefits of using value classes?
- How do sequences improve performance over regular collection operations?
Show Answers
- Use inline functions for higher-order functions that are called frequently, especially in performance-critical code, to eliminate lambda object creation overhead.
- Value classes eliminate object allocation while maintaining type safety, reducing memory usage and improving performance for wrapper types.
- 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.