Kotlin: Kotlinのパフォーマンス最適化
最終更新:2026-08-26
パフォーマンスで重要なのは、早すぎる最適化ではなく、測定に基づく適切な選択です。Charlieは、数百万件の注文で中間コレクションの割り当てを避けるためにListチェーンをSequenceへ置き換え、ボクシングをなくすためにArray<Int>の代わりにIntArrayを使い、すべての最適化をJMHで検証しています。
1. 学習内容
inlineの機能:ラムダオブジェクトの割り当てを排除する- コレクションの選択:
List対Sequence対Flow対Array - コルーチン・ディスパッチャーのチューニング
- プリミティブ型の配列:
IntArray対Array<Int> - Charlieの実践ガイド:100万件規模のスループット最適化
2. ある建築家の実話
(1) 課題:100万件の注文処理でメモリ不足が発生
CharlieのOrderProcessorは、100万件の注文を処理する際、各段階で新しいコレクションを作成していました。その結果、メモリ使用量のピークは4GBに達し、GCの停止時間は2秒に及びました。
(2) パフォーマンス最適化ソリューション
KOTLIN
// Before: 3 intermediate collections, 4GB peak memory
orders.map { enrich(it) }.filter { it.total > 100 }.groupBy { it.region }
// After: 0 intermediate collections, 200MB peak memory
orders.asSequence()
.map { enrich(it) }
.filter { it.total > 100 }
.groupBy { it.region } // Only 1 final collection
シーケンスの遅延評価 + プリミティブ型の配列 + コルーチンプールのチューニング = メモリ使用量を95%削減、スループットを10倍向上。
3. インライン関数によるオーバーヘッドの解消
(1) ラムダの隠れたコスト
KOTLIN
// Non-inline: Lambda compiles to anonymous class
fun process(order: Order, block: (Order) -> String): String {
return block(order) // Creates Function1 object every call
}
// Inline: Lambda code is inlined at call site
inline fun process(order: Order, block: (Order) -> String): String {
return block(order) // No object allocation!
}
(2) インライン効果の測定
KOTLIN
// JMH micro-benchmark (conceptual)
// Non-inline: ~50ns per call (object allocation)
// Inline: ~5ns per call (no allocation, JIT inlines further)
(3) インラインでの使用に関するガイドライン
| シナリオ | 推奨事項 | 理由 |
|---|---|---|
| 高階関数(本体1~5行) | ✅ インライン | ラムダオブジェクトの割り当てを排除 |
| 具体化された型パラメータ | ✅ インライン化必須 | コンパイル時に型を保持 |
| 関数本体が大きい(20行以上) | ❌ インライン化しない | コードサイズが増大する |
| ラムダが保存・渡される | ❌ インライン化しない | インライン化されたラムダは存在しなくなる |
4. コレクションの選定
(1) 4つのデータ処理手法
KOTLIN
// 1. List (Eager): each step creates new collection
val result1 = orders
.map { enrich(it) } // New List
.filter { it.total > 100 } // New List
.toList() // New List
// 2. Sequence (Lazy): processes one element at a time
val result2 = orders.asSequence()
.map { enrich(it) }
.filter { it.total > 100 }
.toList() // Only 1 List
// 3. Flow (Async Lazy): suspend-capable lazy stream
val result3 = orders.asFlow()
.map { enrichAsync(it) } // Can be suspend
.filter { it.total > 100 }
.toList() // Only 1 List
// 4. Array: lowest overhead, fixed size
val result4 = ordersArray
.map { enrich(it) } // Creates new Array
.filter { it.total > 100 } // No filter on Array
(2) コレクション選定の決定木
flowchart LR
A[Data Processing] --> B{Async needed?}
B -->|Yes| C[Flow]
B -->|No| D{Data size?}
D -->|Large >10K| E{Multiple steps?}
D -->|Small <10K| F[List]
E -->|Yes| G[Sequence]
E -->|No| F
A --> H{Primitive types?}
H -->|Yes| I[IntArray/DoubleArray]
H -->|No| A
(3) 性能比較
| アプローチ | メモリ | CPU | 適した用途 |
|---|---|---|---|
List |
O(n×ステップ数) | 低 (キャッシュに優しい) | データ量が少なく、ステップ数が少ない |
Sequence |
O(1) | 中程度(要素ごとにフルパイプライン) | データ量が多く、処理ステップが多い |
Flow |
O(1) | 中程度 + 非同期処理のオーバーヘッド | 非同期データソース |
Array |
O(n) | 最小(ボクシングなし) | プリミティブ型、固定サイズ |
5. プリミティブ型の配列
(1) IntArray 対 Array<Int>
KOTLIN
// Array<Int>: each element is boxed Integer object
val boxed: Array<Int> = Array(1_000_000) { it }
// Memory: ~24MB (4MB data + 20MB object headers)
// IntArray: primitive int[], no boxing
val primitive: IntArray = IntArray(1_000_000) { it }
// Memory: ~4MB (no object overhead)
// 6x memory savings for primitive arrays!
(2) プリミティブ配列の選択
| タイプ | ボックス配列 | プリミティブ配列 | 節約額 |
|---|---|---|---|
| Int | Array<Int> |
IntArray |
約6倍 |
| ロング | Array<Long> |
LongArray |
約6倍 |
| ダブル | Array<Double> |
DoubleArray |
約6倍 |
| ブール値 | Array<Boolean> |
BooleanArray |
約8倍 |
6. コルーチン・ディスパッチャーのチューニング
(1) ディスパッチャーの選定
KOTLIN
// Default: CPU-bound (parallelism = CPU cores)
launch(Dispatchers.Default) { sortLargeCollection() }
// IO: blocking I/O (up to 64 threads by default)
launch(Dispatchers.IO) { queryDatabase() }
// Custom: for specific I/O patterns
val orderIoDispatcher = Executors.newFixedThreadPool(32)
.asCoroutineDispatcher()
// Increase IO pool size
System.setProperty("kotlinx.coroutines.io.parallelism", "128")
(2) ディスパッチャのチューニング比較
| シナリオ | デフォルト | 調整後 | 改善率 |
|---|---|---|---|
| 64件の同時DBクエリ | IOのデフォルト:64スレッド | カスタム:128スレッド | スループット2倍 |
| CPU負荷の高い計算 | デフォルト 4 コア | 固定 8 スレッド | スループット 2 倍 |
| 混合ワークロード | 共有I/O | 分離されたスレッドプール | レイテンシ50%削減 |
7. JMHベンチマーク
(1) JMHの設定
KOTLIN
// build.gradle.kts
dependencies {
implementation("org.openjdk.jmh:jmh-core:1.37")
implementation("org.openjdk.jmh:jmh-generator-annprocess:1.37")
}
(2) ベンチマークの例
KOTLIN
@State(Scope.Benchmark)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
open class OrderProcessingBenchmark {
private lateinit var orders: List<Order>
@Setup
fun setup() {
orders = (1..100_000).map {
Order("ORD-$it", it * 10.0, "CONFIRMED", "Customer-$it")
}
}
@Benchmark
fun listPipeline(): Map<String, List<Order>> {
return orders
.filter { it.total > 1_000 }
.map { it.copy(total = it.total * 0.9) }
.groupBy { it.customer }
}
@Benchmark
fun sequencePipeline(): Map<String, List<Order>> {
return orders.asSequence()
.filter { it.total > 1_000 }
.map { it.copy(total = it.total * 0.9) }
.groupBy { it.customer }
}
}
8. 完全な例:100万オーダーのスループット最適化
▶ サンプル:大規模注文処理の最適化
KOTLIN
// ============================================
// OrderProcessor - Performance Optimization
// Feature: Sequence, IntArray, Coroutines, Benchmarking
// ============================================
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis
data class Order(val id: String, val total: Double, val status: String, val customer: String)
fun generateOrders(count: Int): List<Order> = (1..count).map {
Order("ORD-$it", it * 0.1, if (it % 5 == 0) "CANCELLED" else "CONFIRMED", "CUST-${it % 100}")
}
// Strategy 1: List (eager)
fun processWithList(orders: List<Order>): Double {
return orders
.filter { it.status != "CANCELLED" }
.map { it.total }
.sum()
}
// Strategy 2: Sequence (lazy)
fun processWithSequence(orders: List<Order>): Double {
return orders.asSequence()
.filter { it.status != "CANCELLED" }
.map { it.total }
.sum()
}
// Strategy 3: IntArray (primitive, no boxing)
fun processWithIntArray(totals: DoubleArray): Double {
return totals.sum()
}
// Strategy 4: Coroutines (parallel)
suspend fun processWithCoroutines(orders: List<Order>, chunkSize: Int = 10_000): Double {
return coroutineScope {
orders.chunked(chunkSize)
.map { chunk -> async(Dispatchers.Default) { chunk.filter { it.status != "CANCELLED" }.sumOf { it.total } } }
.awaitAll()
.sum()
}
}
fun main() = runBlocking {
val orderCount = 1_000_000
println("Generating $orderCount orders...")
val orders = generateOrders(orderCount)
val totalsArray = DoubleArray(orderCount) { orders[it].total }
println("\n=== Performance Benchmark ===\n")
// List
val listTime = measureTimeMillis { val r = processWithList(orders); println("List result: \$$r USD") }
println("List time: ${listTime}ms\n")
// Sequence
val seqTime = measureTimeMillis { val r = processWithSequence(orders); println("Sequence result: \$$r USD") }
println("Sequence time: ${seqTime}ms\n")
// IntArray
val arrayTime = measureTimeMillis { val r = processWithIntArray(totalsArray); println("DoubleArray result: \$$r USD") }
println("DoubleArray time: ${arrayTime}ms\n")
// Coroutines
val coroTime = measureTimeMillis { val r = processWithCoroutines(orders); println("Coroutine result: \$$r USD") }
println("Coroutine time: ${coroTime}ms\n")
// Summary
println("=== Optimization Summary ===")
println(" List vs Sequence speedup: ${"%.1f".format(listTime.toDouble() / seqTime)}x")
println(" List vs DoubleArray speedup: ${"%.1f".format(listTime.toDouble() / arrayTime)}x")
println(" List vs Coroutine speedup: ${"%.1f".format(listTime.toDouble() / coroTime)}x")
}
出力(サンプル。実際の結果はハードウェアによって異なります):
TEXT
📖 参照専用
Generating 1000000 orders...
=== Performance Benchmark ===
List result: $4.99995E7 USD
List time: 120ms
Sequence result: $4.99995E7 USD
Sequence time: 45ms
DoubleArray result: $5.0E7 USD
DoubleArray time: 3ms
Coroutine result: $4.99995E7 USD
Coroutine time: 35ms
=== Optimization Summary ===
List vs Sequence speedup: 2.7x
List vs DoubleArray speedup: 40.0x
List vs Coroutine speedup: 3.4x
❓ よくある質問
Q いつパフォーマンスの最適化を行うべきですか?
A まずは正しいコードを書き、ボトルネックを特定し、重要な箇所を最適化してください。「時期尚早な最適化は万悪の根源」と言われますが、適切なデータ構造(シーケンスとリストのどちらを使うか)を選ぶことは、時期尚早な最適化ではなく、設計上の判断です。
Q インライン化によってAPK/IPAのサイズが増加しますか?
A はい。インライン化される呼び出しごとに、コードが呼び出し元にコピーされます。小さく、頻繁に呼び出される関数ほど、インライン化の恩恵を大きく受けます。一方、大きな関数をインライン化すると、サイズが肥大化します。標準ライブラリでは、すでにこのバランスが適切に取られています。
Q Sequenceは常にListよりも高速ですか?
A 必ずしもそうとは限りません。データ量が少なく(1000未満)、操作が1ステップのみの場合、Listの方が高速な場合があります(キャッシュ効率が良く、Sequenceのラッパーによるオーバーヘッドがないため)。Sequenceは、大規模なデータセットに対して3ステップ以上の操作を行う場合に真価を発揮します。
Q IntArrayと
List<Int>、どちらが高速ですか?A IntArrayはボクシングやオブジェクトの割り当てを回避するため、走査や合計処理が5~10倍高速です。 ただし、IntArrayは関数型演算子(map/filter)をサポートしていないため、手動でのループ処理や変換が必要になります。
Q コルーチンは常にスレッドよりも高速ですか?
A いいえ。CPUに依存するタスクの場合、コルーチンとスレッドのパフォーマンスはほぼ同等です。コルーチンが真価を発揮するのはI/Oに依存するシナリオであり、少数のスレッドで大規模な並行I/Oを処理し、スレッドのブロックによるオーバーヘッドを回避できます。
Q Kotlinのパフォーマンスを適切に測定するにはどうすればよいですか?
A JMH(Java Microbenchmark Harness)を使用してください。 マイクロベンチマークには
measureTimeMillis を使用しないでください。JIT コンパイル、GC、クラス読み込みのすべてが結果を歪めてしまいます。JMH なら、これらすべてを自動的に処理してくれます。📖 まとめ
inlineはラムダオブジェクトの割り当てを排除しますが、大規模な関数をインライン化するとコードサイズが増大しますSequenceの遅延評価により、中間コレクションを回避できます。大規模なデータや多段階の操作に適しています。IntArray/DoubleArrayボクシングを回避;数値計算が5~10倍高速化- コルーチン・ディスパッチャーの調整:I/O ボトルネックの場合は
Dispatchers.IOを、CPU ボトルネックの場合はDispatchers.Defaultを使用する - JMHはマイクロベンチマークの標準ツールです。手動での測定は避けてください。
- 最適化の手順:修正 → 測定 → 最適化 → 再度測定
📝 練習問題
- 初心者 (⭐):
measureTimeMillisを使用して、100,000 個の要素に対するListとSequenceの実行時間を比較してください。ヒント:asSequence() - 中級 (⭐⭐):
DoubleArrayを使用して注文総額を計算し、List<Double>と性能を比較してください。ヒント:DoubleArray(size) { ... } - 上級 (⭐⭐⭐): 100万件の注文を対象に、List/Sequence/Flowのスループットを比較する正式なJMHベンチマークを作成してください。ヒント:
@Benchmark+@Setup