はじめに — AIに「速くして」って言っても、なぜか速くならない話
正直に言いましょう。こんな経験、ありませんか。
アプリの動きがなんか重い。APIのレスポンスがもっさりしてる。そこでAIに、こうお願いするわけです。
「このコード、遅いから速くして」
で、AIはそれっぽい修正を返してくれます。ループを書き換えたり、見たことない関数を入れてきたり。なんか賢そう。でも実際に動かしてみると…体感、ほとんど変わってない。むしろ別の場所が壊れた。あるいはコードがやたら読みにくくなっただけ。
これ、めっちゃあるあるなんです。そして、ちょっと不思議じゃないですか。AIはこんなに賢いのに、なんで「速くして」だけは上手くいかないんやろう、と。
今日お伝えしたい結論を、先に置いておきます。
性能改善(パフォーマンスチューニング)は、才能でも職人芸でもなくて、「計測 → ボトルネック特定 → 1つだけ変える → 再計測」っていう、誰でも回せる“型”なんです。 そしてAIは、その型の中で使うと一気に頼れる相棒になる。でも型の外で「丸投げ」すると、当て推量を高速化するだけの装置になってしまう。
この記事は、AI開発にまだ不慣れな人でも読めるように、専門用語は全部その場で噛み砕きながら進めます。「プロファイラって名前は聞くけど、結局なにを見ればええの?」っていう状態から、「まず1か所、ちゃんと数字で測る」の一歩を踏み出せるところまで、一緒に行きましょう。
「とりあえず速くして」が失敗する、ちょっと不都合な真実
なんで「速くして」が失敗しやすいのか。理由はシンプルで、人間もAIも、コードのどこが遅いかを“当てられない”からです。
これ、昔から言われてることなんです。Goという言語を作ったRob Pikeさんの有名な「プログラミングの5つのルール」。その1番目がこれ。
ルール1(推測するな): プログラムがどこで時間を使うかは予測できない。ボトルネックは意外な場所で起きる。だから「ここが遅いはずだ」と証明する前に、速度ハックを入れるな。
そしてルール2。
ルール2(計測せよ): 計測するまでチューニングするな。計測した後でも、ある部分が他を圧倒的に上回っていない限り、手を出すな。
ここで言う ボトルネック っていうのは、渋滞の先頭みたいなもんです。道全体が混んでるように見えても、実際に詰まらせてる原因は、たいてい1か所の交差点だったりする。そこを直さずに他の道路を広げても、渋滞は1ミリも解消しない。コードも同じで、体感で「ここが重いやろ」と思った場所は、だいたい外れてる。これがプログラミングの、ちょっと怖いところです。
AIに任せたら? → 2026年の研究が、わりとシビアな答えを出してます
「いやいや、最新のAIなら正しく速くしてくれるでしょ」と思いたくなりますよね。でも、ここは事実をちゃんと見ておきたいんです。
2025年に出た SWE-Perf という研究があります。タイトルがそのまま問いになっていて、「Can Language Models Optimize Code Performance on Real-World Repositories?(言語モデルは実在リポジトリのコード性能を最適化できるか?)」。実在のGitHubリポジトリの“性能改善プルリクエスト”から作った 140件 の課題で、AIを評価しました。
結論はというと、「既存のLLMと、専門家レベルの最適化の間には、大きな能力差(substantial capability gap)がある」。AIはコード生成やバグ修正は得意になったけど、現実のコードを速くする仕事では、まだ人間の専門家にかなり及ばない、と。
もう一本。2025年のソフトウェア工学の国際会議で発表された 「When Faster Isn't Greener(速い=エコ、とは限らない)」 という研究。複数のAIモデルで 118件 の最適化タスクを調べたところ、「最適化の過程で、しばしば“間違ったコード”や“むしろ遅いコード”が生まれた」 と報告されています。さらに、性能向上と実際の省エネには弱い負の相関すらあった、と。
つまり、AIが出してくる「速くしました」を、計測せずに信じるのは危ない、ということ。これは別にAIをディスってるわけじゃないんです。業界のコンセンサスもこうなっています。
AIの最も現実的な役割は「性能アシスタント」。最適化案を出してくれる。でも人間が、それを批判的に評価し、ベンチマークで測り、検証してから取り込むべき。
要は、判断は人間、提案と作業はAI。この役割分担を外すと事故る、ということですね。
古典が今も効く:Knuth と Amdahl
もう2つだけ、古い言葉を。古いけど、AI時代にこそ効きます。
ひとつは Donald Knuth の超有名なやつ。
「早すぎる最適化は、諸悪の根源である」
小さな効率の話は、97%のケースでは忘れていい。でも、残りの“critical 3%”の機会は逃すな、という話です。つまり、闇雲に全部速くしようとするな。本当に効く場所を、計測で見つけてからやれ、と。
もうひとつは アムダールの法則(Amdahl's Law)。これがめちゃくちゃ大事で、ひと言でいうと「全体の中で小さい部分をいくら速くしても、全体は大して速くならない」。
具体的にいきましょう。ある処理が全体の実行時間の 5% しか占めてないとします。そこを、仮に「一瞬(無限に速く)」できたとしても、全体は最大で約5%しか縮まない。100秒かかってた処理が95秒になるだけ。残りの95%を触ってないんだから、当然ですよね。
なのに、私たちはつい「自分が直しやすい場所」「コードが汚くて気になる場所」を最適化したくなる。でもそこが全体の5%なら、どれだけ頑張っても5%。「直しやすい場所」と「直す価値がある場所」は、全然ちがう。 ここを混同するのが、消耗のいちばんの原因かなと思います。
まず、言葉の意味をそろえておきましょう
ここで一回、用語を整理します。知ってる人は飛ばしてOKです。でも「なんとなくしか分かってない」状態だと、AIに指示を出すときも曖昧になっちゃうので、サッとそろえておきましょう。
- プロファイリング: プログラムを動かして、「どの関数に、どれだけ時間がかかったか」を実測すること。いわば コードの健康診断。体調が悪いとき、自己診断(推測)じゃなくて、ちゃんと検査の数値を見ますよね。あれです。
-
プロファイラ: その健康診断をやってくれるツール。Pythonなら
cProfileやpy-spy、Node.jsなら--profや Chrome DevTools、ブラウザなら Performance タブ、みたいに、言語ごとに用意されてます。 - ボトルネック: さっきの渋滞の先頭。全体を遅くしている、一番の原因箇所。
- ホットパス(hot path): よく通る道。何度も実行される、処理の中心ルート。ここが遅いと全体に効きます。
- レイテンシ と スループット: レイテンシは「1件あたりの待ち時間」(注文してから料理が来るまで)。スループットは「単位時間あたりに、どれだけ捌けるか」(1時間で何人さばけるか)。速くしたいのがどっちなのかで、打ち手が変わります。
- Big-O(計算量): データの量が増えたとき、処理時間が どんな伸び方をするか のざっくり表現。O(n) は「データが10倍なら時間も約10倍」。O(n²) は「データが10倍なら時間は約100倍」。小規模だと差が見えないのに、本番でデータが増えた瞬間に爆発するのが、この子の怖さです。
- p50 / p99: 計測値を小さい順に並べたときの、真ん中(中央値=p50)と、悪い方から1%のライン(p99)。平均だけ見てると、たまに激遅な「不運なお客さん」を見落とすので、p99みたいな“悪い方”の数字も見るのが大事です。
- ベンチマーク: 「同じ条件で、公平に時間を測る」こと。これがないと、「速くなった気がする」で終わってしまう。
この中でひとつだけ持って帰るなら、「速い・遅いは“感覚”じゃなくて“数字”で語る」。これだけで、もう半分勝ったようなもんです。
この記事の主役:計測駆動ループ(推測をやめる型)
お待たせしました。ここが本丸です。性能改善は、次の流れをぐるぐる回すだけ。私はこれを 計測駆動ループ と呼んでます。
- ベースラインを測る — 「いま、どれくらい遅いのか」を数字で持つ。これが基準点。基準がないと「速くなった」が言えません。
- プロファイルでボトルネックを特定する — 健康診断して、一番時間を食ってる関数(ホットパス)を見つける。体感で決めない。
- なぜ遅いか、仮説を立てる — Big-O的に重い? 無駄な再計算? 同じデータを何回も読んでる? ここでAIに候補を出させると速い。
- 1つだけ変える — ここ超重要。同時に2つ変えたら、何が効いたのか分からなくなる。 1つ変えて、効果を確かめる。これの繰り返し。
- 再計測する — 本当に速くなった? ついでに、正しさは保ってる? 別の場所が遅くなってない(退行してない)? ここまでやって、初めて「改善した」と言える。
- 記録する — 何をどう測って、どれだけ速くなったかを残す。これが後で効いてきます(あとで触れます)。
そして、ループに入る前にひとつ。アムダールの法則を思い出して、「この箇所は全体の何%か」を先に見積もる。全体の3%しかないところを必死に磨くなら、20%を占めるところを少し改善した方が、よっぽど効く。どこを攻めるかの“地図”を持ってから走る。 これが、消耗しないコツです。
「なんや、当たり前やん」と思うかもしれません。でも、いざコードを前にすると、人はこの順番をすっ飛ばして、いきなりステップ4(書き換え)から始めちゃう。AIに丸投げすると、なおさらステップ4だけが高速に回る。遅いコードが、速い速度で量産される。 これ、めっちゃもったいないんですよね。
じゃあ、AIはどこで使うのか
「計測が大事なのは分かった。じゃあAIいらんやん」とはならないんです。むしろ逆で、このループの 各ステップを加速する相棒 として、AIはめちゃくちゃ優秀。大事なのは、人間が握るところ(What / Why)と、AIに任せるところ(How)を分ける こと。
| 工程 | 人間が決める(What / Why) | AIに任せる(How) |
|---|---|---|
| ベースライン計測 | 何を「速い」とするか(目標値・p99) | 計測スクリプトを書く |
| ボトルネック特定 | どこを直す価値があるか(Amdahlで判断) | プロファイル出力を読解・要約する |
| 仮説立て | どの仮説を採用するか | 原因の候補を幅広く出す |
| 改善の実装 | 1つに絞る・トレードオフを引き受ける | パッチ案を書く・差分を説明する |
| 検証 | 速さと正しさの合否を下す | before/after ベンチやテストを生成する |
| 戻せない変更 | 承認する/しない(ゲート) | 提案までにとどめる |
ポイントは、AIに「判断」をさせないこと。「どこを最適化すべき?」と聞くのはいいけど、その答えを鵜呑みにして本番に入れない。さっきの研究どおり、AIの「速くなります」はけっこう外れます。AIは「候補を大量に出す」「読むのがしんどいプロファイルを要約する」「公平なベンチコードを書く」のが得意。人間は、出てきた候補を計測で選別する。 この分担が、いちばん事故りにくいです。
そのまま使えるプロンプト3本
ここからは、明日の仕事でそのままコピペして使えるプロンプトを置いておきます。共通のコツは、**「修正コードをいきなり書かせない」**こと。まず読解・分析だけさせて、人間が方針を決めてから実装に進む。これだけで、当て推量の修正がグッと減ります。
プロンプト1:プロファイル結果を読解させる
あなたは性能分析のアシスタントです。以下は cProfile の出力です。
次の形式で答えてください。修正コードはまだ書かないでください。
1. 累積時間が長い上位3つの関数(関数名 / 全体に占めるおおよその割合 / 呼び出し回数)
2. それぞれが遅い「可能性のある」理由(出力から読み取れる根拠を必ず添える)
3. この出力だけでは判断できないこと(推測になる部分は「推測」と明記する)
--- プロファイル出力 ---
{ここに cProfile の出力を貼る}
「推測は推測と明記して」と縛るのがミソです。AIは聞かれると自信満々に断定しがちなので、根拠と推測を分けさせる。これは、知らないコードを読むときの“グラウンディング(根拠づけ)”と同じ発想ですね。
プロンプト2:before / after を公平に測るベンチを書かせる
次の2つの関数 before / after の実行時間を、公平に比較するベンチマークを書いてください。
条件:
- 計測の前にウォームアップを数回入れる(最初の数回は遅いので捨てる)
- 30回以上繰り返し、平均ではなく「中央値」と「p95」を出す
- 入力データは {データ規模・分布の説明} を模した合成データで生成する
(実データや個人情報は絶対に使わない)
- 出力は before / after の 中央値・p95・改善率(%)
言語: Python
**「平均でなく中央値とp95」**を指定しているのが大事です。平均は、たまたま遅かった1回に引っ張られる。中央値とp99/p95を見ると、現実のユーザー体験に近い数字になります。あと、合成データを使わせるのは安全のため(後で触れます)。
プロンプト3:AIの最適化案を、取り込む前にレビューさせる
以下は「高速化のために」提案されたパッチです。取り込む前に、批判的にレビューしてください。
観点:
- 正しさ: 元のコードと振る舞いが変わる入力はないか?(具体的なエッジケースを挙げる)
- 可読性/保守性: 失われるものはないか?
- メモリ: 速くなる代わりに、メモリ使用量が増えていないか?
- 全体効果(Amdahlの法則): この関数が全体実行時間の何%を占めるなら、この最適化に取り組む価値があるか?
最後に「取り込み推奨 / まず計測してから判断 / 見送り」のどれかを、理由とともに述べてください。
ただし最終判断は人間が行います。断定ではなく、判断材料を示してください。
自分が書いたコードでも、別のAIに「批判的にレビューして」と頼むと、けっこう穴が見つかります。“速くなる代わりに何を失うか”を必ず聞く。 速度だけ見て可読性が死ぬと、明日の自分が泣きますからね。
(おまけ)「これはアルゴリズムの問題か、定数倍の問題か」を切り分けたいときは、こう聞くと早いです。
このコードが遅い主因は「アルゴリズム的(データ規模が増えると急に悪化)」か、
「定数倍(毎回ちょっと遅いだけ)」のどちらでしょうか。
データ規模が 10倍 / 100倍 になったとき計算量がどう変わる見込みか、根拠つきで。
判断できなければ「計測が必要」と書いてください。
そのまま使えるコード:測ってから、1つだけ変える
プロンプトの次は、実際の計測コードです。Pythonの例ですが、考え方はどの言語でも同じです。
コード1:まず cProfile で健康診断する
いきなり書き換える前に、まず測る。Pythonなら標準ライブラリの cProfile だけで十分です。
# いちばん手軽:スクリプトをまるごとプロファイルして、累積時間が長い順に表示
python -m cProfile -s cumulative your_script.py
特定の処理だけ測りたいときは、コードの中から呼びます。
import cProfile
import pstats
def main():
run_report() # ← 計測したい処理
profiler = cProfile.Profile()
profiler.enable()
main()
profiler.disable()
# 累積時間(cumtime)が長い順に、上位20件を表示
stats = pstats.Stats(profiler).sort_stats("cumulative")
stats.print_stats(20)
出てきた表の cumtime(その関数とその中で呼ばれた処理の合計時間)が長い順の上位が、あなたの“渋滞の先頭”候補です。この出力を、さっきのプロンプト1にそのまま貼ると、AIが読解を手伝ってくれます。
コード2:before / after を公平に測るマイクロベンチ
「速くなった気がする」を、数字に変えます。ポイントは、ウォームアップと、平均でなく中央値・p95。
import time
import statistics
def benchmark(fn, *, warmup=3, repeat=30):
# ウォームアップ:最初の数回はキャッシュ等が温まっておらず遅いので捨てる
for _ in range(warmup):
fn()
samples = []
for _ in range(repeat):
start = time.perf_counter()
fn()
samples.append(time.perf_counter() - start)
samples.sort()
return {
"median": statistics.median(samples),
"p95": samples[int(len(samples) * 0.95) - 1],
"min": samples[0],
}
before = benchmark(lambda: slow_version(data))
after = benchmark(lambda: fast_version(data))
gain = (before["median"] - after["median"]) / before["median"] * 100
print(f"median: {before['median']*1000:.2f}ms -> {after['median']*1000:.2f}ms ({gain:+.1f}%)")
これで「中央値が 42ms → 11ms(−74%)」みたいに、堂々と数字で言えるようになります。逆に「思ったより変わってない」が分かることも多くて、それも立派な収穫。“効かなかった”と分かるのも、計測の価値なんです。
コード3:アルゴリズムを変える(データ構造が支配する)
Rob Pikeのルール5、「データが支配する」。多くの“遅い”は、凝った最適化じゃなくて、データ構造を1つ変えるだけで直ります。古典的なやつを。
# Before: リストに対する「in」は、毎回先頭から順に探す(要素数 n に比例=O(n))。
# それをループの中で回すと、全体は O(n * m)。データが増えた瞬間に急に重くなる。
def find_new_users(all_users, known_ids): # known_ids が list だと…
result = []
for user in all_users:
if user["id"] not in known_ids: # ← ここが毎回 O(n)
result.append(user)
return result
# After: 集合(set)に対する「in」は、平均 O(1)(一発で当たる)。
# データ構造を変えるだけで、全体が O(n) に落ちる。
def find_new_users(all_users, known_ids):
known = set(known_ids) # 最初に一度だけ set 化
return [u for u in all_users if u["id"] not in known]
all_users が10万件、known_ids が5万件あると、Before と After で体感がまるで変わります。しかも After の方が短くて読みやすい。速さと読みやすさが両立するのが、こういう“正しい最適化”の気持ちいいところです。(注意:known_ids がすでに set なら、この最適化は効きません。だから「測ってから」なんですね。)
コード4:速くなった証拠を、CIで守る(退行ガード)
最後に、いちばん地味で、いちばん効くやつ。せっかく速くしても、半年後に誰か(未来の自分かも)が、うっかり遅いコードに戻してしまう。それを防ぐのが、性能の退行(リグレッション)をCIで見張る仕組みです。
# tests/perf/test_hot_path.py
def test_find_new_users_perf(benchmark): # pytest-benchmark の fixture
users = [{"id": i} for i in range(100_000)]
known = list(range(0, 100_000, 2))
result = benchmark(find_new_users, users, known)
# ★ 速さだけでなく「正しさ」も必ず一緒に固定する。
# 速いけど壊れてたら、本末転倒なので。
assert len(result) == 50_000
# .github/workflows/perf-guard.yml
name: perf-guard
on: [pull_request]
jobs:
benchmark:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install pytest pytest-benchmark
# 保存済みベースラインと比較し、中央値が 25% 悪化したら CI を落とす
- run: |
pytest tests/perf --benchmark-only \
--benchmark-compare=baseline \
--benchmark-compare-fail=median:25%
これで、誰かが性能を悪化させるとPRの時点で気づけます。注意点として、CIマシンは実行ごとに速さがブレるので、絶対時間でなく「ベースラインとの比率」で見て、しきい値(ここでは25%)に余裕を持たせるのがコツ。神経質に1%で落とすと、ブレで誤検知して、みんなオオカミ少年扱いして無視するようになります。
やりがちな落とし穴6つと、引き返すライン
型は分かっても、つい踏むやつ。ここに並べておきます。
| # | 落とし穴 | どうする |
|---|---|---|
| 1 | 計測せずに最適化を始める | まずベースライン。基準がないと「速くなった」は言えない |
| 2 | ボトルネックでない所を磨く(Amdahl無視) | 「直しやすい場所」でなく「全体に効く場所」から。割合を先に見積もる |
| 3 | AIの「速くなります」を計測せず信じる | 研究でも“間違い・むしろ遅い”が頻発。必ず再計測で確かめる |
| 4 | 同時に複数の変更を入れる | 何が効いたか分からなくなる。1つ変えて、測る、を繰り返す |
| 5 | マイクロベンチだけで満足する | 本番のデータ規模・分布・p99を見落とす。実運用に近い条件で測る |
| 6 | 可読性や正しさを犠牲にして速くする | 速くても壊れたら意味なし。レビューで「何を失うか」を必ず確認 |
そして、引き返すライン(撤退基準) も先に決めておきましょう。これは「やめる判断」を、感情でなくルールでやるための線です。
- 計測しても効果が頭打ちになった → そこで止める。完璧を狙わない。
- 改善のうまみより、壊すリスク(複雑化・バグ)の方が大きい → 元に戻す。
- 戻しにくい変更(後述)に踏み込みそう → 人間の承認を取るまで進まない。
「速くすること」自体が目的化すると、沼にハマります。目的は“ユーザー体験や費用を良くすること”で、最適化はその手段。ここを見失わないようにしたいですね。
安全に倒す:本番・個人情報・戻せない変更の線引き
性能改善は、ふつうのコード修正より「本番に近いところ」を触りがちなので、安全の線引きを最後に。ここは省略しないでほしいところです。
- 本番環境でのプロファイリングは、原則として承認ゲート。 プロファイラはオーバーヘッド(計測自体の負荷)をかけることがあり、本番に負荷をかけたり、サービスを不安定にすることがあります。やるなら、影響の小さい手法を選び、人間の承認のもとで。
- エラーログやプロファイル出力を、そのままAIに貼らない。 ここに本番のユーザーデータ・個人情報(PII)・トークンやAPIキーなどのシークレットが混ざっていることが、本当によくあります。外部サービスに貼った情報は、消しても残ると思った方がいい。 ベンチで使うデータは、合成データ(ダミー)に置き換える。プロンプト2で「実データやPIIは使わない」と縛ったのは、このためです。
- 戻しにくい最適化は、必ず人間が承認する。 例えば、キャッシュ層の追加、DBのインデックス変更、データ構造のスキーマ変更、インフラ構成の変更。これらは「速くなったけど、別の問題が出て、すぐには戻せない」になりがち。戻せない操作(本番DB・課金・削除・公開・デプロイ)は、AIに自動実行させず、人間のゲートを必ず通す。
- AIに渡す外部由来のテキストは「データ」として扱う。 プロファイル出力やログに紛れた文字列を、指示として実行させない。
ここを守るだけで、「速くしようとして、もっと大きな事故を起こす」という、いちばん悲しいパターンを避けられます。
おわりに — 今日の計測は、明日の自分への置き手紙
ここまで読んでくれて、あざっす。最後に、ちょっとだけ俯瞰の話を。
性能改善って、なんとなく「できる人だけの職人芸」みたいなイメージがあるじゃないですか。でも、ここまで見てきたとおり、その正体は 「推測をやめて、証拠(数字)で動く」 という、すごくシンプルな態度なんですよね。計測して、ボトルネックを特定して、1つ変えて、また測る。AIはその各ステップを猛烈に加速してくれる。でも「どこを攻めるか」「何を失っていいか」を決めるのは、最後まで人間。
そして、今日とった計測の記録やベンチマーク。あれって、未来の自分への置き手紙 なんです。
僕は最近、何かを判断するとき「明日の自分が、今日の自分に“あざっす”って言ってくれるかな?」って考えるようにしてるんです。性能改善でいうと——
「ちゃんと測ってから直してくれて、しかも退行ガードまで置いといてくれたから、半年後にデグレせずに済んだ。あざっす」。
たぶん、こう言ってもらえる。逆に、計測もせず当て推量で書き換えて、何が効いたか分からないコードを残したら、明日の自分は「なんやこれ…」って頭を抱える。責める軸じゃなくて、思いやりの軸で。 未来の自分にプレゼントを贈る感覚で、今日ちょっとだけ測っておく。
それに、「計測して判断する力」って、AIに奪われない、積み上がっていく資産だと思うんです。AIがどれだけコードを速く書けるようになっても、「どこを、なぜ、どこまで速くすべきか」を測って決める力は、自分の中に残り続ける。What と Why は人間、How はAI。この分担で積み上げたものは、ちゃんと自分の財産になる。
最後に、明日からの一歩を、めちゃくちゃ小さくしておきます。
いま「なんか遅いな」と思ってる処理を、1か所だけ、time か cProfile で測ってみる。 それだけ。書き換えなくていい。直さなくていい。まず、数字を1個持つ。
それだけで、明日の自分は「あざっす」って言ってくれる気がします。ほな、また。