ソフトウェアが遅いのは言語のせいではなく設計のせい

遅いプログラムを Rust や C で書き直しても、たいてい速くはならない。Sean Goedecke は、ほとんどの高速化はプログラムのやることを設計し直すことから来るのであって、言語の速さから来るのではないと論じる。

日本語
コピー

多くの人が言うように、AI はソフトウェアをより速い言語で書き直して高速化するだろう。だが、それはたいてい当たらない。ほとんどのプログラムは C や Rust で書き直しても、実際にはほとんど速くならない。

そんなはずがあるだろうか。言語によっては、他の言語より数桁速いものがある。たとえば C は Python より五十倍速い。ならば Python のプログラムを C で書き直せば五十倍速くなるのか。ならない。ソフトウェアの性能はふつう言語の速さではなく I/O で決まる。言語の速さが本当に効く場面では、遅い言語のほとんどにすでに定番のライブラリがあり、C に近い速度を引き出せる。要するに、性能を決めるのは設計であって、どの言語を選ぶかではない。

実際のプログラムの大半は I/O バウンド

データベースを伴う簡単な CRUD サイトを考えてみる。どのリクエストでも、おそらく 50% のリクエスト時間1がデータベースへの問い合わせに費やされる。現在のユーザーを取る、要求されたレコードを取る、関連するユーザーのメンバーシップや設定を取る、といった具合だ。このサイトを C で書き直しても速くなるのは残りの 50% だけで、これが最適化の余地に硬い上限を設ける2。最良の場合でリクエストが二倍速くなる程度(実際は 1.5 倍に近いだろう)。悪くはないが、桁違いの改善にはほど遠い。

言い換えれば、実際のプログラムの大半は「I/O バウンド」で、時間の大半をデータの入出力待ちに費やしている。「CPU バウンド」なプログラムのように、能動的に処理を進めることに時間を使っているわけではない。I/O バウンドなプログラムが待つ相手はデータベースだけではない。サードパーティ API を待つことも、データをディスクに書いたり読み出したりするのを待つこともよくある。curl のコードを 100 倍速くしても、おそらく誰も気づかない。curl の時間はすべてネットワーク待ちに消えているからだ。

遅い言語はとっくに仕事を速い言語に任せている

では、明らかに I/O バウンドではない処理はどうか。たとえば wiki が保存された markdown をレンダリング可能な HTML に変換する3ような場合。この部分を速い言語で書き直せば大きく速くなるのではないか。なる。だからこうした処理を行うアプリは cmark-gfm のようなライブラリを使い、markdown のレンダリングを速い言語に任せている。要するに、遅い言語のほとんどはこうしたライブラリを呼ぶことで、非常に速いプログラムを書ける。Python は遅い言語として有名だが、機械学習や数学のような CPU バウンドの処理では十分に競争力がある。そうした処理を C のコードにオフロードする一揃いのよくできたライブラリがあるからだ。Python プログラムの 80% の時間が GPU での行列積なら、C で書き直して速くすることはまず不可能だ。その行列積はすでに高度に最適化された C かアセンブリのコードで動いている。

もちろん、遅い言語で大量の CPU バウンドな処理をしているプログラムもある。工学的な作り込みがほとんどされていないプログラムなら、すぐに効く最適化の余地がかなりあるはずだ。だが、そういうプログラムはたいてい重要ではない。重要なら、AI が最適化を(比較的)容易にする前にとっくに誰かが手を入れている。だからといって、書き直す価値のある有用なプログラムが見つからないわけではない。Actual Computer は最近 tiktoken をアセンブリで書き直し、大幅な4高速化を実現した。だが、そうやって計算分野そのものを革新することはできない。高速なトークナイズでさえ効果は周辺にとどまる。トークナイズにかかる時間は推論全体のごく一部だからだ。

高速化とはたいてい再設計のこと

それでも、ほとんどのソフトウェアは本来の速度にはほど遠いと思う。よく使われるソフトウェアの性能を大きく上げることについては(AI の有無にかかわらず)楽観している。ただ、その高速化の大半は、速い言語で書き直すことではなく、プログラムを再設計することから来るはずだ。

2018 年の記事で、Bryan Cantrill は C のプログラムを Rust で書き直そうとした経験を語っている。難航した。入念に最適化された C プログラムの設計が、Rust の借用チェッカーと噛み合わなかったからだ。彼は諦めて、ごく素直な方法で Rust で書き直し、20% かそれ以上遅くなることを覚悟した。ところが逆に 30% 速くなった。理由は、Rust の内部が二分探索木ではなく B 木を好むことにあり、B 木はたまたまキャッシュ効率のよいデータ構造だった。つまり、ほぼ CPU バウンドな処理であっても、高性能な C を書くのが非常に得意な人の手にかかっても、性能を決めるのは言語の速さではなく設計なのだ。

これは高水準言語での私自身の経験とも一致する。私が達成した目立った高速化を振り返ると、ほぼ例外なくソフトウェアを再設計して仕事を減らしたものばかりだ。

  • 遅い処理をキャッシュする(多くは外部 API 呼び出し)
  • 遅い処理を非同期にしてバックグラウンドタスクに回す、あるいはリクエスト/レスポンスのライフサイクルの外に出す
  • 逐次実行していた処理を並列化する(そのために大きな処理を分割することもある)
  • 複数の SQL クエリを単一の SQL クエリに置き換える(N+1 問題の解消)
  • 遅い SQL クエリを速い SQL クエリに置き換える
  • 不要な遅い処理を飛ばす(たとえばページング表示で、現在のページにある実体の付随データだけを取る)

これらの戦略は、言語が速いかどうかとは何の関係もない。むしろ遅い高水準言語で書いたほうが、プログラムが何をしているか一目でわかり、最適化しやすくなることさえある。(何らかの理由で)最初からあの Web サーバーをアセンブリで書き直していたら、N+1 クエリに気づくのも、ある API 呼び出しをバックグラウンドタスクに移せることに気づくのも難しかっただろう。

結論

今や誰もが、自分の AI agent にプログラムを Rust や C、アセンブリで書き直させている。遊びとしては面白いが、真面目な最適化の試みとは言えない。こうした書き直しで実際に速くなるプログラムの範囲はごく狭い。

AI を使ってあらゆるものを速くしたいなら(そうすべきだ!)、agent にやらせるべきはプログラムのプロファイリング、ホットスポットの特定、そしてプログラムの振る舞いを再設計する提案だ。ときどき、リファクタリングでは仕事量を減らせない本物の CPU バウンドなホットスポットにぶつかる。そういう稀なケースでは、その部分をより速い言語で書き直すのが効く。しかし最初から全体を書き直しても、ほとんどの人が想像するほど効果は出ない。

ソフトウェアを別の言語で書き直す正当な理由はたくさんある。ポータビリティ、効率、保守性などだ。だが速度だけを取るなら、力を注ぐべきはプログラムの設計だ。

Footnotes

  1. この数字はばかげて高く見えるかもしれない(遅い言語や CPU を酷使する負荷を普段使っているなら)。あるいはばかげて低く見えるかもしれない(そうでないなら)。私の経験では、インタプリタ言語なら少なくとも桁としてはこの程度だ。 ↩

  2. 形式的にはこれを Amdahl の法則と呼ぶ。 ↩

  3. HTML をそのまま保存しておけばいいのでは?それでもいいが、HTML の中身がユーザーによって変わることはある(し、HTML を保存するにしても少なくとも一度は変換が必要だ)。 ↩

  4. tiktoken より 4〜23 倍速いと主張していて、私のノート PC でも実際に約 5 倍速かった。ただこの高速化は、regex への依存を捨てる再設計によるものであって、Rust と Python バインディング自体に固有の速さがあるわけではないと言える。 ↩

出典: Sean Goedecke's Blog← ホームへ戻る