第5章: 制御構造 - マシンレベルの理解

学習目標

この章を終えると、以下ができるようになります:

  • if文で条件分岐を記述でき、アセンブリレベルでの動作を理解できる
  • for文の多様な使い方をマスターし、ループ構造の最適化を理解できる
  • switch文で複雑な条件分岐を簡潔に書け、jump tableの仕組みを理解できる
  • defer、panic、recoverの内部実装を理解できる
  • 分岐予測とCPUパイプラインへの影響を理解できる

---

1. if文のマシンレベル実装

1.1 基本的なif文とアセンブリ

if文は、CPUレベルでは比較命令(CMP)条件付きジャンプ命令(JMP)に変換されます。

x := 10

if x > 0 {
    fmt.Println("Positive")
}

対応するアセンブリコード(x86-64):

    ; x := 10
    MOVQ    $10, AX          ; AX = 10

    ; if x > 0
    CMPQ    $0, AX           ; AXと0を比較(flags設定)
    JLE     .else            ; AX <= 0 なら .else へジャンプ

    ; then分岐
    CALL    fmt.Println      ; fmt.Println("Positive")
    JMP     .end             ; 無条件で .end へ

.else:
    ; else分岐(この例では何もしない)

.end:
    ; if文の終了

🔑 キーポイント: 条件フラグレジスタ

CPUは比較結果をFLAGSレジスタに保存します:

  • ZF (Zero Flag): 結果がゼロなら1
  • SF (Sign Flag): 結果が負なら1
  • CF (Carry Flag): キャリーが発生したら1
  • OF (Overflow Flag): オーバーフローが発生したら1

CMP命令の内部動作:
┌─────────────────────────────────┐
│ CMPQ $0, AX                     │
│                                 │
│ 内部的に: AX - 0 を計算         │
│ 結果を捨てる(レジスタに保存しない)│
│ FLAGSレジスタのみ更新           │
└─────────────────────────────────┘

FLAGSレジスタ:
┌──┬──┬──┬──┬──┬──┬──┬──┐
│OF│SF│ZF│AF│PF│CF│  │  │
├──┼──┼──┼──┼──┼──┼──┼──┤
│ 0│ 0│ 0│ 0│ 1│ 0│  │  │
└──┴──┴──┴──┴──┴──┴──┴──┘
     ↑
     x > 0 の場合、SF=0, ZF=0

1.2 条件付きジャンプ命令の種類

Goのif文は、様々な条件付きジャンプ命令に変換されます:

// 等価比較
if x == y { }     // JE  (Jump if Equal)        ZF == 1
if x != y { }     // JNE (Jump if Not Equal)    ZF == 0

// 符号付き比較
if x > y { }      // JG  (Jump if Greater)      ZF==0 && SF==OF
if x >= y { }     // JGE (Jump if Greater/Equal) SF==OF
if x < y { }      // JL  (Jump if Less)         SF != OF
if x <= y { }     // JLE (Jump if Less/Equal)   ZF==1 || SF!=OF

// 符号なし比較
if uint(x) > uint(y) { }   // JA  (Jump if Above)
if uint(x) < uint(y) { }   // JB  (Jump if Below)

💡 豆知識: 符号付きと符号なしの違い

x := int8(-1)    // 0xFF (2の補数表現)
y := int8(1)     // 0x01

// 符号付き比較
if x < y { }     // true: -1 < 1
// CMPB $1, AL
// JL   .true      ; SFとOFを見る

// 符号なし比較
if uint8(x) < uint8(y) { }  // false: 255 < 1
// CMPB $1, AL
// JB   .true      ; CFを見る

1.3 else if チェーンの最適化

複数のelse if は順次比較になります:

if x > 0 {
    fmt.Println("Positive")
} else if x < 0 {
    fmt.Println("Negative")
} else {
    fmt.Println("Zero")
}

アセンブリコード:

    CMPQ    $0, AX
    JLE     .check_negative    ; x <= 0 なら次の条件へ

    ; x > 0 の場合
    CALL    fmt.Println        ; "Positive"
    JMP     .end

.check_negative:
    CMPQ    $0, AX
    JGE     .zero              ; x >= 0 なら zero へ

    ; x < 0 の場合
    CALL    fmt.Println        ; "Negative"
    JMP     .end

.zero:
    ; x == 0 の場合
    CALL    fmt.Println        ; "Zero"

.end:

⚠️ パフォーマンス考慮事項

// 非効率: 3つの条件すべてをチェック
if x > 100 {
    // レアケース
} else if x > 10 {
    // 頻繁なケース
} else if x > 0 {
    // 頻繁なケース
}

// 効率的: 頻繁なケースを先に
if x > 0 && x <= 10 {
    // 頻繁なケース(早期リターン)
} else if x > 10 && x <= 100 {
    // 頻繁なケース
} else if x > 100 {
    // レアケース
}

1.4 初期化付きif文のスコープ管理

// 初期化付きif
if num := computeValue(); num > 0 {
    fmt.Println("Positive:", num)
} else {
    fmt.Println("Non-positive:", num)
}
// numはこのスコープにない

スタックフレームでの管理:

スタックフレーム構造:
┌────────────────────┐ 低位アドレス
│ リターンアドレス    │
├────────────────────┤
│ 前のフレームポインタ│
├────────────────────┤ ← RBP (フレームポインタ)
│ num (8 bytes)      │ ← RBP-8
├────────────────────┤
│ その他のローカル変数│
└────────────────────┘ 高位アドレス

if文終了後:
┌────────────────────┐
│ リターンアドレス    │
├────────────────────┤
│ 前のフレームポインタ│
├────────────────────┤ ← RBP
│ (numはスコープ外)   │ メモリは残るがアクセス不可
└────────────────────┘

🔑 キーポイント: スコープとスタック領域

初期化付きif文の変数は、if文のブロックが終了するとコンパイラがアクセスを禁止します。 メモリ自体は残りますが、論理的にアクセスできません。

1.5 論理演算子とショートサーキット評価

if expensive() && cheap() {
    // ...
}

アセンブリ実装(ショートサーキット):

    CALL    expensive          ; expensive()を呼び出し
    TESTB   AL, AL             ; 戻り値をテスト(bool → byte)
    JZ      .skip              ; falseなら cheap()をスキップ

    CALL    cheap              ; expensive()がtrueの場合のみ実行
    TESTB   AL, AL
    JZ      .skip

    ; 両方trueの場合の処理
    ; ...

.skip:

パフォーマンス測定:

// 例: 高コスト関数を先に評価(非効率)
if checkDatabase() && checkCache() {
    // checkCache()は高速だが、毎回checkDatabase()が実行される
}

// 効率的: 高速な関数を先に
if checkCache() && checkDatabase() {
    // キャッシュがfalseならデータベースチェックをスキップ
}

💡 豆知識: 分岐予測との関係

// パターンA: 予測しやすい
for i := 0; i < 1000; i++ {
    if i%2 == 0 {  // 規則的なパターン
        evenCount++
    }
}

// パターンB: 予測しにくい
for i := 0; i < 1000; i++ {
    if rand.Intn(2) == 0 {  // ランダム
        randomCount++
    }
}

パターンAは分岐予測が成功しやすく、パターンBは予測ミスが多発します。

---

2. for文のループ構造

2.1 基本的なforループのアセンブリ

for i := 0; i < 10; i++ {
    fmt.Println(i)
}

対応するアセンブリコード:

    ; i := 0
    XORQ    CX, CX             ; CX = 0(XORで高速ゼロ化)

.loop:
    ; i < 10 のチェック
    CMPQ    $10, CX
    JGE     .end               ; i >= 10 なら終了

    ; fmt.Println(i)
    MOVQ    CX, AX             ; iを引数に
    CALL    fmt.Println

    ; i++
    INCQ    CX                 ; CX++
    JMP     .loop              ; ループの先頭へ

.end:

🔑 キーポイント: ループの基本構造

すべてのループは以下の要素から構成されます:

  • 初期化: ループカウンタの設定
  • 条件チェック: ループを続けるか判定
  • ループ本体: 実際の処理
  • 更新: カウンタのインクリメント
  • ジャンプ: ループの先頭に戻る

ループのフローチャート:
┌──────────────┐
│ 初期化: i=0  │
└──────┬───────┘
       │
       ↓
┌──────────────┐
│ i < 10?      │←─────┐
└──────┬───────┘      │
       │ true         │
       ↓              │
┌──────────────┐      │
│ ループ本体    │      │
└──────┬───────┘      │
       │              │
       ↓              │
┌──────────────┐      │
│ i++          │      │
└──────┬───────┘      │
       │              │
       └──────────────┘
       │ false
       ↓
    終了

2.2 ループの最適化技術

2.2.1 ループアンローリング (Loop Unrolling)

コンパイラは小さなループを展開することがあります:

// オリジナル
sum := 0
for i := 0; i < 4; i++ {
    sum += arr[i]
}

// コンパイラによる最適化(概念的)
sum := 0
sum += arr[0]
sum += arr[1]
sum += arr[2]
sum += arr[3]

アセンブリレベルでの効果:

; 最適化前: 4回のループ
XORQ    CX, CX           ; sum = 0
XORQ    DX, DX           ; i = 0
.loop:
    ADDQ    (AX)(DX*8), CX
    INCQ    DX
    CMPQ    $4, DX
    JL      .loop

; 最適化後: ループ展開
XORQ    CX, CX           ; sum = 0
ADDQ    0(AX), CX        ; sum += arr[0]
ADDQ    8(AX), CX        ; sum += arr[1]
ADDQ    16(AX), CX       ; sum += arr[2]
ADDQ    24(AX), CX       ; sum += arr[3]

💡 利点:

  • 分岐命令の削減(JMP命令が不要)
  • ループオーバーヘッドの削減
  • パイプラインストールの削減

⚠️ 欠点:

  • コードサイズの増加
  • 命令キャッシュのミスが増える可能性

2.2.2 ループ不変コードの移動 (Loop Invariant Code Motion)

// 最適化前
for i := 0; i < len(arr); i++ {
    x := computeExpensive()  // ループ内で変わらない
    arr[i] = arr[i] * x
}

// コンパイラの最適化
x := computeExpensive()      // ループ外に移動
for i := 0; i < len(arr); i++ {
    arr[i] = arr[i] * x
}

2.2.3 強度の削減 (Strength Reduction)

// 最適化前
for i := 0; i < n; i++ {
    arr[i*8] = 0  // 乗算
}

// コンパイラの最適化
offset := 0
for i := 0; i < n; i++ {
    arr[offset] = 0
    offset += 8   // 加算(乗算より高速)
}

2.3 無限ループとwhile風のfor

// 無限ループ
for {
    if shouldStop() {
        break
    }
    doWork()
}

アセンブリコード:

.loop:
    CALL    shouldStop
    TESTB   AL, AL
    JNZ     .end           ; trueならループ脱出

    CALL    doWork
    JMP     .loop          ; 無条件ジャンプ

.end:

🔑 キーポイント: CPUパイプラインへの影響

無条件ジャンプのパイプライン:
┌────┬────┬────┬────┬────┐
│命令│Fetch│Dec │Exec│WB  │
├────┼────┼────┼────┼────┤
│JMP │ F  │ D  │ E  │ W  │ ← 分岐先が確定
│次の│    │ F  │ D  │ E  │ ← パイプライン継続(ストールなし)
│命令│    │    │ F  │ D  │
└────┴────┴────┴────┴────┘

条件付きジャンプのパイプライン(予測ミス時):
┌────┬────┬────┬────┬────┐
│命令│Fetch│Dec │Exec│WB  │
├────┼────┼────┼────┼────┤
│JNZ │ F  │ D  │ E  │ W  │ ← 分岐先が確定
│予測│    │ F  │ D  │ X  │ ← フラッシュ!
│命令│    │    │ F  │ X  │ ← フラッシュ!
│正解│    │    │    │ F  │ ← パイプライン再開(ペナルティ)
└────┴────┴────┴────┴────┘

無条件ジャンプは予測ミスがないため、パイプラインストールが発生しません。

2.4 break と continue のジャンプ構造

for i := 0; i < 10; i++ {
    if i == 5 {
        break
    }
    if i%2 == 0 {
        continue
    }
    fmt.Println(i)
}

アセンブリコード:

    XORQ    CX, CX             ; i = 0

.loop:
    CMPQ    $10, CX
    JGE     .end

    ; if i == 5
    CMPQ    $5, CX
    JE      .end               ; break → ループ終了へ

    ; if i%2 == 0
    MOVQ    CX, AX
    ANDQ    $1, AX             ; i & 1(偶数判定の最適化)
    JZ      .continue          ; continue → ループ更新へ

    ; fmt.Println(i)
    MOVQ    CX, AX
    CALL    fmt.Println

.continue:
    INCQ    CX                 ; i++
    JMP     .loop

.end:

💡 豆知識: 偶数判定の最適化

// 一般的な書き方
if i%2 == 0 { }

// コンパイラの最適化(ビット演算)
if i&1 == 0 { }

// アセンブリレベル:
// MOD命令(遅い): IDIVQ → 数十サイクル
// AND命令(速い): ANDQ  → 1サイクル

2.5 ラベル付きbreak/continueの実装

outer:
for i := 0; i < 3; i++ {
    for j := 0; j < 3; j++ {
        if i == 1 && j == 1 {
            break outer  // 外側のループを抜ける
        }
        fmt.Printf("(%d, %d) ", i, j)
    }
}

アセンブリコード:

    XORQ    BX, BX             ; i = 0

.outer_loop:
    CMPQ    $3, BX
    JGE     .outer_end

    XORQ    CX, CX             ; j = 0

.inner_loop:
    CMPQ    $3, CX
    JGE     .inner_end

    ; if i == 1 && j == 1
    CMPQ    $1, BX
    JNE     .print
    CMPQ    $1, CX
    JNE     .print
    JMP     .outer_end         ; break outer

.print:
    ; fmt.Printf("(%d, %d) ", i, j)
    MOVQ    BX, AX
    MOVQ    CX, DX
    CALL    fmt.Printf

    INCQ    CX
    JMP     .inner_loop

.inner_end:
    INCQ    BX
    JMP     .outer_loop

.outer_end:

---

3. range式の内部実装

3.1 スライスのrangeループ

numbers := []int{10, 20, 30, 40, 50}
for i, num := range numbers {
    fmt.Printf("Index: %d, Value: %d\n", i, num)
}

コンパイラの内部展開:

// range式は以下のように展開される(概念的)
numbers := []int{10, 20, 30, 40, 50}
{
    _slice := numbers       // スライスのコピー(ポインタ、長さ、容量)
    _len := len(_slice)
    _index := 0

    for _index < _len {
        i := _index
        num := _slice[_index]

        // ループ本体
        fmt.Printf("Index: %d, Value: %d\n", i, num)

        _index++
    }
}

スライスの内部構造:

スライスのメモリレイアウト:
┌──────────────────────┐
│ ptr  (8 bytes)       │ → 配列へのポインタ
├──────────────────────┤
│ len  (8 bytes)       │ → 要素数
├──────────────────────┤
│ cap  (8 bytes)       │ → 容量
└──────────────────────┘

実際の配列(ヒープ上):
ptr →
┌────┬────┬────┬────┬────┐
│ 10 │ 20 │ 30 │ 40 │ 50 │
└────┴────┴────┴────┴────┘

アセンブリコード(簡略版):

    ; numbers := []int{10, 20, 30, 40, 50}
    ; スライスヘッダーをスタックに作成

    ; _slice := numbers(スライスヘッダーのコピー)
    MOVQ    numbers_ptr(SP), AX   ; ポインタ
    MOVQ    numbers_len(SP), BX   ; 長さ
    MOVQ    numbers_cap(SP), CX   ; 容量

    ; _index := 0
    XORQ    DX, DX

.loop:
    ; _index < _len
    CMPQ    BX, DX
    JGE     .end

    ; i := _index
    MOVQ    DX, R8                 ; i = _index

    ; num := _slice[_index]
    MOVQ    (AX)(DX*8), R9         ; num = ptr[_index]

    ; fmt.Printf(...)
    MOVQ    R8, AX
    MOVQ    R9, BX
    CALL    fmt.Printf

    ; _index++
    INCQ    DX
    JMP     .loop

.end:

🔑 キーポイント: rangeはスライスのコピーを作る

slice := []int{1, 2, 3}
for i, v := range slice {
    if i == 0 {
        slice = []int{10, 20, 30}  // 新しいスライスに置き換え
    }
    fmt.Println(v)  // 元のスライスの値が出力される(1, 2, 3)
}

これは、range開始時にスライスヘッダーがコピーされるためです。

3.2 マップのrangeループ

ages := map[string]int{
    "Alice": 25,
    "Bob":   30,
}

for name, age := range ages {
    fmt.Printf("%s: %d\n", name, age)
}

マップの内部構造(概念的):

ハッシュマップの構造:
┌─────────────────────┐
│ hmap (ヘッダー)      │
├─────────────────────┤
│ count: 2            │ 要素数
│ B: 0                │ バケット数の指数(2^B)
│ buckets: ptr        │ バケット配列へのポインタ
└─────────────────────┘
           │
           ↓
┌──────────────────────┐
│ bucket[0]            │
├──────────────────────┤
│ tophash[8]           │ ハッシュの上位8ビット
│ keys[8]              │ キー配列
│ values[8]            │ 値配列
│ overflow: ptr        │ オーバーフローバケット
└──────────────────────┘

rangeの内部展開:

// コンパイラの内部展開(概念的)
{
    _map := ages
    _iter := runtime.mapiterinit(&_map)  // イテレーター初期化

    for runtime.mapiternext(_iter) {     // 次の要素へ
        name := _iter.key()              // キーを取得
        age := _iter.value()             // 値を取得

        // ループ本体
        fmt.Printf("%s: %d\n", name, age)
    }
}

⚠️ 重要: マップのイテレーション順序はランダム

// 同じマップでも順序が変わる
m := map[string]int{"a": 1, "b": 2, "c": 3}

// 実行1: a, b, c
// 実行2: c, a, b
// 実行3: b, c, a

// Goは意図的にランダム化している(Go 1.0以降)

これは、順序依存のバグを防ぐための仕様です。

3.3 文字列のrangeループ(UTF-8デコード)

s := "こんにちは"
for i, r := range s {
    fmt.Printf("%d: %c (U+%04X)\n", i, r, r)
}

UTF-8エンコーディング:

"こんにちは" のバイト列:
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│E3│81│93│E3│82│93│E3│81│AB│E3│81│A1│E3│81│AF│
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
 └───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘
    "こ"      "ん"      "に"      "ち"      "は"
   (3 bytes)(3 bytes)(3 bytes)(3 bytes)(3 bytes)

rangeの内部展開:

// コンパイラの内部展開(概念的)
{
    _str := s
    _len := len(_str)
    _index := 0

    for _index < _len {
        r, size := utf8.DecodeRuneInString(_str[_index:])
        i := _index

        // ループ本体
        fmt.Printf("%d: %c (U+%04X)\n", i, r, r)

        _index += size  // UTF-8のバイト数分進む
    }
}

UTF-8デコードのアルゴリズム:

// utf8.DecodeRuneInString の概念的実装
func DecodeRuneInString(s string) (rune, int) {
    if len(s) == 0 {
        return RuneError, 0
    }

    b0 := s[0]

    // 1バイト文字 (0xxxxxxx)
    if b0 < 0x80 {
        return rune(b0), 1
    }

    // 2バイト文字 (110xxxxx 10xxxxxx)
    if b0 < 0xE0 {
        if len(s) < 2 {
            return RuneError, 1
        }
        b1 := s[1]
        return rune(b0&0x1F)<<6 | rune(b1&0x3F), 2
    }

    // 3バイト文字 (1110xxxx 10xxxxxx 10xxxxxx)
    if b0 < 0xF0 {
        if len(s) < 3 {
            return RuneError, 1
        }
        b1, b2 := s[1], s[2]
        return rune(b0&0x0F)<<12 | rune(b1&0x3F)<<6 | rune(b2&0x3F), 3
    }

    // 4バイト文字 (11110xxx 10xxxxxx 10xxxxxx 10xxxxxx)
    if b0 < 0xF8 {
        if len(s) < 4 {
            return RuneError, 1
        }
        b1, b2, b3 := s[1], s[2], s[3]
        return rune(b0&0x07)<<18 | rune(b1&0x3F)<<12 |
               rune(b2&0x3F)<<6 | rune(b3&0x3F), 4
    }

    return RuneError, 1
}

💡 豆知識: バイトループとruneループの違い

s := "こんにちは"

// バイトループ(15回)
for i := 0; i < len(s); i++ {
    fmt.Printf("%d: %X\n", i, s[i])
}
// 出力: 0: E3, 1: 81, 2: 93, ...(15行)

// runeループ(5回)
for i, r := range s {
    fmt.Printf("%d: %c\n", i, r)
}
// 出力: 0: こ, 3: ん, 6: に, 9: ち, 12: は

3.4 チャネルのrangeループ

ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch)

for num := range ch {
    fmt.Println(num)
}

rangeの内部展開:

// コンパイラの内部展開(概念的)
{
    _ch := ch

    for {
        num, ok := <-_ch  // チャネルから受信
        if !ok {          // チャネルが閉じられた
            break
        }

        // ループ本体
        fmt.Println(num)
    }
}

チャネル受信のブロッキング:

チャネル受信の状態遷移:
┌──────────────┐
│ goroutine    │
│ 実行中       │
└──────┬───────┘
       │ ch受信試行
       ↓
┌──────────────┐
│ チャネルに    │
│ データあり?  │
└──┬───────┬───┘
   │Yes    │No
   ↓       ↓
┌──────┐ ┌──────────────┐
│受信  │ │goroutineを    │
│成功  │ │スリープキュー │
└──────┘ │に追加        │
         └──────┬───────┘
                │ データ到着
                ↓
         ┌──────────────┐
         │ goroutineを   │
         │ 実行キューに  │
         └──────────────┘

---

4. switch文とjump table

4.1 基本的なswitch文

day := 2

switch day {
case 1:
    fmt.Println("Monday")
case 2:
    fmt.Println("Tuesday")
case 3:
    fmt.Println("Wednesday")
default:
    fmt.Println("Other")
}

アセンブリコード(if-else chain方式):

    MOVQ    day(SP), AX        ; day = 2

    ; case 1
    CMPQ    $1, AX
    JNE     .case2
    CALL    fmt.Println        ; "Monday"
    JMP     .end

.case2:
    CMPQ    $2, AX
    JNE     .case3
    CALL    fmt.Println        ; "Tuesday"
    JMP     .end

.case3:
    CMPQ    $3, AX
    JNE     .default
    CALL    fmt.Println        ; "Wednesday"
    JMP     .end

.default:
    CALL    fmt.Println        ; "Other"

.end:

4.2 jump tableによる最適化

連続した整数値の場合、コンパイラはjump tableを使います:

switch x {
case 0:
    doA()
case 1:
    doB()
case 2:
    doC()
case 3:
    doD()
case 4:
    doE()
}

jump tableの構造:

ジャンプテーブル(メモリ上):
┌────────────┐
│ .case0_addr│ ← テーブルの先頭
├────────────┤
│ .case1_addr│
├────────────┤
│ .case2_addr│
├────────────┤
│ .case3_addr│
├────────────┤
│ .case4_addr│
└────────────┘

アセンブリコード(jump table方式):

    MOVQ    x(SP), AX          ; AX = x

    ; 範囲チェック(0 <= x <= 4)
    CMPQ    $4, AX
    JA      .default           ; x > 4 なら default

    ; jump table経由でジャンプ
    LEAQ    .jump_table(IP), BX
    MOVQ    (BX)(AX*8), CX     ; アドレスを取得
    JMP     *CX                ; 間接ジャンプ

.case0:
    CALL    doA
    JMP     .end

.case1:
    CALL    doB
    JMP     .end

.case2:
    CALL    doC
    JMP     .end

.case3:
    CALL    doD
    JMP     .end

.case4:
    CALL    doE
    JMP     .end

.default:
    ; デフォルト処理

.end:

; ジャンプテーブル(データセクション)
.jump_table:
    .quad   .case0
    .quad   .case1
    .quad   .case2
    .quad   .case3
    .quad   .case4

🔑 キーポイント: jump tableの利点

方式 比較回数 ジャンプ回数 計算量
if-else chain 最大N回 最大N回 O(n)
jump table 1回(範囲チェック) 1回(間接ジャンプ) O(1)

パフォーマンス比較:

// 5個のcase: わずかな差
// 50個のcase: jump tableが圧倒的に高速

4.3 疎な値のswitch(ハイブリッド方式)

switch x {
case 0:
    doA()
case 10:
    doB()
case 100:
    doC()
case 1000:
    doD()
}

このような疎な値の場合、jump tableは非効率です:

無駄なジャンプテーブル(1001エントリ!):
┌────────────┐
│ .case0     │ ← 0
├────────────┤
│ .default   │ ← 1
├────────────┤
│ .default   │ ← 2
│    ...     │
├────────────┤
│ .case10    │ ← 10
├────────────┤
│ .default   │ ← 11
│    ...     │ ← メモリの無駄!

コンパイラの最適化: 二分探索 + jump table

    MOVQ    x(SP), AX

    ; 二分探索で範囲を絞る
    CMPQ    $50, AX
    JL      .lower_half        ; x < 50
    JG      .upper_half        ; x > 50
    JMP     .default           ; x == 50

.lower_half:
    CMPQ    $0, AX
    JE      .case0
    CMPQ    $10, AX
    JE      .case10
    JMP     .default

.upper_half:
    CMPQ    $100, AX
    JE      .case100
    CMPQ    $1000, AX
    JE      .case1000
    JMP     .default

.case0:
    CALL    doA
    JMP     .end

; ... 他のcase

4.4 条件式switch

score := 85

switch {
case score >= 90:
    fmt.Println("A")
case score >= 80:
    fmt.Println("B")
case score >= 70:
    fmt.Println("C")
default:
    fmt.Println("F")
}

これは常にif-else chainに変換されます(jump table不可):

    MOVQ    score(SP), AX      ; AX = 85

    ; case score >= 90
    CMPQ    $90, AX
    JGE     .caseA

    ; case score >= 80
    CMPQ    $80, AX
    JGE     .caseB

    ; case score >= 70
    CMPQ    $70, AX
    JGE     .caseC

    ; default
    JMP     .caseF

.caseA:
    CALL    fmt.Println        ; "A"
    JMP     .end

.caseB:
    CALL    fmt.Println        ; "B"
    JMP     .end

.caseC:
    CALL    fmt.Println        ; "C"
    JMP     .end

.caseF:
    CALL    fmt.Println        ; "F"

.end:

4.5 型switch(インターフェース動的ディスパッチ)

func describe(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Printf("Integer: %d\n", v)
    case string:
        fmt.Printf("String: %s\n", v)
    case bool:
        fmt.Printf("Boolean: %t\n", v)
    default:
        fmt.Printf("Unknown: %T\n", v)
    }
}

インターフェースの内部構造:

interface{}の構造(eface):
┌──────────────────────┐
│ _type (*_type)       │ → 型情報へのポインタ
├──────────────────────┤
│ data (unsafe.Pointer)│ → 実データへのポインタ
└──────────────────────┘

型情報(_type構造体):
┌──────────────────────┐
│ size: 8              │ 型のサイズ
├──────────────────────┤
│ hash: 0x...          │ 型のハッシュ
├──────────────────────┤
│ kind: kindInt        │ 型の種類
├──────────────────────┤
│ ...                  │
└──────────────────────┘

型switchのアセンブリ:

    ; interface{}からtypeを取得
    MOVQ    i_type(SP), AX     ; AX = _type pointer

    ; type hash比較(最適化)
    MOVQ    0(AX), BX          ; BX = type.hash

    ; case int
    CMPQ    $type_int_hash, BX
    JE      .case_int

    ; case string
    CMPQ    $type_string_hash, BX
    JE      .case_string

    ; case bool
    CMPQ    $type_bool_hash, BX
    JE      .case_bool

    ; default
    JMP     .default

.case_int:
    ; vを取得
    MOVQ    i_data(SP), CX     ; CX = data pointer
    MOVQ    (CX), DX           ; DX = int value
    ; fmt.Printf(...)
    JMP     .end

; ... 他のcase

💡 豆知識: 型ハッシュの衝突

型ハッシュが衝突する可能性があるため、完全な型比較も行われます:

.case_int:
    ; ハッシュ一致後の完全チェック
    LEAQ    type_int(SB), BX
    CMPQ    AX, BX             ; ポインタ比較
    JNE     .next_case         ; 衝突していたら次へ

    ; 正しい型
    ; ...

---

5. 分岐予測とCPUパイプライン

5.1 CPUパイプラインの基礎

現代のCPUはパイプライン処理で複数命令を並列実行します:

5段パイプラインの例:
┌─────┬─────┬─────┬─────┬─────┐
│段階 │  1  │  2  │  3  │  4  │ 時間
├─────┼─────┼─────┼─────┼─────┤
│命令1│ IF  │ ID  │ EX  │ MEM │ WB
│命令2│     │ IF  │ ID  │ EX  │ MEM
│命令3│     │     │ IF  │ ID  │ EX
│命令4│     │     │     │ IF  │ ID
│命令5│     │     │     │     │ IF
└─────┴─────┴─────┴─────┴─────┘

IF  = Instruction Fetch(命令フェッチ)
ID  = Instruction Decode(命令デコード)
EX  = Execute(実行)
MEM = Memory Access(メモリアクセス)
WB  = Write Back(書き戻し)

🔑 パイプラインの利点: 1命令/サイクルの実行(理想的には)

5.2 分岐によるパイプラインストール

条件分岐では、次の命令が確定するまでパイプラインが停止します:

if x > 0 {
    doA()
} else {
    doB()
}

分岐予測なしの場合:

パイプラインストール:
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│時刻 │  1  │  2  │  3  │  4  │  5  │  6  │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│CMP  │ IF  │ ID  │ EX  │ MEM │ WB  │     │
│JLE  │     │ IF  │ ID  │ EX  │ MEM │ WB  │ ← 分岐先確定
│???? │     │     │ --  │ --  │ --  │ IF  │ ← ストール!
│doA  │     │     │     │     │     │ ID  │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘

ペナルティ: 3サイクル

5.3 分岐予測の仕組み

CPUは分岐予測器(Branch Predictor)で次の命令を推測します。

5.3.1 静的分岐予測

コンパイル時に決定される予測:

// 前方分岐(forward branch): 通常はtaken(ジャンプする)
if rare_condition {
    // ...
}

// 後方分岐(backward branch): 通常はnot taken(ループ継続)
for i := 0; i < 1000; i++ {
    // ...
}

5.3.2 動的分岐予測

実行時の履歴に基づく予測:

1. 1ビット予測器:

前回の結果を記憶:
┌───────────────┐
│ 前回: taken   │
│ 予測: taken   │
└───────────────┘

問題: 交互パターンで予測ミス
if (i % 2 == 0) { }  // taken, not taken, taken, ...
→ 常に予測ミス(50%の精度)

2. 2ビット飽和カウンタ:

状態遷移図:
        taken      taken
SNT ←────→ WNT ←────→ WT ←────→ ST
   not taken   not taken   taken

SNT = Strongly Not Taken
WNT = Weakly Not Taken
WT  = Weakly Taken
ST  = Strongly Taken

予測: WNT/SNT → not taken
      WT/ST   → taken

3. グローバル履歴予測器:

過去N回の分岐履歴を記録:
┌─────────────────────────┐
│ History Register        │
│ [T T N T N T T N]       │ 8ビット履歴
└─────────────────────────┘
           │
           ↓ インデックス
┌─────────────────────────┐
│ Pattern History Table   │
│ 0: ST                   │
│ 1: WNT                  │
│ ...                     │
│ 255: WT                 │
└─────────────────────────┘

5.4 分岐予測のパフォーマンス影響

ベンチマーク例:

// パターン1: 予測可能
func predictable() {
    data := make([]int, 10000)
    for i := range data {
        data[i] = i
    }

    sum := 0
    for _, v := range data {
        if v < 5000 {  // 前半はtrue、後半はfalse(予測しやすい)
            sum += v
        }
    }
}

// パターン2: 予測不可能
func unpredictable() {
    data := make([]int, 10000)
    for i := range data {
        data[i] = rand.Intn(10000)
    }

    sum := 0
    for _, v := range data {
        if v < 5000 {  // ランダム(予測ミス多発)
            sum += v
        }
    }
}

実測結果(概算):

predictable():   10,000,000 ns
unpredictable(): 25,000,000 ns

差: 2.5倍!(分岐予測ミスのペナルティ)

5.5 分岐予測を回避する最適化

5.5.1 条件付き移動命令(CMOV)

// 分岐あり
max := a
if b > a {
    max = b
}

// 分岐なし(CMOVを使う)
max := a
if b > a {
    max = b  // コンパイラがCMOVに最適化
}

アセンブリコード:

; 分岐あり(予測ミスのリスク)
MOVQ    a(SP), AX          ; max = a
MOVQ    b(SP), BX
CMPQ    AX, BX
JLE     .end
MOVQ    BX, AX             ; max = b
.end:

; CMOV使用(分岐なし)
MOVQ    a(SP), AX          ; max = a
MOVQ    b(SP), BX
CMPQ    AX, BX
CMOVGT  BX, AX             ; BX > AX なら AX = BX(条件付き移動)

💡 CMOVの利点:

  • パイプラインストールなし
  • 予測可能な実行時間

⚠️ CMOVの欠点:

  • 両方の値を計算する(副作用がある場合は使えない)
  • 短い分岐でのみ有効

5.5.2 ビット演算による分岐削除

// 絶対値の計算(分岐あり)
func abs1(x int) int {
    if x < 0 {
        return -x
    }
    return x
}

// 絶対値の計算(分岐なし)
func abs2(x int) int {
    mask := x >> 63         // x < 0 なら -1, x >= 0 なら 0
    return (x + mask) ^ mask
}

アセンブリ比較:

; abs1(分岐あり)
MOVQ    x(SP), AX
TESTQ   AX, AX
JGE     .positive
NEGQ    AX
.positive:
RET

; abs2(分岐なし)
MOVQ    x(SP), AX
MOVQ    AX, BX
SARQ    $63, BX            ; mask = x >> 63(算術右シフト)
ADDQ    BX, AX             ; x + mask
XORQ    BX, AX             ; (x + mask) ^ mask
RET

---

6. deferのスタック実装

6.1 defer構造体

func example() {
    defer fmt.Println("First")
    defer fmt.Println("Second")
    defer fmt.Println("Third")
}

defer構造体(runtime内部):

type _defer struct {
    siz     int32       // 引数のサイズ
    started bool        // 実行開始フラグ
    sp      uintptr     // スタックポインタ
    pc      uintptr     // プログラムカウンタ
    fn      *funcval    // 遅延実行する関数
    _panic  *_panic     // panic中の場合のリンク
    link    *_defer     // 次のdefer(リンクリスト)
}

defer chainの構造:

Goroutineのdefer chain:
┌─────────────┐
│ g (goroutine)│
├─────────────┤
│ _defer: ptr │───┐
└─────────────┘   │
                  ↓
┌─────────────┐   ┌─────────────┐   ┌─────────────┐
│ _defer      │   │ _defer      │   │ _defer      │
├─────────────┤   ├─────────────┤   ├─────────────┤
│ fn: println │   │ fn: println │   │ fn: println │
│ arg: "Third"│   │ arg: "Second"│  │ arg: "First"│
│ link: ────→│   │ link: ────→│   │ link: nil   │
└─────────────┘   └─────────────┘   └─────────────┘
    最新            中間              最古

🔑 キーポイント: LIFO(後入れ先出し)

defer chainはスタック構造で、最後に登録されたdeferから実行されます。

6.2 deferの実行タイミング

func main() {
    defer fmt.Println("defer 1")
    defer fmt.Println("defer 2")

    fmt.Println("normal")

    if true {
        return  // ここでdeferが実行される
    }

    fmt.Println("unreachable")
}

アセンブリコード(概念的):

main:
    ; プロローグ
    PUSHQ   BP
    MOVQ    SP, BP

    ; defer fmt.Println("defer 1")
    LEAQ    defer1_closure(SB), AX
    CALL    runtime.deferproc

    ; defer fmt.Println("defer 2")
    LEAQ    defer2_closure(SB), AX
    CALL    runtime.deferproc

    ; fmt.Println("normal")
    CALL    fmt.Println

    ; return(deferreturnを呼ぶ)
    CALL    runtime.deferreturn  ; ← defer実行
    MOVQ    BP, SP
    POPQ    BP
    RET

runtime.deferreturnの実装:

func deferreturn() {
    gp := getg()  // 現在のgoroutine

    for {
        d := gp._defer
        if d == nil {
            return  // defer chainが空
        }

        // deferをchainから削除
        gp._defer = d.link

        // defer関数を実行
        d.fn(d.args)

        // deferプールに返却
        freedefer(d)
    }
}

6.3 deferの引数評価タイミング

func example() {
    x := 10
    defer fmt.Println(x)  // 引数は即座に評価される
    x = 20
}
// 出力: 10

メモリレイアウト:

defer登録時:
┌─────────────┐
│ _defer      │
├─────────────┤
│ fn: println │
│ args:       │
│   [0] = 10  │ ← xの値をコピー
│ link: ...   │
└─────────────┘

x = 20 実行後も、_defer.args[0]は10のまま

クロージャを使う場合:

func example2() {
    x := 10
    defer func() {
        fmt.Println(x)  // クロージャ: xへの参照を保持
    }()
    x = 20
}
// 出力: 20

defer登録時:
┌─────────────┐
│ _defer      │
├─────────────┤
│ fn: closure │
│ closure:    │
│   &x ──────→│ xのアドレスを保持
│ link: ...   │
└─────────────┘
           │
           ↓
┌─────────────┐
│ x = 10      │ ← 後で20に変更される
└─────────────┘

6.4 最適化: open-coded defer (Go 1.14+)

小さな関数では、deferをインライン展開します:

func simple() {
    defer mu.Unlock()

    // クリティカルセクション
    doWork()
}

最適化前:

simple:
    ; defer mu.Unlock()
    LEAQ    mu.Unlock, AX
    CALL    runtime.deferproc

    ; doWork()
    CALL    doWork

    ; return
    CALL    runtime.deferreturn  ; mu.Unlock()を実行
    RET

最適化後(open-coded defer):

simple:
    ; doWork()
    CALL    doWork

    ; return前に直接Unlock
    CALL    mu.Unlock            ; インライン化!
    RET

💡 利点:

  • runtime.deferproc/deferreturnのオーバーヘッド削減
  • より高速な実行

---

7. panic/recoverの実装

7.1 panicの内部構造

type _panic struct {
    argp      uintptr    // defer引数のポインタ
    arg       interface{} // panic()に渡された値
    link      *_panic    // 前のpanic(ネスト用)
    recovered bool       // recover()されたか
    aborted   bool       // 中止されたか
}

7.2 panicの実行フロー

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered:", r)
        }
    }()

    panic("Something wrong!")
}

panic実行の流れ:

1. panic("Something wrong!")
   ↓
2. runtime.gopanic()
   ┌────────────────────────┐
   │ _panic構造体を作成      │
   │ arg = "Something wrong!"│
   └────────────────────────┘
   ↓
3. defer chainを逆順実行
   ┌────────────────────────┐
   │ defer func() { ... }    │
   │   recover()を呼び出し   │
   └────────────────────────┘
   ↓
4. recover()成功
   ┌────────────────────────┐
   │ _panic.recovered = true │
   │ 通常のフローに復帰      │
   └────────────────────────┘

runtime.gopanicの実装(簡略版):

func gopanic(e interface{}) {
    gp := getg()

    // _panic構造体を作成
    var p _panic
    p.arg = e
    p.link = gp._panic
    gp._panic = &p

    // defer chainを実行
    for {
        d := gp._defer
        if d == nil {
            break
        }

        // deferを実行
        d.fn()

        // recover()された?
        if p.recovered {
            // 通常のフローに復帰
            gp._panic = p.link
            return
        }

        gp._defer = d.link
    }

    // recoverされなかった場合
    fatalpanic(&p)  // プログラム終了
}

7.3 recoverの実装

func gorecover() interface{} {
    gp := getg()
    p := gp._panic

    // panicがない、またはdefer内でない場合
    if p == nil || !p.recovered {
        return nil
    }

    // panicをrecoverとしてマーク
    p.recovered = true
    return p.arg
}

🔑 キーポイント: recoverはdeferの中でのみ有効

// 無効: defer外でrecover
func invalid() {
    recover()  // 何も起こらない
    panic("error")
}

// 有効: defer内でrecover
func valid() {
    defer func() {
        recover()  // panicをキャッチ
    }()
    panic("error")
}

7.4 スタックアンワインド

panicが発生すると、スタックアンワインドが起こります:

スタックアンワインド:
┌─────────────┐
│ main()      │
├─────────────┤
│ funcA()     │
├─────────────┤
│ funcB()     │
├─────────────┤
│ funcC()     │ ← panic発生
└─────────────┘
     ↓ アンワインド
┌─────────────┐
│ main()      │
├─────────────┤
│ funcA()     │
├─────────────┤
│ funcB()     │ ← funcC()のdeferを実行
└─────────────┘
     ↓
┌─────────────┐
│ main()      │
├─────────────┤
│ funcA()     │ ← funcB()のdeferを実行
└─────────────┘
     ↓
┌─────────────┐
│ main()      │ ← funcA()のdeferを実行
└─────────────┘
     ↓
  プログラム終了 or recover

---

8. gotoとラベルの低レベル活用

8.1 gotoの基本

func example() {
    i := 0
loop:
    fmt.Println(i)
    i++
    if i < 5 {
        goto loop
    }
}

アセンブリコード:

    XORQ    CX, CX             ; i = 0

loop:
    ; fmt.Println(i)
    MOVQ    CX, AX
    CALL    fmt.Println

    ; i++
    INCQ    CX

    ; if i < 5
    CMPQ    $5, CX
    JL      loop               ; goto loop

💡 gotoとfor文の等価性

すべてのfor文はgotoで書き換え可能:

// for文
for i := 0; i < 5; i++ {
    fmt.Println(i)
}

// goto版
{
    i := 0
loop:
    if i >= 5 {
        goto end
    }
    fmt.Println(i)
    i++
    goto loop
end:
}

8.2 リソース管理でのgoto活用

func processFile(filename string) error {
    f, err := os.Open(filename)
    if err != nil {
        goto error1
    }

    buf := make([]byte, 1024)
    _, err = f.Read(buf)
    if err != nil {
        goto error2
    }

    // 正常処理
    processData(buf)
    f.Close()
    return nil

error2:
    f.Close()
error1:
    return err
}

deferとの比較:

// defer版(より推奨)
func processFile(filename string) error {
    f, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer f.Close()  // 必ず実行される

    buf := make([]byte, 1024)
    _, err = f.Read(buf)
    if err != nil {
        return err
    }

    processData(buf)
    return nil
}

⚠️ gotoの制限

// エラー: 変数の宣言をスキップできない
func invalid() {
    goto label
    x := 10  // スキップされる
label:
    fmt.Println(x)  // エラー!
}

// エラー: ブロック内へのジャンプ
func invalid2() {
    goto label
    if true {
label:
        fmt.Println("error")  // エラー!
    }
}

---

9. 実践的なパフォーマンス最適化

9.1 ループ融合(Loop Fusion)

// 最適化前: 2つのループ
for i := 0; i < n; i++ {
    a[i] = b[i] + c[i]
}
for i := 0; i < n; i++ {
    d[i] = a[i] * 2
}

// 最適化後: 1つのループに融合
for i := 0; i < n; i++ {
    a[i] = b[i] + c[i]
    d[i] = a[i] * 2
}

💡 利点:

  • ループオーバーヘッドの削減
  • キャッシュ効率の向上

9.2 範囲チェックの削減

// コンパイラの範囲チェック
func sum(arr []int) int {
    s := 0
    for i := 0; i < len(arr); i++ {
        s += arr[i]  // 毎回範囲チェック
    }
    return s
}

// range使用(範囲チェック不要)
func sumRange(arr []int) int {
    s := 0
    for _, v := range arr {
        s += v  // 範囲チェックなし
    }
    return s
}

アセンブリ比較:

; sum(範囲チェックあり)
.loop:
    CMPQ    CX, len(arr)
    JGE     .panic           ; i >= len ならpanic
    ADDQ    (AX)(CX*8), BX
    INCQ    CX
    JMP     .loop

; sumRange(範囲チェックなし)
.loop:
    CMPQ    CX, len(arr)
    JGE     .end             ; ループ終了のみ
    ADDQ    (AX)(CX*8), BX
    INCQ    CX
    JMP     .loop

9.3 分岐予測の最適化

// データをソートすると分岐予測が改善される
func processData(data []int) int {
    sum := 0
    for _, v := range data {
        if v < threshold {  // ソート済みなら予測しやすい
            sum += v
        }
    }
    return sum
}

// ベンチマーク
func BenchmarkUnsorted(b *testing.B) {
    data := generateRandomData(10000)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        processData(data)
    }
}

func BenchmarkSorted(b *testing.B) {
    data := generateRandomData(10000)
    sort.Ints(data)  // ソート
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        processData(data)
    }
}

// 結果:
// BenchmarkUnsorted    5000    250000 ns/op
// BenchmarkSorted     10000    120000 ns/op

---

10. 自己確認問題

問題1: if文のアセンブリ

次のGo言語コードに対応するアセンブリコードで使われる命令は?

if x >= 10 {
    fmt.Println("大きい")
}

🔑 解答: CMPQ $10, AXJL .skip(x < 10ならスキップ)

---

問題2: jump tableの条件

次のうち、jump tableが使われる可能性が高いのは?

A. switch x { case 0, 100, 200: ... } B. switch x { case 0, 1, 2, 3, 4: ... } C. switch { case x > 10: ... }

🔑 解答: B(連続した整数値)

---

問題3: rangeのコピー

次のコードの出力は?

slice := []int{1, 2, 3}
for i, v := range slice {
    if i == 0 {
        slice = []int{10, 20, 30}
    }
    fmt.Print(v, " ")
}

🔑 解答: 1 2 3(rangeは元のスライスのコピーを使う)

---

問題4: deferの実行順序

次のコードの出力は?

func main() {
    defer fmt.Print("A")
    defer fmt.Print("B")
    defer fmt.Print("C")
    fmt.Print("D")
}

🔑 解答: DCBA(Dが先、deferはLIFO)

---

問題5: 分岐予測

次のうち、分岐予測ミスが最も少ないのは?

A. if rand.Intn(2) == 0 { } B. if i%2 == 0 { }(ループ内) C. if i < 1000 { }(ループ内、i=0から開始)

🔑 解答: C(ほぼ常にtrue → 予測しやすい)

---

問題6: switch最適化

次のswitch文は、どの最適化が適用される可能性が高い?

switch x {
case 0, 1, 2, 3, 4, 5, 6, 7, 8, 9:
    doSomething()
}

🔑 解答: jump table(0-9の連続値)

---

問題7: recoverの有効性

次のうち、panicをrecoverできるのは?

A. recover(); panic("error") B. defer func() { recover() }(); panic("error") C. go func() { recover() }(); panic("error")

🔑 解答: B(defer内でのみrecover有効)

---

問題8: UTF-8のrange

次のコードの出力行数は?

s := "Go言語"
for range s {
    fmt.Println("x")
}

🔑 解答: 4行(G, o, 言, 語 の4文字)

---

問題9: ループアンローリング

ループアンローリングの主な利点は?

A. メモリ使用量の削減 B. 分岐命令の削減 C. 変数の節約

🔑 解答: B(ジャンプ命令が減る)

---

問題10: defer引数の評価

次のコードの出力は?

func main() {
    x := 1
    defer fmt.Println(x)
    defer func() { fmt.Println(x) }()
    x = 2
}

🔑 解答:

2
1
(クロージャは参照、通常のdeferは値コピー。LIFOなので2→1の順)

---

問題11: gotoの制限

次のコードはコンパイルエラーになる?

func test() {
    goto end
    x := 10
end:
    fmt.Println(x)
}

🔑 解答: Yes(変数宣言をスキップできない)

---

問題12: 条件付き移動(CMOV)

次のうち、CMOVに最適化される可能性が高いのは?

A.

if x > 0 {
    return expensiveFunc()
} else {
    return cheapFunc()
}

B.

if x > 0 {
    return a
} else {
    return b
}

🔑 解答: B(両方の値を計算できる簡単な式)

---

まとめ

重要ポイント

  • if文: CMP/JMP命令に変換、分岐予測が重要
  • for文: すべてのループはgotoで表現可能、最適化技術が多数
  • range: 型によって内部実装が異なる(スライス、マップ、文字列、チャネル)
  • switch文: jump table(連続値)vs if-else chain(疎な値)
  • defer: LIFO構造、open-coded deferで高速化
  • panic/recover: スタックアンワインド、recoverはdefer内のみ
  • 分岐予測: パフォーマンスに大きな影響、予測しやすいパターンを意識
  • パフォーマンスのヒント

  • ✅ 頻繁なケースを先にチェック
  • ✅ rangeを積極的に使う(範囲チェック削減)
  • ✅ 予測しやすい分岐パターンを使う
  • ✅ 小さなループは手動で展開を検討
  • ✅ deferは少数なら影響小(open-coded defer)
  • ⚠️ ランダムな分岐は遅い
  • ⚠️ 疎なswitch文は複数のif文と同等

次の章では、関数とメソッドについて学びます。