Kotlin: Kotlinのパフォーマンス最適化

最終更新:2026-08-26

パフォーマンスで重要なのは、早すぎる最適化ではなく、測定に基づく適切な選択です。Charlieは、数百万件の注文で中間コレクションの割り当てを避けるためにListチェーンをSequenceへ置き換え、ボクシングをなくすためにArray<Int>の代わりにIntArrayを使い、すべての最適化をJMHで検証しています。

1. 学習内容


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) コレクション選定の決定木

100%
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 なら、これらすべてを自動的に処理してくれます。

📖 まとめ


📝 練習問題

  1. 初心者 (⭐): measureTimeMillis を使用して、100,000 個の要素に対する ListSequence の実行時間を比較してください。ヒント: asSequence()
  2. 中級 (⭐⭐): DoubleArray を使用して注文総額を計算し、List<Double> と性能を比較してください。ヒント: DoubleArray(size) { ... }
  3. 上級 (⭐⭐⭐): 100万件の注文を対象に、List/Sequence/Flowのスループットを比較する正式なJMHベンチマークを作成してください。ヒント: @Benchmark + @Setup

← 前へ | 次へ →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%