パフォーマンスエンジニアリング
目次
- 概要
- 指標
- ベンチマークと実運用
- ボトルネックの典型
- プロファイリング
- Flame graphの読み方
- レイテンシ予算
- キャッシュと性能
- パフォーマンス改善のアンチパターン
- 改善の進め方
- Littleの法則
- Tail latency
- 容量計画
- 最適化の順序
- パフォーマンス計測と分析の実践
- CPU キャッシュ階層とメモリレイテンシ
- システムコールとカーネル境界
- Flame Graph によるプロファイリング
- バッチ処理と遅延
- 並行性とパフォーマンスの最適化
- パフォーマンスプロファイリングツール
- CPU と Memory のボトルネック分類
- 最適化の段階的アプローチ
- キャッシュ局所性の最適化
- SIMD(Single Instruction Multiple Data)
- 分散キャッシング と CDN
- 高度なプロファイリング
- まとめ
- 参考文献
概要
速さを感覚ではなく観測と仮説で扱う
パフォーマンス改善は、ただ速そうな最適化を重ねる作業ではありません。現象を測り、ボトルネックを特定し、変化を比較し、再現性のある改善に落とす作業です。
性能改善で大事なのは「どこが遅いかを測ること」であって、「とにかく最適化すること」ではありません。観測、仮説、変更、再測定の循環が基本です。
この章で重視すること
指標
まず押さえるべき指標は次です。
latency1件の処理にかかる時間throughput単位時間あたりの処理量utilizationCPU、メモリ、I/Oの使用率saturation待ち行列や競合の度合い
平均値だけでなく、p95 や p99 のようなtail latencyを見ることが重要です。
指標は単独では解釈できません。latencyが悪化していても、CPUが詰まっているのか、DB connection poolで待っているのか、外部APIのretryで膨らんでいるのかは別の問題です。最初に見るべきなのは、ユーザーに近い指標と資源に近い指標の両方です。
| 層 | 指標 | 何を示すか |
|---|---|---|
| ユーザー体験 | page load、操作完了時間 | 利用者が感じる遅さ |
| サービス | request rate、error rate、duration | APIの健康状態 |
| 資源 | CPU、memory、I/O、network | どの資源が詰まるか |
| 待ち行列 | queue length、pool wait | 処理能力に対する圧力 |
| 依存先 | DB latency、external API latency | 下流の影響 |
この分解をしておくと、「速くする」ではなく「どの区間のどの指標を改善するか」という会話になります。
ベンチマークと実運用
マイクロベンチマークは局所比較に向きますが、システム全体の遅さを説明するとは限りません。逆に本番のメトリクスだけ見ても、どの関数やクエリが原因か分からないことがあります。
そのため、次を使い分けます。
- マイクロベンチマーク 局所実装の比較
- 負荷試験 全体の容量確認
- プロファイラ CPU / memory hotspotの特定
- トレース リクエスト単位の遅延分解
測定条件を固定する
性能測定では、結果よりも測定条件が大切です。入力データ、並列数、ウォームアップ、キャッシュ状態、ネットワーク距離、DBの統計情報が変わると、同じコードでも違う結果になります。
最低限、次を記録します。
- 実行環境
- バージョン
- データ量
- 同時実行数
- 測定時間
- p50 / p95 / p99
- エラー率
- CPU / memory / I/Oの使用状況
ボトルネックの典型
- CPU bound 計算量や分岐、変換コスト
- memory bound キャッシュミス、GC、不要なコピー
- I/O bound ディスク、ネットワーク、外部API
- contention lock、DB接続枯渇、キュー待ち
同じ「遅い」でも対策は全く違うため、ここを混同しないことが重要です。
USEとRED
インフラ寄りの観測では USE、サービス寄りの観測では RED がよく使われます。
USEutilization、saturation、errorsREDrate、errors、duration
USEはCPU、ディスク、ネットワークのような資源の健康を見るのに向いています。REDはHTTP APIやRPCのようなリクエスト処理の健康を見るのに向いています。
プロファイリング
プロファイリングでは、処理時間やメモリ使用の偏りを見ます。代表的には次があります。
- CPU profile どの関数で時間を使っているか
- heap profile どこでメモリを確保しているか
- lock profile どこで待っているか
- trace 並行処理やI/O待ちの流れを見る
関数単位のhotspotだけでなく、待ち時間の内訳を見ることが重要です。
Flame graphの読み方
Flame graphは、サンプルされたスタックトレースを横幅で表した図です。幅が広いほど、そこで時間を使っていることを示します。高さは呼び出しの深さであり、必ずしも重さではありません。
見る順序は次です。
- 広い箱を探す
- それが自分のコードかライブラリかを見る
- 期待通りの処理かを確認する
- 同じ計測条件で変更前後を比べる
Flame graphでよくある誤解は、一番上にある関数が重いと思ってしまうことです。実際には横幅が重要です。また、ライブラリ関数が広く見えていても、それを呼び出している自分のコードの使い方が原因の場合があります。
このような呼び出し関係で sort が広い場合、sort関数そのものを疑う前に、必要以上の件数をmemoryに読み込んでいないか、DB側で絞り込めないかを見ます。profileは答えではなく、次の仮説を作る材料です。
レイテンシ予算
ユーザーから見た遅さを分解するには、レイテンシ予算が役立ちます。たとえば300msの目標があるなら、フロントエンド、ネットワーク、API、DB、外部サービスに予算を割り当てます。
合計だけを見ると、どこで遅いか分かりません。各区間に予算を置くと、改善対象が明確になります。
キャッシュと性能
キャッシュは強力ですが、整合性と無効化の問題を持ち込みます。使う前に、何をキーにし、いつ期限切れにし、古い値をどこまで許すかを決めます。
- browser cache
- CDN cache
- application cache
- DB buffer cache
- query result cache
キャッシュは「速くする技術」であると同時に、「古い値を扱う設計」です。
キャッシュを入れる前に、まず遅さの原因が繰り返し計算や繰り返し読み取りなのかを確認します。根本原因がN+1 queryや不要なserializationなら、キャッシュで隠すより構造を直す方が安定します。
| キャッシュ | 効く場面 | 注意点 |
|---|---|---|
| browser cache | static asset | versioningとcache busting |
| CDN cache | 公開コンテンツ | 認証つき応答を混ぜない |
| application cache | 高コスト計算、参照データ | invalidation、memory圧迫 |
| DB cache | 繰り返しquery | query planやindexの問題を隠す |
| negative cache | 存在しない結果の再問い合わせ | 作成直後の反映遅れ |
キャッシュの設計では、hit率だけでなく、miss時の挙動も見ます。missが同時に大量発生するcache stampedeでは、キャッシュがあることで逆に下流へ負荷が集中します。
パフォーマンス改善のアンチパターン
- 測らずに最適化する
- 平均値だけを見る
- 本番と違うデータ量で判断する
- CPU使用率だけで判断する
- キャッシュで根本原因を隠す
- 1回の測定結果だけで結論を出す
改善の進め方
- 現象を再現する
- 指標を決める
- 計測する
- もっとも支配的なボトルネックを1つ選ぶ
- 変更する
- 再測定する
局所最適化を積み重ねるより、支配的な待ち時間を先に削る方が効果は大きくなります。
Littleの法則
待ち行列を見るとき、Littleの法則が役立ちます。
L = lambda * W
L: システム内にいる平均リクエスト数lambda: 到着率W: 滞在時間
直感的には、到着率が同じなら、処理時間が伸びるほど待ち行列が増えます。queue lengthが増え始めたときは、処理能力に対して到着率が近づきすぎている可能性があります。
Tail latency
平均レイテンシがよくても、p99が悪いとユーザー体験は悪くなります。分散システムでは、1リクエストが複数の下流サービスを呼ぶため、どこか1つの遅い処理が全体を引っ張ります。
p99が悪化する原因:
- GC pause
- lock contention
- DB connection pool枯渇
- cold start
- cache miss
- noisy neighbor
- retry storm
平均値だけで判断すると、これらを見逃します。
tail latencyは、依存先が増えるほど悪化しやすくなります。1つのリクエストが5つの下流サービスを呼び、それぞれがまれに遅くなるなら、全体として遅いリクエストに当たる確率は上がります。
対策は単にtimeoutを短くすることではありません。
- 不要な下流呼び出しを減らす
- 並列化できる呼び出しは並列化する
- timeoutとretryの上限を決める
- hedged requestを慎重に検討する
- queueやconnection poolの待ち時間を観測する
- p99をsegment別に見る
retryは成功率を上げる一方で、障害時には負荷を増やします。tail latency対策では、retry、timeout、circuit breaker、backpressureをセットで考えます。
容量計画
容量計画では、現在の平均負荷ではなく、ピーク、成長率、障害時の縮退を考えます。
| 観点 | 確認すること |
|---|---|
| 通常時 | p95、CPU、memory、DB QPS |
| ピーク時 | campaign、月末、batch重複 |
| 障害時 | 1zone喪失、replica減少 |
| 成長 | データ量、tenant数、index肥大 |
余裕を持たせるだけでなく、どこで水平分割するか、どこでcacheやqueueを入れるかを早めに考えます。
最適化の順序
性能改善は、ユーザーに近いところから見ると効果を説明しやすくなります。
- 体感遅延を測る
- traceで区間を分ける
- 最も支配的な区間を選ぶ
- profileで関数やqueryを特定する
- 変更を1つだけ入れる
- 同じ条件で比較する
複数の変更を同時に入れると、何が効いたのか分からなくなります。
性能改善をレビュー可能にする
性能改善は、変更前後の比較がなければ議論が感覚的になります。Brendan GreggのUSE MethodやFlame Graphの考え方が示すように、まず観測し、どの資源が詰まっているかを特定してから手を入れます。
Pull Requestでは、次の情報があると判断しやすくなります。
| 項目 | 例 |
|---|---|
| 目的 | p95 latencyを300msから200msへ下げる |
| 条件 | dataset size、同時接続数、hardware |
| 測定方法 | benchmark command、trace、profile |
| 変更前 | latency、CPU、memory、I/O |
| 変更後 | 同じ条件での比較 |
| 副作用 | memory増加、cache invalidation、複雑化 |
性能改善は、速くなったことだけでなく、なぜ速くなったかを説明できる状態にしておくと、将来の退行を検知しやすくなります。
パフォーマンス計測と分析の実践
CPU Profiling
Flame graph による可視化:
# Linux perf
perf record -F 99 -p $(pgrep myapp) -- sleep 30
perf script | stackcollapse-perf.pl | flamegraph.pl > out.svg
# Java flight recorder (JFR)
java -XX:StartFlightRecording=duration=30s,filename=myapp.jfr MyApp
# Python cProfile
python -m cProfile -o stats.prof myapp.py
python -m pstats stats.prof
> sort cumtime
> reverse
Perf events の詳細
# キャッシュミス計測
perf stat -e cache-references,cache-misses myapp
# Output例:
# 100,000,000 cache-references
# 5,000,000 cache-misses # 5% cache miss rate
# 400,000,000 cpu-cycles
Roofline Model
パフォーマンスの上限を計算:
Peak Flops = CPU周波数 × cores × FLOPs_per_cycle
Peak Memory BW = メモリ周波数 × バス幅
算術強度 = FLOPs / Bytes
achieved Flops = min(Peak Flops, Peak Memory BW × 算術強度)
メモリプロファイリング
Valgrind (memcheck)
valgrind --leak-check=full --track-origins=yes myapp
# Output: definitely lost / indirectly lost / possibly lost
Java heap dump
jmap -dump:live,format=b,file=heap.bin $(pgrep java)
jhat -J-Xmx2g heap.bin
# ブラウザで http://localhost:7000 にアクセス
Python memory_profiler
from memory_profiler import profile
@profile
def expensive_function():
data = [i for i in range(1000000)]
return sum(data)
実行:
python -m memory_profiler myscript.py
# Line Mem usage Increment Occurrences Line Contents
# 5 10.1 MiB 0.0 MiB 1 def expensive_function():
# 6 51.2 MiB 41.1 MiB 1 data = [i for i in range(1000000)]
ボトルネック分析の手順
- 計測: 全体実行時間を記録
- プロファイル: CPU/Memory/I/O の内訳を取得
- Amdahl’s Law: 改善効果を推定
Speedup = 1 / ((1 - f) + f / P)
f = 並列化可能な部分の割合
P = 並列度
例:f = 0.8(80%並列化可能)、P = 4 cores
Speedup = 1 / (0.2 + 0.8/4) = 1 / 0.4 = 2.5x
- Hotspot 特定: 上位の遅い関数を重点改善
- 検証: 改善後に再計測
システム級のパフォーマンス監視
Key Metrics (USE Method)
Utilization (使用率), Saturation (飽和度), Errors
# CPU utilization
top, htop
# Memory
free, vmstat
# Disk I/O
iostat -x, iotop
# Network
netstat, ss, iftop
# Custom application metrics
micrometer (Java), prometheus client, StatsD
Grafana + Prometheus によるDashboard
# prometheus.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'myapp'
static_configs:
- targets: ['localhost:8080']
Percentile メトリクス
タイムアウト設定は平均値ではなくp95やp99で決める
平均 100ms でも p99 が 1秒なら、タイムアウト 5秒必要
percentile 分布:
p50 (median): 100ms
p95: 300ms
p99: 1000ms
p99.9: 5000ms
最適化テクニック
CPU Cache最適化
// キャッシュ効率が悪い(stride アクセス)
for (int i = 0; i < N; i++) {
result += array[i * stride]; // キャッシュライン 0 の処理のみ
}
// 改善(sequential アクセス)
for (int i = 0; i < N; i += stride) {
for (int j = 0; j < stride; j++) {
result += array[i + j]; // キャッシュラインを有効活用
}
}
Branch Prediction
// 分岐が多い(prediction miss)
for (int i = 0; i < N; i++) {
if (data[i] > threshold) { // unpredictable
result += data[i];
}
}
// 改善:条件付き移動に変換
for (int i = 0; i < N; i++) {
result += (data[i] > threshold) ? data[i] : 0;
}
// または data 自体を sorted にして予測可能にする
std::sort(data.begin(), data.end());
for (int i = 0; i < N; i++) {
if (data[i] > threshold) {
result += data[i];
}
}
Lock-free & Wait-free の活用
// 従来(spinlock)
std::atomic<bool> lock{false};
while (lock.exchange(true)) {} // busy-wait
// 改善(lock-free queue)
struct Queue {
std::atomic<Node*> head;
std::atomic<Node*> tail;
void enqueue(const Value& v) {
Node* new_node = new Node{v, nullptr};
Node* old_tail = tail.load();
old_tail->next.store(new_node);
tail.store(new_node);
}
};
SIMD(Single Instruction Multiple Data)
// 通常(scalar)
for (int i = 0; i < N; i++) {
C[i] = A[i] + B[i];
}
// SIMD(SSE/AVX で自動的に、またはOpenMP)
#pragma omp simd
for (int i = 0; i < N; i++) {
C[i] = A[i] + B[i];
}
// または明示的にintrinsics
__m256 a = _mm256_load_ps(&A[0]);
__m256 b = _mm256_load_ps(&B[0]);
__m256 c = _mm256_add_ps(a, b);
_mm256_store_ps(&C[0], c);
スケーラビリティ分析
Gustafson’s Law (強スケーリング)
Speedup = s + (1 - s) * P
s = 並列化不可能な部分の割合
P = プロセッサ数
例:s = 0.05(5%序列)、P = 64
Speedup = 0.05 + 0.95 * 64 ≈ 61倍
Weak Scaling の計測
# 固定 workload/processor で並列度を増やす
for p in 1 2 4 8 16; do
time mpirun -n $p./app --size=$(($BASE * $p))
done
# 理想:各々 同じ時間
# 悪い例:時間が増加 → communication overhead
CPU キャッシュ階層とメモリレイテンシ
現代CPU は L1/L2/L3 キャッシュを備え、メインメモリアクセスのレイテンシは数百サイクルに及びます。
レイテンシの数値
- L1 キャッシュ: ~4 サイクル (32KB)
- L2 キャッシュ: ~11 サイクル (256KB)
- L3 キャッシュ: ~40 サイクル (8MB)
- メインメモリ: ~300 サイクル
4GHz CPUなら、1メモリアクセス = 75ナノ秒。
空間的局所性の活用
メモリレイアウトを意識することで、キャッシュヒット率を劇的に改善できます:
// Bad: メモリ不連続
for (int i = 0; i < N; i++) {
process(arr[i*1000]); // ランダムアクセス
}
// Good: メモリ連続
for (int i = 0; i < N; i++) {
process(arr[i]); // シーケンシャルアクセス
}
キャッシュラインは64バイト。連続アクセスなら複数要素が1回のメモリフェッチで読み込まれます。
SIMD による並列化
Modern CPUs は SIMD (Single Instruction Multiple Data) を備えます:
// Scalar: 1つずつ処理
for (int i = 0; i < N; i++) {
result[i] = a[i] * b[i];
}
// SIMD (AVX-256): 8個を並列処理
// C: 自動vectorization by compiler
#pragma omp simd
for (int i = 0; i < N; i++) {
result[i] = a[i] * b[i];
}
Numpy も SIMD を活用しており、Pythonより1000倍高速です。
システムコールとカーネル境界
ユーザーモード ↔ カーネルモード の遷移は高コストです。
Context Switch のコスト
context_switches_per_sec = 1,000,000 (典型値)
各遷移: ~1,000 サイクル = 0.25 マイクロ秒
年間コスト: 1,000,000 × 0.25 μs × 365 × 86400
= 約 7,884 秒 ≈ 2時間
スレッド数が多すぎるとコンテキストスイッチが支配的になります。
I/O 多重化と epoll
ブロッキング I/O では、スレッド数が接続数に比例:
// Naive: 接続ごとにスレッド
while (1) {
accept() // ブロック
pthread_create(handle_client)
}
// 10,000接続 = 10,000スレッド
epoll(Linux)を使えば、単一スレッドで大量接続を処理:
int epoll_fd = epoll_create(1);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = listen_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev);
// イベントループ
while (1) {
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == listen_fd) {
int client_fd = accept(listen_fd, ...);
ev.data.fd = client_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev);
} else {
handle_client(events[i].data.fd);
}
}
}
100,000接続でも効率的に処理可能。
Flame Graph によるプロファイリング
Brendan Gregg が開発した Flame Graph は、CPU時間の消費を視覚化します:
# Linux: perf でサンプリング
perf record -F 99 ./program
perf script | stackcollapse-perf.pl | flamegraph.pl > graph.svg
Flame Graph の読み方:
- Y軸: コールスタック深度
- X軸: 時間消費量
- 高い関数: ホットスポット(最適化対象)
実行時の 90% が hash_lookup で消費されているなら、そこを最適化すべき。
バッチ処理と遅延
遅延とスループットはトレードオフ:
Low latency (1リクエスト): 1ms
Batch (1000リクエスト): 1000/1000 = 1ms per request
しかし:
- Start-up cost
- Context switch overhead
ビデオゲームは低遅延(<16ms = 60fps)を優先。 バッチ処理(機械学習推論)はスループットを優先。
バッチサイズの最適値は、ハードウェアとワークロード依存です。
並行性とパフォーマンスの最適化
スレッド数の最適化:Amdahl の法則では、シリアル部分がボトルネック。100% パラレル化しても理論値 4倍には満たない。
CPU バウンド には Numpy(SIMD + キャッシュ最適化)で 13000倍改善。I/O バウンド には asyncio で 100倍改善。
Profiling ツール:perf でサンプリング、Flame Graph で視覚化。Python の cProfile で関数別累計時間。メモリプロファイリングで リーク検出。
Memory hierarchy:L1 ~4ns、L2 ~11ns、L3 ~40ns、RAM ~300ns。ホットループで L1 キャッシュにフィット(<32KB)が目標。
本番化チェックリスト:ボトルネック特定、アルゴリズム複雑度削減、並行性の適切な活用、メモリリーク防止。
実装例:SIMD による高速化
// 通常のスカラー処理
void vector_add_scalar(float *a, float *b, float *c, int n) {
for (int i = 0; i < n; i++) {
c[i] = a[i] + b[i];
}
}
// AVX-256(8個同時)
#include <immintrin.h>
void vector_add_simd(float *a, float *b, float *c, int n) {
for (int i = 0; i < n; i += 8) {
__m256 va = _mm256_loadu_ps(&a[i]);
__m256 vb = _mm256_loadu_ps(&b[i]);
__m256 vc = _mm256_add_ps(va, vb);
_mm256_storeu_ps(&c[i], vc);
}
}
SIMD により 8倍の並列処理。最新 CPU は AVX-512(16個同時)対応。
キャッシュ効率の測定と最適化
# perf でキャッシュミスを測定
perf stat -e cache-references,cache-misses ./program
# L3キャッシュミスの詳細
sudo perf record -e LLC-loads,LLC-load-misses ./program
sudo perf report
メモリアクセスパターンを改善:
# 悪い:列優先アクセス(キャッシュミス多い)
import numpy as np
A = np.random.randn(1000, 1000)
result = 0
for j in range(1000): # 列ループ
for i in range(1000): # 行ループ
result += A[i, j] # キャッシュラインと直交
# 良い:行優先アクセス
result = 0
for i in range(1000): # 行ループ
for j in range(1000): # 列ループ
result += A[i, j] # キャッシュラインと並行
行優先がデフォルト(C言語, NumPy)。キャッシュヒット率が劇的に向上。
オンライン ビデオストリーミングの遅延最適化
import socket
import time
import threading
class LowLatencyStreamer:
def __init__(self, bitrate_kbps=2000):
self.bitrate_kbps = bitrate_kbps
self.chunk_size = (bitrate_kbps * 1000) // (8 * 30) # 30fps
self.buffer_time_ms = 50 # 50ms バッファ
def stream_chunk(self, sock, chunk):
sock.sendall(chunk)
# Rate limiting
expected_time = len(chunk) * 8 / (self.bitrate_kbps * 1000)
time.sleep(expected_time)
def receive_with_latency_check(self, sock):
received_time = time.time()
chunk = sock.recv(self.chunk_size)
latency = (time.time() - received_time) * 1000
if latency > self.buffer_time_ms:
print(f"Latency warning: {latency}ms (threshold: {self.buffer_time_ms}ms)")
return chunk
ネットワーク遅延(RTT)、バッファリング、フレームレート間のバランス調整。
スレッドプール の最適サイズ決定
import concurrent.futures
import time
import statistics
def benchmark_thread_pool(num_threads, num_tasks, io_wait_ratio):
start = time.time()
with concurrent.futures.ThreadPoolExecutor(max_workers=num_threads) as executor:
futures = []
for i in range(num_tasks):
future = executor.submit(io_bound_task, io_wait_ratio)
futures.append(future)
concurrent.futures.wait(futures)
elapsed = time.time() - start
return elapsed
def io_bound_task(wait_ratio):
# I/O 待機をシミュレート
time.sleep(0.1 * wait_ratio)
compute_time = 0.01 * (1 - wait_ratio)
total = 0
for _ in range(int(compute_time * 1_000_000)):
total += 1
return total
# 最適スレッド数を探索
for num_threads in [1, 2, 4, 8, 16, 32]:
elapsed = benchmark_thread_pool(num_threads, 100, io_wait_ratio=0.9)
throughput = 100 / elapsed
print(f"{num_threads} threads: {throughput:.1f} tasks/sec")
# 結果:I/O 90% なら threads = CPU * 10 が最適
I/O 待機率が高いほど、スレッド数を増加。CPU バウンド ならスレッド数 = CPU コア数。
パフォーマンスプロファイリングツール
Linux perf
CPU フレームグラフ生成: perf record → perf script → flamegraph.pl。ホットスポット関数の即座把握。
Cycle / Instruction / Branch prediction miss を測定。L1/L2/L3 キャッシュミス率も計測。
Flame Graph
縦軸は呼び出しスタック、横軸は時間(CPU サイクル)。広い関数がボトルネック。アルゴリズム改善、並列化の優先順位付けに利用。
CPU と Memory のボトルネック分類
Compute-bound: CPU が限界。SIMD、並列化で改善。
Memory-bound: メモリレイテンシが限界。キャッシュの改善(locality, prefetching)、NUMA aware 最適化。
I/O-bound: ディスク/ネットワーク待ち。非同期 I/O、バッチ処理で改善。
Roofline Model
ピークメモリ帯域幅 vs ピーク計算性能からボトルネック分類。アルゴリズムの計算密度(FLOPs/byte)で限界性能の上界を決定。
最適化の段階的アプローチ
- プロファイリングで ボトルネック特定(推測するな、測定せよ)
- アルゴリズム改善(O(n^2) → O(n log n))
- 実装最適化(キャッシュ局所性、SIMD)
- 並列化(マルチスレッド、GPU)
- 分散化(複数マシン)
早期最適化は避ける。測定駆動で改善。
- [perf(1) man page](h
キャッシュ局所性の最適化
Temporal locality: 同じメモリを短時間に再利用。Caching で効率向上。
Spatial locality: メモリ近傍にアクセス。Prefetching で次のキャッシュ行を先読み。
Loop tiling(blocking): 大型行列操作をキャッシュフィットするサイズに分割。
例:行列積で tile size = 64。キャッシュミス劇的削減。
SIMD(Single Instruction Multiple Data)
벡터화: スカラー操作をベクトル操作に変換。AVX、SSE で 4-8 倍高速化。
コンパイラ最適化: -O3 -march=native で auto-vectorization。
Intrinsics: 明示的に SIMD 命令を記述。
分散キャッシング と CDN
Redis、Memcached: キャッシュミスの削減。メモリ効率重視。
CDN: 地理的分散で遅延削減。エッジキャッシング。
Cache-aside: アプリケーションで cache hit/miss 判定。
Write-through: メモリ更新後キャッシュ更新。一貫性保証、遅延増加。
Write-back: キャッシュ優先更新、後で永続化。遅延低減、障害リスク。
高度なプロファイリング
Trace-based Analysis
Firefox Profiler、Chrome DevTools で時系列分析。フレームレート、メモリ、CPU を同時表示。
Sampling vs Instrumentation
Sampling: 定期的スナップショット。オーバーヘッド低。荒い精度。
Instrumentation: 全関数計測。詳細。オーバーヘッド高(Profiler Overhead)。
CPU キャッシュの詳細
L1(プライベート)→ L2(プライベート)→ L3(共有)。階層的。
False sharing: 異なる変数が同じ cache line に乗る。性能劇的低下。Padding で解決。
Memory Profiler
メモリリーク検出(Valgrind、AddressSanitizer)。確保・解放の trace。
Peak memory usage monitor。Out of memory 予測。
ttps://man7.org/linux/man-pages/man1/perf.1.html)
関連技術とエコシステム
ここで紹介した各技術には、活発なコミュニティ・エコシステムが存在。 継続的な学習とアップデートを推奨。実装言語・フレームワークの選択は プロジェクト要件に基づいて判断。性能、保守性、開発速度のバランスが重要。
まとめ
パフォーマンスエンジニアリングは、最適化テクニックの暗記ではなく、システムの遅さを科学的に扱う姿勢です。観測と仮説を軸にすれば、CPU、DB、ネットワーク、ランタイムのどこが詰まっているかを整理しやすくなります。
参考文献
義・記事