第8章:計算機アーキテクチャと性能最適化の科学

8.1 計算機アーキテクチャの歴史と基礎

8.1.1 フォン・ノイマン・アーキテクチャ

現代のほぼ全てのコンピュータは、1945年にJohn von Neumannが提案したアーキテクチャに基づいている。このアーキテクチャの核心的な特徴を理解することは、性能最適化の第一歩である。

フォン・ノイマン・アーキテクチャの5大コンポーネント

graph TD
    subgraph CPU
        CU[Control Unit]
        DP[Datapath]
        ALU
        Reg[Registers]
        DP --- ALU
        DP --- Reg
        CU --- DP
    end
    CPU <-->|Bus| MEM[Main Memory]
    MEM <-->|Bus| IO[Input/Output]

フォン・ノイマン・ボトルネック

この古典的設計には根本的な性能限界がある。CPUとメモリ間のデータ転送が単一のバスを通じて行われるため、メモリ帯域幅がシステム全体の性能を制限する。

CPUの進化(クロック速度):
1971年: Intel 4004      740 kHz
1985年: Intel 386       16 MHz        (約20倍)
2000年: Pentium 4       1.5 GHz       (約100倍)
2024年: Modern CPU      5+ GHz        (約3倍)

メモリ速度の進化:
1980年: DRAM           ~200 ns
2000年: DDR SDRAM      ~50 ns        (約4倍)
2024年: DDR5           ~14 ns        (約3倍)

結論:CPUはメモリより遥かに速く進化
     → メモリアクセスが最大のボトルネック

8.1.2 メモリ階層の理論

局所性の原理(Principle of Locality)は、メモリ階層設計の基礎である。

時間的局所性(Temporal Locality): 最近アクセスされたデータは、近い将来に再びアクセスされる可能性が高い。

// 時間的局所性の例
int sum = 0;
for (int i = 0; i < 1000; i++) {
    sum += i;  // 'sum' は何度もアクセスされる
}

空間的局所性(Spatial Locality): アクセスされたデータの近くにあるデータは、近い将来にアクセスされる可能性が高い。

// 空間的局所性の例
int arr[1000];
for (int i = 0; i < 1000; i++) {
    arr[i] = i;  // 連続したメモリ位置にアクセス
}

メモリ階層ピラミッド

block-beta
columns 1
  Reg["Registers (<1ns)"]
  L1["L1 Cache (1-2ns)"]
  L2["L2 Cache (3-10ns)"]
  L3["L3 Cache (10-40ns)"]
  Mem["Main Memory (50-100ns)"]
  SSD["SSD (10-100us)"]
  HDD["HDD (ms)"]
  style Reg fill:#f9f
  style L1 fill:#ff9
  style L2 fill:#ff9
  style L3 fill:#ff9
  style Mem fill:#9f9
  style SSD fill:#99f
  style HDD fill:#99f

8.1.3 キャッシュの動作原理

キャッシュは、CPUとメインメモリの速度差を埋める重要な役割を果たす。

キャッシュライン(Cache Line):

典型的なキャッシュライン = 64バイト

メモリアドレスの分解:
mermaid packet-beta title Cache Line Address Decomposition 0-10: "Tag" 11-20: "Index" 21-31: "Offset"

例:64バイトラインの場合
- Offset: 6ビット(64 = 2^6)
- 1バイトをアクセス → 64バイト全体がロード

キャッシュの構造

1. 直接マップ(Direct Mapped)
   - 各メモリブロックは1箇所にしかマッピングできない
   - 単純だが衝突しやすい

2. 全連想(Fully Associative)
   - 任意の位置にマッピング可能
   - 最も柔軟だがハードウェアが複雑

3. セット連想(Set Associative)
   - N-way: N箇所のいずれかにマッピング
   - 現代のCPUは8-way〜16-wayが一般的

キャッシュミスの種類

1. 強制ミス(Compulsory Miss)
   - 初回アクセス時に必ず発生
   - 対策:プリフェッチ

2. 容量ミス(Capacity Miss)
   - キャッシュが小さすぎる
   - 対策:データ分割、ブロッキング

3. 競合ミス(Conflict Miss)
   - 同じキャッシュセットへのマッピング衝突
   - 対策:データ配置の調整

8.1.4 現代CPUのマイクロアーキテクチャ

パイプライン処理(Pipelining):

命令の実行を複数のステージに分割し、並列処理を実現する。

クラシック5段パイプライン(MIPS):

mermaid gantt title 5-Stage Pipeline dateFormat s axisFormat %s section Instr 1 IF :0, 1 ID :1, 1 EX :2, 1 MEM :3, 1 WB :4, 1 section Instr 2 IF :1, 1 ID :2, 1 EX :3, 1 MEM :4, 1 WB :5, 1 section Instr 3 IF :2, 1 ID :3, 1 EX :4, 1 MEM :5, 1 WB :6, 1 section Instr 4 IF :3, 1 ID :4, 1 EX :5, 1 MEM :6, 1 WB :7, 1

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

理想的にはN命令がN+4サイクルで完了(スループット向上)

パイプラインハザード

1. 構造ハザード(Structural Hazard)
   - 複数の命令が同じハードウェア資源を要求
   - 対策:ハードウェアの複製

2. データハザード(Data Hazard)
   - 命令間のデータ依存関係
   - 対策:フォワーディング、ストール

   例:
   ADD R1, R2, R3    ; R1 = R2 + R3
   SUB R4, R1, R5    ; R1 を使用(まだ書き込まれていない!)

3. 制御ハザード(Control Hazard)
   - 分岐命令による実行フローの不確定性
   - 対策:分岐予測

分岐予測(Branch Prediction):

分岐予測器の進化:

1. 静的予測
   - 常に taken または not taken と予測
   - 予測精度: 約50-70%

2. 2ビット飽和カウンタ
   - 履歴に基づいて予測
   - 状態: Strong Not Taken → Weak Not Taken →
           Weak Taken → Strong Taken
   - 予測精度: 約85-90%

3. 相関予測器(Two-Level Predictor)
   - 他の分岐の結果を考慮
   - 予測精度: 約95%

4. ニューラル分岐予測器
   - 機械学習ベース
   - 予測精度: 約99%

誤予測のペナルティ:10-20サイクル

スーパースカラーとアウトオブオーダー実行

スーパースカラー:
- 1サイクルで複数の命令を発行
- 現代のCPUは4-8命令/サイクル

アウトオブオーダー実行:
- 依存関係のない命令を先に実行
- 命令ウィンドウ(ROB)で順序を追跡

例:
1: LOAD R1, [mem]    ; メモリアクセス(遅い)
2: ADD R2, R3, R4    ; R1に依存しない → 先に実行可能
3: SUB R5, R1, R6    ; R1に依存 → LOADを待つ

実行順序: 2 → 1 → 3 (プログラム順序: 1 → 2 → 3)

8.2 漸近的計算量と実際の性能

8.2.1 Big-O表記の限界

アルゴリズムの計算量(Big-O)は重要だが、実際の性能を完全には予測できない。

定数因子の影響

// 両方とも O(n) だが、実行時間は大きく異なる

// 実装A:単純なループ
size_t strlen_naive(const char *s)
{
    size_t len = 0;
    while (s[len])
        len++;
    return len;
}

// 実装B:8バイトずつ処理
size_t strlen_word(const char *s)
{
    // アラインメント調整 + 8バイト処理
    // 約5-10倍高速
}

// Big-Oは同じ O(n) でも、
// 実際の時間は T_A(n) = c₁ × n
//              T_B(n) = c₂ × n  (c₂ << c₁)

メモリアクセスパターンの影響

// 両方とも O(n×n) だが、性能は大きく異なる

// 行優先アクセス(良い)
for (int i = 0; i < N; i++)
    for (int j = 0; j < N; j++)
        sum += matrix[i][j];  // 連続したメモリアクセス

// 列優先アクセス(悪い)
for (int j = 0; j < N; j++)
    for (int i = 0; i < N; i++)
        sum += matrix[i][j];  // 不連続なメモリアクセス

// N = 1000 の場合:
// 行優先: キャッシュヒット率 ~99%
// 列優先: キャッシュヒット率 ~10%(100倍以上遅い可能性)

8.2.2 キャッシュ効率の数学

キャッシュミス率と実行時間

実行時間 = ヒットサイクル + ミスペナルティ × ミス率

例:
- L1ヒット: 4サイクル
- L1ミス、L2ヒット: 12サイクル
- L2ミス: 100サイクル

アルゴリズムA: ミス率 1%
  平均サイクル = 4 + 0.01 × 100 = 5サイクル

アルゴリズムB: ミス率 10%
  平均サイクル = 4 + 0.10 × 100 = 14サイクル

→ ミス率10%で約3倍遅い

ワーキングセットサイズ

ワーキングセット = プログラムが頻繁にアクセスするデータの集合

L1に収まる(<32KB): 最速
L2に収まる(<256KB): 高速
L3に収まる(<8MB): 中速
メモリのみ: 低速

libft関数のワーキングセット:
- ft_isalpha: 1バイト(常にL1ヒット)
- ft_strlen(短い文字列): <1KB(L1ヒット)
- ft_memcpy(大きなデータ): サイズ依存

8.2.3 Rooflineモデル

Rooflineモデルは、性能のボトルネックを視覚的に理解するためのツールである。

性能 (FLOPS)
    │
    │           ┌──────────── Compute Bound
    │          ╱
    │         ╱
    │        ╱
    │       ╱
    │      ╱
    │     ╱
    │    ╱
    │   ╱
    │  ╱  Memory Bound
    │ ╱
    └─────────────────────────────────────
              算術強度 (FLOPS/Byte)

算術強度 = 計算量 / メモリ転送量

libft関数の分類

メモリバウンド(低い算術強度):
- ft_memcpy: 算術強度 ≈ 0(単純なコピー)
- ft_memset: 算術強度 ≈ 0(単純な書き込み)
- ft_strlen: 算術強度 ≈ 0.1(比較のみ)

計算バウンド(高い算術強度):
- ft_atoi: 算術強度 > 1(多くの算術演算)
- ft_itoa: 算術強度 > 1(除算と変換)

ほとんどのlibft関数はメモリバウンド
→ メモリ帯域幅の最大化が重要

8.3 SIMDとベクトル化の理論

8.3.1 SIMD(Single Instruction, Multiple Data)

SIMDは、単一の命令で複数のデータを並列処理する技術である。

フリンの分類(Flynn's Taxonomy):

| | Single Data | Multiple Data | |:-|:------------|:--------------| | Single Instruction | SISD(従来のスカラ処理) | SIMD(ベクトル処理) | | Multiple Instruction | MISD(稀なアーキテクチャ) | MIMD(マルチコア/マルチプロセッサ) |

x86 SIMD拡張の歴史

1996年: MMX        64ビットレジスタ(整数のみ)
1999年: SSE       128ビットレジスタ(浮動小数点追加)
2001年: SSE2      128ビット整数演算強化
2004年: SSE3      水平演算追加
2006年: SSSE3     バイトシャッフル追加
2007年: SSE4      文字列処理命令追加
2011年: AVX       256ビットレジスタ
2013年: AVX2      256ビット整数演算
2017年: AVX-512   512ビットレジスタ

SIMDの動作原理

スカラー演算 vs SIMD演算

スカラー(1つずつ処理):
A[0] + B[0] → C[0]
A[1] + B[1] → C[1]
A[2] + B[2] → C[2]
A[3] + B[3] → C[3]
= 4命令

SIMD(並列処理):
mermaid block-beta columns 4 A0["A[0]"] A1["A[1]"] A2["A[2]"] A3["A[3]"] space:4 op["+ (Single Instruction)"] space:4 B0["B[0]"] B1["B[1]"] B2["B[2]"] B3["B[3]"] space:4 eq["="] space:4 C0["C[0]"] C1["C[1]"] C2["C[2]"] C3["C[3]"] style op fill:#f9f
*1命令で4つのデータを同時処理(理論上4倍高速)*

8.3.2 ベクトル化の条件

コンパイラが自動ベクトル化を行うには、いくつかの条件が必要である。

ベクトル化可能なループの条件

// ベクトル化可能
for (int i = 0; i < n; i++)
    a[i] = b[i] + c[i];  // 各反復が独立

// ベクトル化困難(ループ運搬依存性)
for (int i = 1; i < n; i++)
    a[i] = a[i-1] + b[i];  // 前の反復結果に依存

// ベクトル化困難(関数呼び出し)
for (int i = 0; i < n; i++)
    a[i] = unknown_function(b[i]);  // 副作用の可能性

// ベクトル化困難(条件分岐)
for (int i = 0; i < n; i++)
    if (b[i] > 0)
        a[i] = b[i];  // 分岐あり(マスク処理で対応可能)

手動ベクトル化 vs コンパイラ自動ベクトル化

// コンパイラへのヒント
#pragma GCC ivdep  // 依存関係がないことを保証
for (int i = 0; i < n; i++)
    a[i] = b[i] + c[i];

// 手動ベクトル化(SSE2)
#include <emmintrin.h>

void add_sse2(float *a, float *b, float *c, int n)
{
    int i;
    for (i = 0; i + 4 <= n; i += 4)
    {
        __m128 va = _mm_loadu_ps(&a[i]);
        __m128 vb = _mm_loadu_ps(&b[i]);
        __m128 vc = _mm_add_ps(va, vb);
        _mm_storeu_ps(&c[i], vc);
    }
    // 残りをスカラー処理
    for (; i < n; i++)
        c[i] = a[i] + b[i];
}

8.3.3 null終端文字列のSIMD処理

文字列操作はSIMDの典型的な応用である。

SIMDによるnull検出の原理

/*
 * 問題: 16バイトの中からnull(0x00)を見つける
 *
 * SSE2命令を使用:
 * 1. _mm_load_si128: 16バイトをロード
 * 2. _mm_cmpeq_epi8: 各バイトを0と比較
 * 3. _mm_movemask_epi8: 比較結果をビットマスクに変換
 *
 * 例:
 * データ: "Hello World\0abc"
 *         H e l l o   W o r l d \0 a  b  c  ?
 *         0 0 0 0 0 0 0 0 0 0 0 1  0  0  0  0  ← 比較結果
 * マスク: 0b0000100000000000 = 0x0800
 * __builtin_ctz(0x0800) = 11  ← nullの位置
 */

8.4 性能測定の科学

8.4.1 時間測定の理論

時間測定の階層

1. wall-clock time(実時間)
   - 実世界での経過時間
   - OSのスケジューリングを含む

2. CPU time
   - プロセスがCPUを使用した時間
   - user time + system time

3. cycle count
   - CPUサイクル数
   - 最も詳細な測定

測定の不確実性

測定誤差の要因:
1. タイマーの解像度(通常 1-100 ns)
2. 割り込みによる遅延
3. コンテキストスイッチ
4. キャッシュの状態
5. 周波数スケーリング(Turbo Boost等)
6. 熱スロットリング

対策:
- 複数回測定して統計処理
- コールドキャッシュ/ウォームキャッシュの区別
- 周波数を固定(ベンチマーク時)
- 外れ値の除去

8.4.2 統計的ベンチマーク

信頼性のある測定のための統計手法

// 統計的に信頼性のある測定
typedef struct s_measurement {
    double mean;        // 平均
    double std_dev;     // 標準偏差
    double median;      // 中央値
    double min;         // 最小値
    double max;         // 最大値
    double p95;         // 95パーセンタイル
    int    n_samples;   // サンプル数
}   t_measurement;

t_measurement measure_function(void (*func)(void), int iterations)
{
    double *samples = malloc(iterations * sizeof(double));
    t_measurement result;

    // ウォームアップ(キャッシュを温める)
    for (int i = 0; i < 100; i++)
        func();

    // 測定
    for (int i = 0; i < iterations; i++)
    {
        struct timespec start, end;
        clock_gettime(CLOCK_MONOTONIC, &start);
        func();
        clock_gettime(CLOCK_MONOTONIC, &end);

        samples[i] = (end.tv_sec - start.tv_sec) * 1e9 +
                     (end.tv_nsec - start.tv_nsec);
    }

    // ソート(中央値とパーセンタイル用)
    qsort(samples, iterations, sizeof(double), compare_double);

    // 統計計算
    result.min = samples[0];
    result.max = samples[iterations - 1];
    result.median = samples[iterations / 2];
    result.p95 = samples[(int)(iterations * 0.95)];

    // 平均と標準偏差
    double sum = 0, sq_sum = 0;
    for (int i = 0; i < iterations; i++)
        sum += samples[i];
    result.mean = sum / iterations;

    for (int i = 0; i < iterations; i++)
        sq_sum += (samples[i] - result.mean) * (samples[i] - result.mean);
    result.std_dev = sqrt(sq_sum / iterations);

    result.n_samples = iterations;

    free(samples);
    return result;
}

8.4.3 ハードウェア性能カウンタ

現代のCPUには、詳細な性能分析のためのカウンタが内蔵されている。

主要な性能カウンタ

1. サイクル数(Cycles)
   - 消費されたCPUサイクル

2. 命令数(Instructions)
   - 実行された命令数
   - IPC = Instructions / Cycles

3. キャッシュミス
   - L1, L2, L3 各レベルのミス

4. 分岐ミス予測
   - 予測を外した分岐の数

5. TLBミス
   - ページテーブル参照が必要になった回数

Linux perfを使った測定

# 基本的な統計
perf stat ./test_program

# 詳細なカウンタ
perf stat -e cycles,instructions,cache-misses,branch-misses ./test_program

# 典型的な出力:
#  1,234,567,890  cycles
#  2,345,678,901  instructions     # 1.90 IPC
#        123,456  cache-misses
#         12,345  branch-misses    # 0.05%

8.5 libft関数の最適化実装

8.5.1 ft_strlen の最適化

段階的な最適化アプローチ

// レベル0:ナイーブ実装
size_t ft_strlen_v0(const char *s)
{
    size_t len = 0;
    while (s[len])
        len++;
    return len;
}

// レベル1:ポインタ演算
size_t ft_strlen_v1(const char *s)
{
    const char *ptr = s;
    while (*ptr)
        ptr++;
    return ptr - s;
}

// レベル2:ループ展開
size_t ft_strlen_v2(const char *s)
{
    const char *ptr = s;

    // 4バイトずつチェック
    while (ptr[0] && ptr[1] && ptr[2] && ptr[3])
        ptr += 4;

    // 残りを処理
    while (*ptr)
        ptr++;
    return ptr - s;
}

// レベル3:ワード単位処理
size_t ft_strlen_v3(const char *s)
{
    const char *ptr = s;

    // アラインメント調整
    while ((uintptr_t)ptr & 7)
    {
        if (!*ptr)
            return ptr - s;
        ptr++;
    }

    // 8バイト単位で処理
    const uint64_t *word = (const uint64_t *)ptr;

    /*
     * ゼロバイト検出の原理:
     *
     * 0x0101010101010101 を減算すると、
     * 各バイトが 0x00 の場合のみ最上位ビットが立つ。
     *
     * ~word で反転後、0x8080808080808080 とAND。
     * 結果が非ゼロなら、どこかにnullがある。
     */
    const uint64_t lo = 0x0101010101010101ULL;
    const uint64_t hi = 0x8080808080808080ULL;

    while (1)
    {
        uint64_t w = *word;
        if (((w - lo) & ~w & hi) != 0)
        {
            // nullバイトを含むワード
            ptr = (const char *)word;
            while (*ptr)
                ptr++;
            return ptr - s;
        }
        word++;
    }
}

// レベル4:SIMD (SSE2)
#ifdef __SSE2__
#include <emmintrin.h>

size_t ft_strlen_sse2(const char *s)
{
    const char *ptr = s;

    // 16バイトアラインメントまで
    while ((uintptr_t)ptr & 15)
    {
        if (!*ptr)
            return ptr - s;
        ptr++;
    }

    const __m128i zero = _mm_setzero_si128();
    const __m128i *block = (const __m128i *)ptr;

    while (1)
    {
        // 16バイトをロードしてゼロと比較
        __m128i data = _mm_load_si128(block);
        __m128i cmp = _mm_cmpeq_epi8(data, zero);
        int mask = _mm_movemask_epi8(cmp);

        if (mask != 0)
        {
            // nullの位置を特定
            int pos = __builtin_ctz(mask);
            return (const char *)block - s + pos;
        }
        block++;
    }
}
#endif

// レベル5:AVX2
#ifdef __AVX2__
#include <immintrin.h>

size_t ft_strlen_avx2(const char *s)
{
    const char *ptr = s;

    // 32バイトアラインメントまで
    while ((uintptr_t)ptr & 31)
    {
        if (!*ptr)
            return ptr - s;
        ptr++;
    }

    const __m256i zero = _mm256_setzero_si256();
    const __m256i *block = (const __m256i *)ptr;

    while (1)
    {
        __m256i data = _mm256_load_si256(block);
        __m256i cmp = _mm256_cmpeq_epi8(data, zero);
        int mask = _mm256_movemask_epi8(cmp);

        if (mask != 0)
        {
            int pos = __builtin_ctz(mask);
            return (const char *)block - s + pos;
        }
        block++;
    }
}
#endif

8.5.2 ft_memcpy の最適化

// メモリコピーの最適化戦略

// 小さなコピー: インライン展開
// 中サイズ: ワード単位コピー
// 大サイズ: SIMD + プリフェッチ
// 巨大サイズ: 非テンポラルストア

void *ft_memcpy_optimized(void *restrict dst, const void *restrict src, size_t n)
{
    unsigned char *d = dst;
    const unsigned char *s = src;

    // 小さなコピー(<= 16バイト): バイト単位
    if (n <= 16)
    {
        while (n--)
            *d++ = *s++;
        return dst;
    }

    // 中サイズ(<= 128バイト): 8バイト単位
    if (n <= 128)
    {
        // 先頭をアラインメント調整
        size_t align = (8 - ((uintptr_t)d & 7)) & 7;
        n -= align;
        while (align--)
            *d++ = *s++;

        // 8バイトずつコピー
        uint64_t *d64 = (uint64_t *)d;
        const uint64_t *s64 = (const uint64_t *)s;

        while (n >= 8)
        {
            *d64++ = *s64++;
            n -= 8;
        }

        d = (unsigned char *)d64;
        s = (const unsigned char *)s64;

        while (n--)
            *d++ = *s++;

        return dst;
    }

#ifdef __AVX2__
    // 大サイズ: AVX2 (256ビット = 32バイト)
    if (n <= 1024 * 1024)
    {
        // アラインメント調整
        size_t align = (32 - ((uintptr_t)d & 31)) & 31;
        n -= align;
        while (align--)
            *d++ = *s++;

        __m256i *d256 = (__m256i *)d;
        const __m256i *s256 = (const __m256i *)s;

        while (n >= 128)
        {
            // プリフェッチ
            _mm_prefetch((const char *)s256 + 256, _MM_HINT_T0);

            // 4 × 32バイト = 128バイトずつ
            __m256i v0 = _mm256_loadu_si256(s256);
            __m256i v1 = _mm256_loadu_si256(s256 + 1);
            __m256i v2 = _mm256_loadu_si256(s256 + 2);
            __m256i v3 = _mm256_loadu_si256(s256 + 3);

            _mm256_storeu_si256(d256, v0);
            _mm256_storeu_si256(d256 + 1, v1);
            _mm256_storeu_si256(d256 + 2, v2);
            _mm256_storeu_si256(d256 + 3, v3);

            s256 += 4;
            d256 += 4;
            n -= 128;
        }

        d = (unsigned char *)d256;
        s = (const unsigned char *)s256;

        // 残りを処理
        while (n--)
            *d++ = *s++;

        return dst;
    }

    // 巨大サイズ: 非テンポラルストア(キャッシュをバイパス)
    {
        size_t align = (32 - ((uintptr_t)d & 31)) & 31;
        n -= align;
        while (align--)
            *d++ = *s++;

        __m256i *d256 = (__m256i *)d;
        const __m256i *s256 = (const __m256i *)s;

        while (n >= 128)
        {
            _mm_prefetch((const char *)s256 + 512, _MM_HINT_NTA);

            __m256i v0 = _mm256_loadu_si256(s256);
            __m256i v1 = _mm256_loadu_si256(s256 + 1);
            __m256i v2 = _mm256_loadu_si256(s256 + 2);
            __m256i v3 = _mm256_loadu_si256(s256 + 3);

            // 非テンポラルストア
            _mm256_stream_si256(d256, v0);
            _mm256_stream_si256(d256 + 1, v1);
            _mm256_stream_si256(d256 + 2, v2);
            _mm256_stream_si256(d256 + 3, v3);

            s256 += 4;
            d256 += 4;
            n -= 128;
        }

        _mm_sfence();  // ストアフェンス

        d = (unsigned char *)d256;
        s = (const unsigned char *)s256;

        while (n--)
            *d++ = *s++;

        return dst;
    }
#else
    // フォールバック
    while (n--)
        *d++ = *s++;
    return dst;
#endif
}

8.5.3 ft_memset の最適化

void *ft_memset_optimized(void *s, int c, size_t n)
{
    unsigned char *ptr = s;
    unsigned char val = c;

    // 小さなサイズ
    if (n <= 16)
    {
        while (n--)
            *ptr++ = val;
        return s;
    }

    // パターンを8バイトに拡張
    uint64_t pattern = val;
    pattern |= pattern << 8;
    pattern |= pattern << 16;
    pattern |= pattern << 32;

    // アラインメント調整
    size_t align = (8 - ((uintptr_t)ptr & 7)) & 7;
    n -= align;
    while (align--)
        *ptr++ = val;

    // 8バイトずつ書き込み
    uint64_t *ptr64 = (uint64_t *)ptr;

#ifdef __AVX2__
    // AVX2で32バイトパターンを作成
    __m256i pattern256 = _mm256_set1_epi8(val);

    if (n >= 256)
    {
        __m256i *ptr256 = (__m256i *)ptr64;

        // 巨大サイズは非テンポラルストア
        if (n >= 1024 * 1024)
        {
            while (n >= 128)
            {
                _mm256_stream_si256(ptr256, pattern256);
                _mm256_stream_si256(ptr256 + 1, pattern256);
                _mm256_stream_si256(ptr256 + 2, pattern256);
                _mm256_stream_si256(ptr256 + 3, pattern256);
                ptr256 += 4;
                n -= 128;
            }
            _mm_sfence();
        }
        else
        {
            while (n >= 128)
            {
                _mm256_storeu_si256(ptr256, pattern256);
                _mm256_storeu_si256(ptr256 + 1, pattern256);
                _mm256_storeu_si256(ptr256 + 2, pattern256);
                _mm256_storeu_si256(ptr256 + 3, pattern256);
                ptr256 += 4;
                n -= 128;
            }
        }

        ptr64 = (uint64_t *)ptr256;
    }
#endif

    // 残りを8バイトずつ
    while (n >= 8)
    {
        *ptr64++ = pattern;
        n -= 8;
    }

    // 最後のバイト
    ptr = (unsigned char *)ptr64;
    while (n--)
        *ptr++ = val;

    return s;
}

8.6 コンパイラ最適化

8.6.1 最適化レベル

# GCC/Clang 最適化オプション
-O0  # 最適化なし(デバッグ用)
-O1  # 基本的な最適化
-O2  # 標準的な最適化(推奨)
-O3  # 積極的な最適化(コードサイズ増加)
-Os  # サイズ最適化
-Ofast # 数値精度を犠牲にした最適化

主要な最適化手法

1. インライン展開(-finline-functions)
   - 関数呼び出しオーバーヘッドを削減
   - コードサイズとのトレードオフ

2. ループ展開(-funroll-loops)
   - ループオーバーヘッドを削減
   - 並列実行の機会を増加

3. デッドコード削除(-fdce)
   - 実行されないコードを削除

4. 共通部分式削除(-fgcse)
   - 同じ計算を一度だけ実行

5. 定数畳み込み(-fconstant-folding)
   - コンパイル時に定数式を評価

6. レジスタ割り当て最適化
   - メモリアクセスを最小化

7. 自動ベクトル化(-ftree-vectorize)
   - SIMDコードの自動生成

8.6.2 分岐予測ヒント

// 分岐予測をコンパイラに伝える

#define likely(x)   __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)

// 使用例
void *ft_memcpy_hinted(void *dst, const void *src, size_t n)
{
    // NULLチェック(通常は起こらない)
    if (unlikely(dst == NULL || src == NULL))
        return NULL;

    // 小さなコピー(よくある)
    if (likely(n <= 16))
    {
        // インラインで高速処理
        unsigned char *d = dst;
        const unsigned char *s = src;
        while (n--)
            *d++ = *s++;
        return dst;
    }

    // 大きなコピー(稀)
    return ft_memcpy_large(dst, src, n);
}

8.6.3 関数属性

// 関数属性による最適化ヒント

// 常にインライン化
__attribute__((always_inline))
static inline int ft_isdigit_fast(int c)
{
    return (unsigned int)(c - '0') < 10u;
}

// ホットパス(頻繁に呼ばれる)
__attribute__((hot))
size_t ft_strlen_hot(const char *s);

// コールドパス(稀に呼ばれる)
__attribute__((cold))
void handle_error(const char *msg);

// 純粋関数(副作用なし、同じ入力に同じ出力)
__attribute__((pure))
size_t ft_strlen_pure(const char *s);

// const関数(メモリを読まない純粋関数)
__attribute__((const))
int ft_isalpha_const(int c);

// noreturn(戻らない関数)
__attribute__((noreturn))
void ft_exit(int status);

8.7 ベンチマークフレームワーク

8.7.1 プロファイラの実装

// profiler.h

#ifndef PROFILER_H
# define PROFILER_H

# include <time.h>
# include <stdint.h>
# include <stdio.h>

// 高精度タイマー
typedef struct s_timer {
    struct timespec start;
    struct timespec end;
    double          elapsed_ns;
}   t_timer;

// タイマー操作
static inline void timer_start(t_timer *t)
{
    clock_gettime(CLOCK_MONOTONIC, &t->start);
}

static inline void timer_stop(t_timer *t)
{
    clock_gettime(CLOCK_MONOTONIC, &t->end);
    t->elapsed_ns = (t->end.tv_sec - t->start.tv_sec) * 1e9 +
                    (t->end.tv_nsec - t->start.tv_nsec);
}

// CPUサイクルカウンタ(x86_64)
#ifdef __x86_64__
static inline uint64_t rdtsc(void)
{
    unsigned int lo, hi;
    __asm__ __volatile__ ("rdtsc" : "=a" (lo), "=d" (hi));
    return ((uint64_t)hi << 32) | lo;
}

static inline uint64_t rdtscp(void)
{
    unsigned int lo, hi, aux;
    __asm__ __volatile__ ("rdtscp" : "=a" (lo), "=d" (hi), "=c" (aux));
    return ((uint64_t)hi << 32) | lo;
}
#endif

// ベンチマーク結果
typedef struct s_bench_result {
    const char *name;
    size_t     data_size;
    int        iterations;
    double     total_time_ns;
    double     avg_time_ns;
    double     min_time_ns;
    double     max_time_ns;
    double     throughput_gbps;  // GB/s
    uint64_t   cycles_per_byte;
}   t_bench_result;

// キャッシュ操作
void flush_cache(void);
void warm_cache(void *data, size_t size);

// ベンチマーク実行
t_bench_result run_benchmark(
    const char *name,
    void (*func)(void *data, size_t size),
    void *data,
    size_t size,
    int iterations
);

// 結果表示
void print_benchmark_results(t_bench_result *results, int count);

#endif

8.7.2 ベンチマーク実行

// benchmark_suite.c

#include "profiler.h"
#include "libft.h"
#include <stdlib.h>
#include <string.h>

// キャッシュをフラッシュ
void flush_cache(void)
{
    // 大きなデータで上書き(32MB)
    const size_t size = 32 * 1024 * 1024;
    volatile char *buffer = malloc(size);
    if (buffer)
    {
        for (size_t i = 0; i < size; i += 64)
            buffer[i] = i & 0xFF;
        free((void *)buffer);
    }
}

// キャッシュを温める
void warm_cache(void *data, size_t size)
{
    volatile char *ptr = data;
    volatile char sum = 0;
    for (size_t i = 0; i < size; i++)
        sum += ptr[i];
    (void)sum;
}

t_bench_result run_benchmark(
    const char *name,
    void (*func)(void *data, size_t size),
    void *data,
    size_t size,
    int iterations)
{
    t_bench_result result;
    t_timer timer;
    double *samples = malloc(iterations * sizeof(double));

    result.name = name;
    result.data_size = size;
    result.iterations = iterations;
    result.min_time_ns = 1e18;
    result.max_time_ns = 0;

    // ウォームアップ
    for (int i = 0; i < 10; i++)
        func(data, size);

    // 測定
    uint64_t total_cycles = 0;

    for (int i = 0; i < iterations; i++)
    {
        // キャッシュの状態を一定に
        if (i % 100 == 0)
        {
            flush_cache();
            warm_cache(data, size);
        }

        uint64_t start_cycles = rdtscp();
        timer_start(&timer);

        func(data, size);

        timer_stop(&timer);
        uint64_t end_cycles = rdtscp();

        samples[i] = timer.elapsed_ns;
        total_cycles += end_cycles - start_cycles;

        if (samples[i] < result.min_time_ns)
            result.min_time_ns = samples[i];
        if (samples[i] > result.max_time_ns)
            result.max_time_ns = samples[i];
    }

    // 統計計算
    double sum = 0;
    for (int i = 0; i < iterations; i++)
        sum += samples[i];

    result.total_time_ns = sum;
    result.avg_time_ns = sum / iterations;
    result.throughput_gbps = (size * iterations) / sum;  // バイト/ns = GB/s
    result.cycles_per_byte = total_cycles / (size * iterations);

    free(samples);
    return result;
}

void print_benchmark_results(t_bench_result *results, int count)
{
    printf("\n%-25s %10s %12s %12s %12s %10s %10s\n",
           "Function", "Size", "Avg(ns)", "Min(ns)", "Max(ns)",
           "GB/s", "Cyc/Byte");
    printf("%s\n", "────────────────────────────────────────────────────────────────────────────────────");

    for (int i = 0; i < count; i++)
    {
        printf("%-25s %10zu %12.2f %12.2f %12.2f %10.2f %10lu\n",
               results[i].name,
               results[i].data_size,
               results[i].avg_time_ns,
               results[i].min_time_ns,
               results[i].max_time_ns,
               results[i].throughput_gbps,
               results[i].cycles_per_byte);
    }
}

8.7.3 包括的なベンチマーク

// run_benchmarks.c

#include "profiler.h"
#include "libft.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// strlen ベンチマーク用のラッパー
static void bench_strlen_naive(void *data, size_t size)
{
    (void)size;
    volatile size_t len = ft_strlen((const char *)data);
    (void)len;
}

static void bench_strlen_libc(void *data, size_t size)
{
    (void)size;
    volatile size_t len = strlen((const char *)data);
    (void)len;
}

// memcpy ベンチマーク用のラッパー
typedef struct s_memcpy_data {
    void    *src;
    void    *dst;
    size_t  size;
}   t_memcpy_data;

static void bench_memcpy_ft(void *data, size_t size)
{
    (void)size;
    t_memcpy_data *d = data;
    ft_memcpy(d->dst, d->src, d->size);
}

static void bench_memcpy_libc(void *data, size_t size)
{
    (void)size;
    t_memcpy_data *d = data;
    memcpy(d->dst, d->src, d->size);
}

void run_strlen_benchmark(void)
{
    printf("\n=== strlen Benchmark ===\n");

    size_t sizes[] = {10, 100, 1000, 10000, 100000, 1000000};
    int n_sizes = sizeof(sizes) / sizeof(sizes[0]);

    for (int i = 0; i < n_sizes; i++)
    {
        size_t size = sizes[i];

        // テストデータ準備
        char *str = malloc(size + 1);
        memset(str, 'A', size);
        str[size] = '\0';

        printf("\nSize: %zu bytes\n", size);

        t_bench_result results[2];
        results[0] = run_benchmark("ft_strlen", bench_strlen_naive,
                                   str, size, 1000);
        results[1] = run_benchmark("strlen (libc)", bench_strlen_libc,
                                   str, size, 1000);

        print_benchmark_results(results, 2);

        // スピードアップ計算
        double speedup = results[1].avg_time_ns / results[0].avg_time_ns;
        printf("ft_strlen is %.2fx %s than libc\n",
               speedup > 1 ? speedup : 1/speedup,
               speedup > 1 ? "faster" : "slower");

        free(str);
    }
}

void run_memcpy_benchmark(void)
{
    printf("\n=== memcpy Benchmark ===\n");

    size_t sizes[] = {64, 256, 1024, 4096, 16384, 65536, 262144, 1048576, 16777216};
    int n_sizes = sizeof(sizes) / sizeof(sizes[0]);

    for (int i = 0; i < n_sizes; i++)
    {
        size_t size = sizes[i];

        t_memcpy_data data;
        data.src = aligned_alloc(64, size);
        data.dst = aligned_alloc(64, size);
        data.size = size;

        // ランダムデータで初期化
        for (size_t j = 0; j < size; j++)
            ((char *)data.src)[j] = rand() & 0xFF;

        printf("\nSize: ");
        if (size < 1024)
            printf("%zu B\n", size);
        else if (size < 1024 * 1024)
            printf("%zu KB\n", size / 1024);
        else
            printf("%zu MB\n", size / (1024 * 1024));

        t_bench_result results[2];

        int iters = size < 100000 ? 10000 : (size < 10000000 ? 1000 : 100);

        results[0] = run_benchmark("ft_memcpy", bench_memcpy_ft,
                                   &data, size, iters);
        results[1] = run_benchmark("memcpy (libc)", bench_memcpy_libc,
                                   &data, size, iters);

        print_benchmark_results(results, 2);

        free(data.src);
        free(data.dst);
    }
}

void run_alignment_test(void)
{
    printf("\n=== Alignment Impact Test ===\n");

    const size_t size = 4096;
    char *buffer = aligned_alloc(64, size + 64);

    printf("\n%-10s %12s %12s\n", "Offset", "Time(ns)", "Throughput");
    printf("──────────────────────────────────────\n");

    for (int offset = 0; offset < 64; offset += 8)
    {
        char *ptr = buffer + offset;
        memset(ptr, 'A', size);
        ptr[size - 1] = '\0';

        t_bench_result result = run_benchmark("strlen", bench_strlen_naive,
                                              ptr, size, 10000);

        printf("%-10d %12.2f %12.2f GB/s\n",
               offset, result.avg_time_ns, result.throughput_gbps);
    }

    free(buffer);
}

void run_cache_test(void)
{
    printf("\n=== Cache Effects Test ===\n");

    // L1, L2, L3 キャッシュ境界をまたぐサイズ
    size_t sizes[] = {
        16 * 1024,       // L1に収まる
        32 * 1024,       // L1境界
        64 * 1024,       // L1を超える
        256 * 1024,      // L2境界
        1024 * 1024,     // L2を超える
        8 * 1024 * 1024, // L3境界
        32 * 1024 * 1024 // L3を超える
    };
    const char *labels[] = {
        "16 KB (L1)", "32 KB (L1 boundary)", "64 KB (> L1)",
        "256 KB (L2 boundary)", "1 MB (> L2)",
        "8 MB (L3 boundary)", "32 MB (> L3)"
    };

    printf("\n%-25s %12s %12s\n", "Size", "Time(ns)", "Throughput");
    printf("────────────────────────────────────────────────────\n");

    for (int i = 0; i < 7; i++)
    {
        char *data = malloc(sizes[i] + 1);
        memset(data, 'A', sizes[i]);
        data[sizes[i]] = '\0';

        int iters = sizes[i] < 1000000 ? 1000 : 100;

        // キャッシュをフラッシュしてからテスト(コールドキャッシュ)
        flush_cache();

        t_bench_result result = run_benchmark("strlen", bench_strlen_naive,
                                              data, sizes[i], iters);

        printf("%-25s %12.2f %12.2f GB/s\n",
               labels[i], result.avg_time_ns, result.throughput_gbps);

        free(data);
    }
}

int main(void)
{
    printf("╔════════════════════════════════════════════════════╗\n");
    printf("║         LIBFT PERFORMANCE BENCHMARK                ║\n");
    printf("╚════════════════════════════════════════════════════╝\n");

    run_strlen_benchmark();
    run_memcpy_benchmark();
    run_alignment_test();
    run_cache_test();

    printf("\nBenchmark complete.\n");
    return 0;
}

8.8 実践的な最適化ガイドライン

8.8.1 最適化のチェックリスト

【アルゴリズムレベル】
□ より効率的なアルゴリズムを選択したか?
□ 不要な処理を削除したか?
□ 早期終了条件を活用しているか?

【データ構造】
□ キャッシュフレンドリーなレイアウトか?
□ データのアラインメントは適切か?
□ パディングは最小化されているか?

【メモリアクセス】
□ 連続的なアクセスパターンか?
□ キャッシュラインを意識しているか?
□ プリフェッチは適切か?
□ 大きなコピーに非テンポラルストアを使用しているか?

【CPU最適化】
□ SIMD命令を活用しているか?
□ 分岐を最小化しているか?
□ 分岐予測ヒントを使用しているか?
□ ループ展開は適切か?

【コンパイラ】
□ 適切な最適化フラグを使用しているか?
□ インライン化ヒントは適切か?
□ リンク時最適化(LTO)を検討したか?

【測定】
□ 正確なベンチマークを実施したか?
□ 複数のデータサイズでテストしたか?
□ キャッシュの状態を考慮しているか?
□ 標準ライブラリと比較したか?

8.8.2 最適化の落とし穴

/*
 * 避けるべき最適化パターン
 */

// 1. 早すぎる最適化
// BAD: 測定前に最適化
for (int i = 0; i < n; i += 4) { ... }  // 本当に必要?

// GOOD: まず測定、問題があれば最適化
for (int i = 0; i < n; i++) { ... }

// 2. 可読性の犠牲
// BAD: 理解困難なビット演算
x = (x | (x >> 1)) | ((x | (x >> 1)) >> 2);

// GOOD: コメント付きで理解可能に
// 最も近い2のべき乗に切り上げ
x = next_power_of_two(x);

// 3. プラットフォーム依存の過信
#ifdef __AVX512__
// AVX-512を使用
#endif
// AVX-512は熱問題で周波数が下がることがある

// 4. ベンチマークの誤り
// BAD: 1回だけ測定
start = clock();
func();
end = clock();

// GOOD: 統計的に有意な測定
for (int i = 0; i < 1000; i++) { ... }

// 5. 最適化の過剰適用
// BAD: すべての関数にSIMD
__m128i ft_isalpha_sse(int c);  // 1文字に対してSIMD?

// GOOD: 効果がある場所に集中
size_t ft_strlen_sse(const char *s);  // 長い文字列には効果的

まとめ

この章では、コンピュータアーキテクチャの基礎から実践的な最適化技法まで、包括的に学んだ。

学んだ概念

  • 計算機アーキテクチャの基礎
- フォン・ノイマン・アーキテクチャとそのボトルネック - メモリ階層と局所性の原理 - キャッシュの動作原理

  • CPUマイクロアーキテクチャ
- パイプライン処理 - 分岐予測 - スーパースカラーとアウトオブオーダー実行

  • 性能分析
- 漸近的計算量と実際の性能の関係 - キャッシュ効率の数学 - Rooflineモデル

  • SIMD技術
- ベクトル化の理論と条件 - SSE/AVX命令セット - 文字列処理のSIMD化

  • 実践的最適化
- ft_strlen、ft_memcpy、ft_memsetの最適化実装 - コンパイラ最適化とヒント - ベンチマークフレームワーク

重要な教訓

  • 最適化は測定から始める
  • アルゴリズムの選択が最も重要
  • メモリアクセスパターンが性能を決定する
  • 可読性と性能のバランスを取る
  • プラットフォーム固有の最適化は条件付きで