ネイティブコードより遅いのに、なぜPコードが選ばれたのか
1. はじめに
「なぜわざわざ遅い方法を選んだのか」。コンピュータの歴史を学ぶと、こんな疑問にぶつかることがあります。
その代表格が「Pコード(P-code)」です。プログラムをネイティブの機械語に直接変換するのではなく、一度「仮想マシン用の中間コード」に変換してから実行するこのアプローチは、一見すると明らかに非効率です。実際、実行速度はネイティブコードの数分の一から数十分の一になることもありました。
それでも1970年代から80年代にかけて、UCSD Pascal、Smalltalk、そしてのちのJavaまで、多くのシステムがこの「遠回りな道」を選びました。
なぜか。当時の現実に照らせば、Pコードは最も合理的な選択でした。
技術の選択は、常にその時代のハードウェアや産業構造の制約の中で行われます。Pコードが生まれた背景、その設計思想、メリットとデメリット、そして現代の技術への影響を順に追います。
2. 1970年代のコンピュータという「混沌」
2-1. 無数の異なるアーキテクチャが乱立していた
現代のソフトウェアはほぼ2系統で動いています。x86_64(Intel/AMD)とARM(Apple Silicon、スマートフォン)。大多数のコードはこのどちらかに向けて書けば十分です。
1970年代は違いました。
MOS Technology 6502(Apple II、Commodore 64)
Zilog Z-80(CP/Mマシン、TRS-80)
Intel 8080/8085(初期のIBM互換機系統)
Digital Equipment Corporation PDP-11(大学の研究室)
Motorola 6800/68000
これらはすべて、命令セット(プロセッサが直接理解できるバイナリ命令の体系)が根本から異なります。6502向けに書いたネイティブコードは、Z-80では動きません。機械語のバイナリには互換性がゼロです。
つまり、同じプログラムを複数の機種で動かしたいなら、ネイティブコードのアプローチでは「それぞれのCPU向けに個別にコンパイルし、個別にテストし、個別にメンテナンスする」必要がありました。当時のソフトウェアベンダーにとって、これは現実的ではありませんでした。
2-2. メモリは「贅沢品」だった
もう一つ、現代の感覚では想像しにくい制約がメモリです。
1970年代後半のパーソナルコンピュータは、標準的に 4KB〜64KBのRAM しか持っていませんでした。現代のスマートフォンの100万分の1以下です。
この制約は二重の意味でコンパイラ設計に影響しました。
第一に、コンパイラ自体をメモリに収める必要がありました。 高機能なネイティブコード生成器(最適化パスを持つコンパイラ)は、当時の基準でも相当なメモリを要します。64KBという空間に、コンパイラ本体・コンパイル中のソースコード・生成中のコード・作業領域をすべて収めるのは至難の業でした。
第二に、生成されるコードもできるだけ小さくする必要がありました。 プログラム本体が入るメモリ領域は限られています。コードが少しでも小さければ、それだけ複雑な処理を同じメモリ空間に詰め込めます。
Pコードはこの両方の問題に対する答えでもありました。

3. スタックマシンとして設計されたPコード
3-1. ETHチューリッヒでの誕生
Pコードマシンの概念は、1970年代にスイスのETHチューリッヒでNiklaus Wirthと彼のチームが開発したPascal-Pコンパイラから生まれました。最初の実装(Pascal-P1)は1973年に登場し、CDC 6000シリーズのメインフレームを対象としていましたが、その生成するコードは特定のハードウェアに依存しないものでした。
「P-code」の「P」は Pseudo-code(擬似コード) または Portable code(移植可能コード) の略とされています。設計の核心にあったのは、「特定のCPUを仮定しない抽象的な命令セット」を作ることでした。
3-2. スタックベースの仮想マシン
Pコードマシンはスタックマシンとして設計されました。
通常のCPU(レジスタマシン)は、演算の中間結果をレジスタ(CPUの内部記憶)に保持します。レジスタの数はCPUによって異なり(Z-80には8個、PDP-11には8個の汎用レジスタ)、命令の多くは「どのレジスタを使うか」を明示する必要があります。
スタックマシンは違います。演算のオペランド(計算対象)はすべてスタック(LIFO構造の作業領域)から取り出し、結果もスタックに積みます。「どのレジスタを使うか」を命令に埋め込む必要がないため、多くの命令が1バイトで表現できます。
例えば、`2 + 3` という計算はこうなります:
PUSH 2 ; スタックに 2 を積む
PUSH 3 ; スタックに 3 を積む
ADD ; スタックトップ2つを取り出し、和をスタックに積むNiklaus Wirthは1976年の著書 "Algorithms + Data Structures = Programs" の中で、このPコードマシンを正式に仕様化しました。プログラムカウンタ・ベースレジスタ・スタックトップレジスタの3本と、8種類の命令セット(`opr`命令は多形式)という、驚くほど簡潔な設計です。
3-3. UCSD Pascalが世界を変えた
理論だったPコードを実用化したのが、UCSD Pascal(1977年)です。カリフォルニア大学サンディエゴ校(UCSD)のKenneth Bowles教授らのチームが開発したこのシステムは、PコードインタープリタをApple II(6502)・Z-80マシン・PDP-11など複数のプラットフォームに移植し、「同じPコードのプログラムが複数の機種で動く」という体験を初めて現実のものにしました。
UCSD p-Systemと呼ばれたこのポータブルOSは、Javaが「Write Once, Run Anywhere」を掲げる15年以上前に、その概念の原型を実現していたのです。
4. Pコードが選ばれた5つの理由
4-1. 移植性(ポータビリティ)
最も根本的な理由は、1種類のPコードインタープリタを書けば、それ以降はすべてのプラットフォームでプログラムが動くという移植性の大きさです。
ネイティブコード方式の場合、新しいCPUへの対応は「コンパイラのバックエンド全体を書き直す」という大仕事です。コード生成、レジスタ割り当て、命令スケジューリング、プラットフォーム固有の呼び出し規約、そのすべてを新しいアーキテクチャに合わせて再実装する必要があります。
Pコード方式の場合、新しいCPUへの対応は「小さなインタープリタを1つ書く」だけです。Pコードの命令セットは小さく、スタック操作は概念が単純なため、経験のある開発者なら数週間で移植できました。
ETH版PコードマシンはCDC 6000、DEC PDP-11、さらに様々なマイクロコンピュータに移植されました。Wirthの言葉を借りれば、「新しいマシン向けに小さなPコードインタープリタを書く方が、コンパイラ全体を書き換えるよりはるかに容易だった」のです。
4-2. コードサイズの圧縮
スタックマシンの命令は多くが1バイトです。レジスタ番号や複雑なアドレッシングモードを命令に含める必要がないためです。
実測データとして、UCSD Pascal関連の文書には「同等のネイティブコードと比較してPコードはほぼ半分かそれ以下のサイズになる」という記述があります。64KBというメモリ制約の中で、これは非常に大きな意味を持ちます。メモリが半分で済めば、より大きなプログラムが同じ環境で動かせるのです。
4-3. コンパイラ開発のコスト削減
コンパイラは「フロントエンド」と「バックエンド」に分けて考えることができます。
フロントエンドはソースコードを解析して内部表現(この場合はPコード)に変換する部分です。Pascal、C、Fortranなど言語ごとに実装します。
バックエンドは内部表現を実際のCPU命令に変換する部分です。x86、ARM、MIPSなどアーキテクチャごとに実装します。
ネイティブコード方式では、3言語×5プラットフォームなら理論上15通りのバックエンドが必要です。Pコード方式なら、フロントエンドは3つ、バックエンド(インタープリタ)は5つ、合計8つで済みます。開発と保守のコストは大幅に下がります。
この考え方は今も生きており、LLVMはまさにこの「共通IR(中間表現)」のアイデアを現代的に実現したものです。
4-4. ソースコードの保護
1970〜80年代のソフトウェア流通は、フロッピーディスクでのバイナリ配布が中心でした。ネイティブコードは高度なリバースエンジニアリングツールがなくても比較的読み解きやすく、知的財産の保護が難しいという問題がありました。
Pコードは専用の仮想マシン命令セットに変換されているため、直接的なネイティブコードより解読が一段困難です。この「難読化」の副次効果も、商用ソフトウェアの配布手段として評価されていました。
4-5. 実行時のエラー検出
スタックマシンベースのPコードインタープリタは、実行時に様々な安全チェックを自然に組み込めます。配列の境界チェック、ヌルポインタアクセスの検出、型の整合性確認などです。
当時のネイティブコードコンパイラはこれらのチェックをほとんど省略していました(検査コードはコードサイズと速度のコストになるため)。Pコードインタープリタならば、これらのチェックをインタープリタ自体に実装すれば全プログラムに自動適用されます。デバッグのしやすさという点でも優位性がありました。

5. Pコードのデメリット
5-1. 実行速度の大幅な低下
Pコードの最大の欠点は、疑いなく実行速度です。
ネイティブコードなら、CPUは機械語命令を直接フェッチして実行します。ハードウェアパイプラインやキャッシュが最大限に機能します。
Pコードインタープリタの場合、実行のたびに「Pコードを読む→命令をデコードする→対応するネイティブ処理を呼ぶ」というループが必要です。単純なインタープリタは、等価なネイティブコードの 5〜50倍の時間 がかかることもありました。
計算集約的なプログラム(数値計算、画像処理)では、この速度差は致命的です。UCSD Pascalは主に教育目的やビジネスアプリケーションで使われました。科学技術計算はスピードが命なので、5倍以上の実行時間は許容範囲の外でした。
5-2. ランタイム環境への依存
Pコードで書かれたプログラムを実行するには、必ずそのシステム向けのPコードインタープリタ(ランタイム)が必要です。ランタイムがインストールされていない環境では動きません。
「単体実行ファイルを配布する」という形態がネイティブコードでは自然にできることが、Pコードでは常にランタイムとのセット配布を要求します。現代のJava(JVMが必要)やPython(Pythonインタープリタが必要)でも、この問題は本質的に変わっていません。
5-3. 最適化の限界
ネイティブコードコンパイラは、プログラム全体を把握した上で積極的な最適化を行います。不要な計算の除去(定数畳み込み)、ループのアンローリング、インライン展開など。
単純なPコードインタープリタは、このような「全体最適化」が難しい構造です。Pコードを1命令ずつ解釈実行するため、「数命令先を見越した最適化」が原理的にできません。
6. 現代の技術への影響
6-1. JavaがPコードの思想を継承した
1995年にSun Microsystemsが発表したJavaは、UCSD Pascal p-Systemと同じ哲学を現代的に実現したものです。
JavaソースコードはJavaコンパイラ(`javac`)によってバイトコード(`.class`ファイル)にコンパイルされます。このバイトコードは特定のCPUに依存しない仮想的な命令セットであり、Java仮想マシン(JVM)が各プラットフォームでこれを実行します。
「Write Once, Run Anywhere」というJavaのスローガンは、UCSD p-Systemが20年近く前に掲げた思想と本質的に同じです。JavaのバイトコードはPコードの直系の子孫です。
.NET CLR(C#の実行基盤)もPython(`.pyc`バイトコード)も、同じ系譜に属します。
6-2. JITコンパイラが「速度の壁」を突き破った
PコードとネイティブコードのトレードオフをJavaが解決したのが、JIT(Just-In-Time)コンパイラです。
JITコンパイラは、プログラム実行時に「頻繁に実行されるコード(ホットスポット)」を動的に検出し、その部分だけをリアルタイムでネイティブコードにコンパイルします。初回実行はインタープリタ方式で遅くても、繰り返し実行されるループや関数はやがてネイティブコードと同等の速度で動くようになります。
Sun MicrosystemsがJVM 1.1(1997年)でJITを導入して以降、JavaはCプログラムと比較可能な速度圏に入りました。現代のJVM(HotSpot)は状況によってはCで書かれた等価なプログラムと比較可能な速度で動作します。
JITの概念自体は1960年代のLISP処理系にまで遡れますが、それを実用的な規模で実現したのはJavaの普及が大きなきっかけでした。
6-3. LLVMとWebAssembly
現代のコンパイラ基盤であるLLVMは、「LLVM IR(中間表現)」という共通の中間コードを核に設計されています。ClangはC/C++を、SwiftコンパイラはSwiftを、それぞれLLVM IRに変換し、その後LLVMのバックエンドがx86、ARM、RISC-Vなどのネイティブコードを生成します。
PコードマシンのアイデアとLLVM IRの発想は根を同じくします。「特定のハードウェアから独立した抽象的な中間表現を核に置く」という設計思想です。
さらに、WebAssembly(Wasm)はブラウザ上で動く低レベル中間表現として2019年にW3C勧告となり、現在はブラウザの外でも使われ始めています。Wasmはまさに「現代版Pコードマシン」と呼べる存在です。
7. おわりに
当時の開発者たちが性能を犠牲にしてもPコードを選んだのは、「解決すべき問題が他にあった」からです。
ハードウェアが乱立し、メモリが極端に少なく、コンパイラ技術が発展途上だった1970年代には、移植性と開発効率こそが最優先課題でした。JITコンパイラは存在せず、性能問題の解決策も見えていませんでした。そのような状況で、Pコードは最も現実的な答えでした。
技術を評価するなら、「その時代の制約の中で何を解決したか」を見る必要があります。PコードはJava、.NET、Python、WebAssemblyなど、現代のあらゆる「仮想マシン型言語」の礎を築きました。そのアイデアは半世紀を経た今も、形を変えて生き続けています。
参考資料:
