課題23: ベンチマーク駆動最適化

マンダトリー要件

問題1: パフォーマンス分析の理解(15点)

  • プロファイリングツール(5点)
- flamegraph, perf, valgrindの違いを説明しなさい。 - それぞれが適している使用場面を述べなさい。

  • 最適化の原則(5点)
- "Premature optimization is the root of all evil"の意味を説明しなさい。 - 正しい最適化のプロセス(測定→最適化→検証)を説明しなさい。

  • ベンチマークの注意点(5点)
- black_boxが必要な理由を説明しなさい。 - 正確なベンチマークを行うための3つのポイントを挙げなさい。

問題2: 実装とベンチマーク(40点)

以下のアルゴリズムを複数の方法で実装し、ベンチマークで比較しなさい。

2.1 文字列検索(15点)

// 実装1: 素朴な実装
fn find_naive(text: &str, pattern: &str) -> Vec<usize> {
    // 実装
}

// 実装2: 最適化版
fn find_optimized(text: &str, pattern: &str) -> Vec<usize> {
    // 実装
}

// 実装3: Boyer-Moore法(ボーナス)
fn find_boyer_moore(text: &str, pattern: &str) -> Vec<usize> {
    // 実装
}

ベンチマーク:

fn bench_string_search(c: &mut Criterion) {
    let text = "..."; // 大きなテキスト
    let pattern = "search";

    let mut group = c.benchmark_group("string_search");
    group.bench_function("naive", |b| {
        b.iter(|| find_naive(black_box(text), black_box(pattern)));
    });
    group.bench_function("optimized", |b| {
        b.iter(|| find_optimized(black_box(text), black_box(pattern)));
    });
    group.finish();
}

要件:

  • 最低2つの実装
  • Criterionを使ったベンチマーク
  • 結果の分析レポート

2.2 ソートアルゴリズム(15点)

fn bubble_sort(data: &mut [i32]) {
    // 実装
}

fn quick_sort(data: &mut [i32]) {
    // 実装
}

fn merge_sort(data: &mut [i32]) {
    // 実装
}

パラメータ化ベンチマーク:

fn bench_sort(c: &mut Criterion) {
    let mut group = c.benchmark_group("sort");

    for size in [10, 100, 1000, 10000].iter() {
        let data: Vec<i32> = (0..*size).rev().collect();

        group.bench_with_input(
            BenchmarkId::new("bubble", size),
            size,
            |b, _| {
                let mut data_copy = data.clone();
                b.iter(|| bubble_sort(&mut data_copy));
            },
        );

        // quick_sort, merge_sortも同様
    }

    group.finish();
}

要件:

  • 3つのソートアルゴリズム実装
  • 複数のデータサイズでベンチマーク
  • O記法と実測値の比較

2.3 メモリ最適化(10点)

以下のコードを最適化しなさい:

// 最適化前
fn process_data_before(data: &[String]) -> Vec<String> {
    let mut results = Vec::new();
    for item in data {
        let upper = item.to_uppercase();
        let trimmed = upper.trim().to_string();
        results.push(trimmed);
    }
    results
}

// 最適化後
fn process_data_after(data: &[String]) -> Vec<String> {
    // 改善実装
}

測定項目:

  • 実行時間
  • メモリ使用量(valgrind massif使用)
  • アロケーション回数

問題3: プロファイリング実践(25点)

3.1 フレームグラフ分析(10点)

以下のプログラムをプロファイリングし、ボトルネックを特定しなさい:

fn main() {
    let data = generate_data(10000);
    let result = process_pipeline(&data);
    println!("Result: {}", result);
}

fn generate_data(n: usize) -> Vec<i32> {
    (0..n).map(|i| expensive_calc(i)).collect()
}

fn process_pipeline(data: &[i32]) -> i32 {
    let filtered = filter_data(data);
    let transformed = transform_data(&filtered);
    aggregate_data(&transformed)
}

fn expensive_calc(n: usize) -> i32 {
    // 意図的に重い処理
    (0..n).sum::<usize>() as i32
}

// 他の関数も実装

提出物:

  • flamegraph.svg
  • ボトルネック分析レポート
  • 最適化提案
  • 3.2 最適化実装(15点)

    プロファイリング結果に基づいて最適化を実施しなさい:

  • 並列化
   use rayon::prelude::*;

   fn generate_data_parallel(n: usize) -> Vec<i32> {
       (0..n).into_par_iter()
           .map(|i| expensive_calc(i))
           .collect()
   }
   

  • キャッシュ
   use std::cell::RefCell;
   use std::collections::HashMap;

   thread_local! {
       static CACHE: RefCell<HashMap<usize, i32>> = RefCell::new(HashMap::new());
   }

   fn expensive_calc_cached(n: usize) -> i32 {
       CACHE.with(|cache| {
           *cache.borrow_mut()
               .entry(n)
               .or_insert_with(|| expensive_calc(n))
       })
   }
   

要件:

  • 最低2つの最適化手法
  • ビフォア・アフターのベンチマーク
  • 改善率の計算

---

ボーナス課題

ボーナス1: SIMD最適化(10点)

SIMDを使った最適化を実装しなさい:

#![feature(portable_simd)]
use std::simd::*;

// スカラー版
fn sum_scalar(data: &[f32]) -> f32 {
    data.iter().sum()
}

// SIMD版
fn sum_simd(data: &[f32]) -> f32 {
    let (chunks, remainder) = data.as_chunks::<4>();

    let sum = chunks
        .iter()
        .map(|&chunk| f32x4::from_array(chunk))
        .fold(f32x4::splat(0.0), |acc, x| acc + x);

    sum.reduce_sum() + remainder.iter().sum::<f32>()
}

ベンチマーク要件:

  • 複数のデータサイズ(100, 1000, 10000, 100000)
  • スカラー版との比較
  • 高速化率の測定

ボーナス2: カスタムアロケータ(10点)

カスタムアロケータを実装し、パフォーマンスを比較しなさい:

use std::alloc::{GlobalAlloc, Layout, System};

struct CountingAllocator;

static mut ALLOCATION_COUNT: usize = 0;

unsafe impl GlobalAlloc for CountingAllocator {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        ALLOCATION_COUNT += 1;
        System.alloc(layout)
    }

    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
        System.dealloc(ptr, layout)
    }
}

#[global_allocator]
static ALLOCATOR: CountingAllocator = CountingAllocator;

fn get_allocation_count() -> usize {
    unsafe { ALLOCATION_COUNT }
}

測定項目:

  • アロケーション回数
  • メモリ使用量
  • 実行時間

ボーナス3: マルチスレッド最適化(10点)

マルチスレッドを使った最適化を実装しなさい:

use rayon::prelude::*;
use std::sync::Arc;

// シングルスレッド版
fn process_single(data: &[Vec<i32>]) -> Vec<i32> {
    data.iter()
        .map(|chunk| chunk.iter().sum())
        .collect()
}

// マルチスレッド版
fn process_parallel(data: &[Vec<i32>]) -> Vec<i32> {
    data.par_iter()
        .map(|chunk| chunk.iter().sum())
        .collect()
}

// Work Stealing版
fn process_work_stealing(data: &[Vec<i32>]) -> Vec<i32> {
    use rayon::ThreadPoolBuilder;

    let pool = ThreadPoolBuilder::new()
        .num_threads(4)
        .build()
        .unwrap();

    pool.install(|| {
        data.par_iter()
            .map(|chunk| chunk.iter().sum())
            .collect()
    })
}

要件:

  • スレッド数を変えてベンチマーク(1, 2, 4, 8)
  • スケーラビリティの分析
  • オーバーヘッドの測定
  • ボーナス4: 包括的な最適化レポート(10点)

    実際のアプリケーションを最適化し、詳細なレポートを作成しなさい。

    レポート内容:

  • 初期状態の分析
- プロファイリング結果 - ボトルネックの特定

  • 最適化戦略
- 選択した手法とその理由 - 期待される効果

  • 実装
- コード変更の詳細 - ビフォア・アフターの比較

  • 結果
- ベンチマーク結果 - メモリ使用量 - 改善率

  • 考察
- トレードオフ - さらなる改善の可能性

---

評価基準

マンダトリー部分(80点)

項目 配点 評価ポイント
問題1: 基礎理解 15点 正確な説明
問題2: 実装とベンチマーク 40点 複数の実装と比較
問題3: プロファイリング 25点 分析と最適化

ボーナス部分(20点)

項目 配点 評価ポイント
ボーナス1: SIMD 10点 正しい実装と測定
ボーナス2: カスタムアロケータ 10点 詳細な分析
ボーナス3: マルチスレッド 10点 スケーラビリティ分析
ボーナス4: 包括レポート 10点 詳細で実用的な内容

: ボーナスは最大20点まで加算されます。

---

提出方法

ファイル構成

rust-foundations-23/
├── Cargo.toml
├── src/
│   ├── lib.rs
│   ├── string_search.rs
│   ├── sort.rs
│   └── profiling.rs
├── benches/
│   ├── string_search.rs
│   ├── sort.rs
│   └── optimization.rs
├── flamegraph.svg
├── answers.md              # 問題1の回答
└── optimization_report.md  # 最適化レポート

実行方法

# ベンチマーク実行
cargo bench

# フレームグラフ生成
cargo flamegraph

# メモリプロファイリング
valgrind --tool=massif cargo run --release

---

ヒント

問題2.1のヒント

最適化のアイデア:

  • バイトスライスを使う(&str&[u8]
  • マッチ失敗時のスキップ量を増やす
  • SIMDを使った並列比較
  • 問題3のヒント

    プロファイリング手順:

    # 1. リリースビルドで実行
    cargo build --release
    
    # 2. フレームグラフ生成
    cargo flamegraph --release
    
    # 3. ボトルネック特定
    # フレームグラフで横幅が広い関数を見つける
    
    # 4. 最適化実装
    
    # 5. 再度測定して効果を確認
    cargo bench
    

    ---

    学習の確認

    この課題を通じて、以下を理解できたか確認してください:

  • [ ] プロファイリングツールの使い方
  • [ ] ベンチマークの正しい書き方
  • [ ] メモリ最適化のテクニック
  • [ ] 並列化の効果と注意点
  • [ ] SIMD の基礎
  • [ ] 最適化のトレードオフ

次の章では、これまで学んだすべてを統合した実践プロジェクトに取り組みます。