第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, AX と JL .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文と同等
パフォーマンスのヒント
次の章では、関数とメソッドについて学びます。