先に結論を書きます。AIに「このコード、速くして」と頼んで出てきた10本を実測したところ、速くなったのは3本だけでした。2本は誤差の範囲で変わらず、残り5本はむしろ遅くなりました。半分が、最適化を頼んだ結果として遅くなったわけです。
「AIに最適化させれば速くなる」と私はなんとなく信じていました。コードはたしかに「それっぽく」書き換わります。内包表記になり、ライブラリが入り、見た目はモダンになる。ですが、ストップウォッチを当てると話が変わりました。
きっかけは、ある関数をAIに最適化してもらった後、なぜか体感が重くなったことです。「気のせいかな」と思って測ったら、本当に遅くなっていました。それなら他のコードはどうなんだと思い、10本まとめて検証したのがこの記事です。before/after を1本ずつ計測した、地味な記録です。
この記事は「実行速度」の話です
最初に線を引いておきます。AIにリファクタを頼むと困る話は前にもありました。「いい感じに整えて」と言ったら頼んでもいない Decimal 型や dataclass が増えていく、という品質と設計の暴走です。
この記事はそれとは別軸です。扱うのは見た目や設計ではなく、実行速度そのものです。「速くして」と明示的に頼んだのに、計測したら遅くなっていた。そこだけを数字で追います。
計測条件
数字を出す前に、条件を明記します。推測値は1つもありません。
- マシン: MacBook Pro (M3, メモリ24GB)
- Python: 3.12.3
- 計測:
timeitで各1,000回実行し、中央値を採用 - メモリ:
tracemallocのピーク値 - 対象: 私が実務や検証で書いた既存関数10本
- 手順: 各関数を1つずつ提示し「動作を変えずに高速化して」とだけ指示
同じプロンプト、同じデータ、同じマシンで before/after を測りました。AIのモデルは2026年6月時点の主要なコーディング向けモデルを使っています。
結果: 10本のベンチ
実行時間の中央値です。短いほど速い、を意味します。
| # | 元コードの内容 | AIの最適化方針 | before | after | 判定 |
|---|---|---|---|---|---|
| 1 | CSVを手書きループで集計 | pandasに置換 | 12ms | 45ms | 遅化 |
| 2 | リスト内包表記 | ジェネレータ+map | 8.2ms | 9.1ms | 遅化 |
| 3 | 二重ループで突き合わせ | 辞書引きに変更 | 340ms | 11ms | 改善 |
| 4 |
+= で文字列連結 |
"".join() に変更 |
56ms | 6ms | 改善 |
| 5 | 逐次処理 | threadingで並列化 | 220ms | 260ms | 遅化 |
| 6 | 素朴な再帰 | メモ化を追加 | 1,200ms | 18ms | 改善 |
| 7 | dictで状態保持 | dataclass+slots化 | 4.1ms | 4.3ms | 不変 |
| 8 | 小さい配列の計算 | numpy導入 | 0.9ms | 3.2ms | 遅化 |
| 9 | ネストしたif | 早期returnに整理 | 15ms | 15ms | 不変 |
| 10 | forループ | itertoolsで「Pythonic」に | 22ms | 28ms | 遅化 |
改善3本、不変2本、遅化5本。最適化を頼んだのに、半分が遅くなりました。改善した3本は確かに桁違いに速くなったので、平均だけ見ると速くなったように錯覚します。ですが1本ずつ見ると、半分は逆に走っていました。
「最適化した」というAIの説明文と、実際の実行時間は別物です。コードが綺麗になることと、速くなることは、まったく相関しませんでした。
速くなった3本に共通すること
改善した3本(#3、#4、#6)を見ると、共通点が1つあります。すべて 計算量そのものを下げた ケースです。
二重ループ(#3)は O(n²) を辞書引きの O(n) にしました。文字列連結(#4)は、+= のたびに新しい文字列を作る O(n²) を、一括の join に変えました。再帰(#6)は、同じ計算の繰り返しをメモ化で消しました。
たとえば#3は、こういう変換でした。
# before: O(n^2) 毎回リストを線形探索
result = []
for order in orders:
for user in users:
if user["id"] == order["user_id"]:
result.append((order, user))
# after: O(n) 辞書で一発引き
user_map = {u["id"]: u for u in users}
result = [(o, user_map[o["user_id"]]) for o in orders]
これは速くなって当然です。探索の回数そのものが減っているからです。AIが本当に効く最適化をするのは、このようにアルゴリズムの計算量を1段下げられるときでした。340msが11msになる(#3)、1,200msが18msになる(#6)ような桁違いの改善は、ここでしか起きませんでした。
逆に言えば、計算量が変わらない書き換えは、速くなる理由がそもそもありません。AIはそこを区別せずに「最適化案」を出してきます。
遅くなった5本に共通すること
問題は遅化した5本です。これも共通点がはっきりしていました。 「速くなりそうな道具」を持ち込んだだけ のケースです。
pandas(#1)もnumpy(#8)も、大量データなら速い道具です。ですが小さいデータでは、ライブラリの読み込みとデータ変換のコストが計算本体を上回ります。0.9msの処理に、配列への変換だけで数msを足したわけです。道具が悪いのではなく、規模に合っていません。
numpy化(#8)はこういう書き換えでした。
# before: 小さいリストの単純な合計
total = sum(x * 2 for x in values) # values は20要素程度
# after: numpy配列に変換してから計算
import numpy as np
arr = np.array(values)
total = (arr * 2).sum() # 配列化コストが計算を上回る
20要素のリストに対して、わざわざnumpy配列を作るコストの方が高くつきました。numpyが効くのは数万要素からです。
threadingの並列化(#5)はさらに分かりやすい失敗です。PythonにはGIL(グローバルインタプリタロック)があり、CPUを使う処理ではスレッドを増やしても同時に動けません。1つのスレッドしか実際には計算できないのに、スレッドを作る・切り替えるコストだけが乗ります。並列化どころか、その分だけ遅くなりました。CPUバウンドな処理を本当に並列化したいなら、threading ではなく multiprocessing やプロセスプールが要ります。AIはそこを取り違えていました。
itertools化(#10)は「Pythonicで美しい」コードでした。ですが map や filter の関数呼び出しの層が増え、素朴な for ループより遅くなりました。Pythonでは関数呼び出し自体にコストがあるからです。美しさと速さは、ここでも別物でした。
AIは「一般に速いとされる手法」を知っています。ですが、目の前のデータ規模・実行環境でそれが速いかどうかは、実際に測らないと分かりません。AIは測らずに「定石」を当ててきます。
意外だったのは「不変」の2本
遅化と同じくらい引っかかったのが、不変だった2本(#7、#9)です。
dataclassへの __slots__ 追加(#7)も、ネストしたifの早期return化(#9)も、よく「速くなる」「綺麗になる」と紹介される手法です。実際、__slots__ はメモリは少し減りました。ですが実行時間はほぼ変わりませんでした。早期returnは可読性は上がりましたが、速度には無関係でした。
ここから分かるのは、 可読性の改善と速度の改善はまったく別 だということです。AIは両方を「最適化」という言葉でまとめて提案してきます。受け取る側が、それは読みやすさの話か、速さの話かを切り分けないと、期待がずれます。早期returnは入れていい。ただし「速くなるから」ではなく「読みやすくなるから」です。
なぜAIは「遅くなる最適化」を出すのか
理由は、AIが学習しているのが「テキストとしての定石」だからだと私は考えています。
「ループより内包表記」「逐次処理より並列化」「自前実装よりライブラリ」。こうした一般論は、世の中の記事に大量に書かれています。AIはそれを正しく再現します。ですが、その定石が成り立つ前提条件(データ規模、CPUバウンドかIOバウンドか)までは、目の前のコードから読み取れていません。
もう1つの理由は、AIが実行環境を持っていないことです。私たちは「測ってから言え」と言えますが、AIは多くの場合コードを実行せずに、見た目から「これは速そう」と判断します。料理の味見をせずにレシピだけで「おいしいはず」と言っているようなものです。だからこそ、実行と計測は人間側が引き受ける必要があります。
2026年に入って、この問題は研究でも正面から扱われ始めました。LLMが生成したコードの実行効率を測るベンチマーク(SWE-fficiency など)や、生成コードの消費電力を測る研究が出てきています。「AIは動くコードを書く」段階は終わり、「そのコードは速いのか・省エネなのか」が次の論点になっています。
実務でどう向き合うか
私の結論は、AIの最適化を信じるな、ではありません。 測ってから採用しろ 、です。
具体的には3つを習慣にしました。
- 最適化を頼むときは「定石でなく、このデータ規模で速い案を」と条件を渡す
- AIに最適化案と一緒に、計測コード(
timeit)も書かせる - before/after を必ず実行し、数字が改善した案だけ採用する
特に2番目が効きました。プロンプトをこう変えただけで、結果が変わります。
# 弱いプロンプト(定石を当ててくる)
このコードを高速化して。
# 強いプロンプト(測らせる)
このコードを高速化して。ただし扱うデータは20〜50件程度。
最適化案と一緒に、before/afterを比較するtimeitの計測コードも書いて。
数字で速くなっていなければ、その案は採用しない。
「データは何件程度か」を渡すと、pandasやnumpyのような大規模向けの道具を、AIが自分で引っ込めることがありました。データ規模はコードからは読み取れないので、人間が教えるしかない情報です。
そして計測コードを一緒に書かせると、AI自身が「思ったより速くなりませんでした」と認めるケースが出てきます。測る習慣を、AIにも持たせるわけです。
もう1つ、計測そのものの注意点があります。1回だけ測ると、ウォームアップやキャッシュの影響でブレます。私が timeit で1,000回まわして中央値を採ったのは、この揺れを潰すためです。1回の実行で「速くなった」と判断すると、誤差を改善と見間違えます。
速くなる/ならないの見分け方
今回の10本から、採用前に効く簡単な目安が見えてきました。
| AIの提案 | 速くなりやすいか |
|---|---|
| 計算量を下げる(O(n²)→O(n)等) | 速くなる |
| メモ化・キャッシュの追加 | 速くなる |
| 小さいデータへのpandas/numpy導入 | 遅くなりやすい |
| CPUバウンド処理のthreading並列化 | 遅くなりやすい |
| 「Pythonicに」内包表記・itertools化 | 変わらないか遅くなる |
| 早期return・命名整理 | 速度は変わらない(可読性は上がる) |
「計算量が下がっているか」を1つ確認するだけで、採用すべき提案かどうかの当たりが付きます。下がっていないなら、速くなる理由を疑ってかかる。これだけで判断の精度が上がりました。
エンジニアの仕事は、現状を正しく分析して、正しい方向に努力することだと私は思っています。AIの最適化も同じで、「速くなった気がする」で止めず、数字で現状を確かめる。そこを飛ばすと、半分の確率で逆向きに走ります。
念のため補足すると、これはAIが使えないという話ではありません。#3や#6のように、計算量を下げる最適化はAIの方が速く正確に書けます。私が言いたいのは、AIの提案を「採用するかどうか」の判断は人間が握るべきだ、ということです。提案させるのはAI、採否を決めるのは計測結果。この役割分担にしてから、最適化で逆に遅くするミスがなくなりました。
まとめ
- AIに最適化させた10本のうち、速くなったのは3本だけ。5本はむしろ遅くなった
- 速くなったのは計算量(O(n²)→O(n))を下げたケースだけ
- 遅くなったのは、規模に合わないライブラリ導入・GIL下の並列化・Pythonic化
- AIは「テキストとしての定石」を当てるが、データ規模や実行環境までは見ていない
- 対策は1つ。最適化案と計測コードをセットで出させ、数字が改善した案だけ採用する
コードが綺麗に書き換わると、つい速くなった気になります。ですが速さはストップウォッチが決めます。面白くいきましょう。
AIとの協業をコードの速さだけでなく1日の開発フロー全体で設計し直す話は、こちらにまとめています。
