はじめに
ここ最近、「将来的に生成AIがプログラミング言語を介さず、直接機械語(バイナリ)を出力するようになるのではないか」という議論をよく目にする。
今回は、コンパイラの仕組みや最適化の観点から、この問いに対する私なりの考察をまとめたい。
「小さな機械語」は書けるが、「システム」は作れない
簡単な機械語であれば、現在の生成AIでも出力可能だ。
例えば、「Linux x64環境で、8バイト整数2つの最大公約数を返す関数を機械語で生成する」のような、ワンショットで完結する短い命令列なら、AIは正確な機械語を生成できるだろう。
しかし、「Linux用のHTTPサーバーを機械語で生成する」のような大規模システムとなると話は変わる。
このようなシステムは、課題の分割が必要になる。しかし機械語を分割して出力しようとすると「文脈の維持」と「結合の整合性」という新たな問題に直面する。
なぜ「部品化して結合」ではうまくいかないのか
人間が大規模なソフトウェアを作る際、機能をモジュール(部品)に分割する。
「ならば、AIに機械語の部品を作らせて、それをつなぎ合わせればよいのではないか?」と考えるかもしれない。
しかしここには、現状のAIは異なるフェーズの出力を整合性を持って結合する能力が乏しいという問題がある。
それに加え、 「コンパイラによる最適化」 という決定的な壁も存在する。
コンパイラマジックの欠如
現代のプログラミングにおいて、ソースコードは単なる手順書ではない。コンパイラが文脈に合わせて最適な機械語を生成するための「設計図」である。
例えば、Rust言語で書かれた以下のGCD(最大公約数)関数を見てみよう。
fn gcd(mut a: u64, mut b: u64) -> u64 {
while b != 0 {
let temp = b;
b = a % b;
a = temp;
}
a
}
この関数を呼び出すコードを、以下の3パターンでコンパイルするとどうなるか。
fn caller(x: u64, y: u64) {
// パターン1: 両方が変数(実行時まで値が不明)
println!("{}", gcd(x, y));
// パターン2: 片方が定数
println!("{}", gcd(56, y));
// パターン3: 両方が定数
println!("{}", gcd(101, 10));
}
コンパイラは、これら全ての呼び出しに対して、全く同じ機械語(call gcd)を生成するわけではない。
- パターン1: 通常の関数呼び出しを行う
-
パターン2:
56という定数に特化した最適化を行う可能性がある - パターン3: コンパイル時に計算を完了させ、実行時には単に結果の数値を出力するだけの機械語に置き換える(定数畳み込み)
もしAIが、汎用的な「GCD関数の機械語部品」を生成し、それを単純に結合したとしたら、こうした 文脈に応じた最適化(インライン展開や定数畳み込みなど) を行う余地が失われてしまう。
機械語はCPUが直接実行する最終形態であり、後から文脈に合わせて柔軟に形を変えることが極めて困難だからだ。
中間言語アプローチが最適である
では文脈に応じた最適化が済んだ機械語を生成AIが生成できないのかというと、話がまたループし、「そのような規模の出力はワンショットで生成できない」「AI出力を分割しても整合性が取れない」という問題に戻ってしまう。
つまり、生成AIは部品を作り、コンパイラがそれを結合する、という役割分担が最も合理的である。
そして生成AIが作る「部品」は、最適化の余地を残すためには機械語よりも高水準な中間表現(ソースコードや中間言語)であるべきだ。
結論:AIとコンパイラの役割分担
高度な最適化には、全体を見通した上での決定論的な処理が必要であり、これは確率的に振る舞う生成AIよりも、従来のコンパイラの方が圧倒的に得意とする領域だ。
したがって、将来においても最も合理的で現実的なアプローチは以下のようになるだろう。
- 生成AI: 人間の意図を汲み取り、「中間表現(ソースコードやAI向けの中間言語)」を出力する。
- コンパイラ: 中間表現を入力とし、全体最適化を施した上で、決定論的に「機械語」へ変換する。
この「中間表現」が、現在のPythonやRustのままなのか、あるいは人間には読みづらいAI専用の効率的な言語になるのかは分からない。
例えばJITコンパイラのように、AIが生成した中間表現をリアルタイムで最適化しながら機械語に変換する仕組みも考えられる。
しかし、「確率的な思考(AI)」と「論理的な最適化(コンパイラ)」を併用することこそが、システムの性能を最大化する鍵であることに変わりはないだろう。