Pコードマシン

pコードマシンとは



pコードマシンとは、物理的なハードウェアではなく、ソフトウェアで実現される仮想的なプロセッサの一種です。これは、エミュレータや仮想機械のように、インタプリタ型のプログラムとして実装され、p-codeと呼ばれる中間コードを解釈・実行します。pコードマシンという用語は、このような仕様一般を指すこともありますが、特にUCSD Pascalのp-Machineを指すことが多いです。

「p」の意味については、Pascal処理系の場合はPascalの頭文字とされていますが、他の言語の場合はpseudo(マイクロソフトのサポート情報参照)やportableなどと解釈されることがあります。

このコンセプトは1966年頃にBCPLのO-codeやニクラウス・ヴィルトのEulerのPとして実装されたのが最初ですが、pコードと呼ばれるようになったのは1970年代初期です。pコードを生成する初期のコンパイラとしては、1973年にNori、Ammann、Jensen、Hageli、Jacobiが開発したPascal-Pコンパイラや、1975年にヴィルトが開発したPascal-Sコンパイラがあります。

ソースコードはコンパイラのコード生成によってpコードに変換され、そのpコードはpコードマシンのエミュレータ、つまりインタプリタによって解釈・実行されます。商業的な観点から、pコードを直接実行するハードウェアが実装された例(Pascal MicroEngineなど)も存在します。

pコードによる実装の利点と欠点



利点


実機の機械語コードに直接変換するのではなく、pコードを経由してインタプリタや実行時コンパイラでpコードを実行する方式には、以下のような利点があります。

移植性の高さ: コンパイラを別のプラットフォームに移植する際、小さなpコードインタプリタを移植する方が、コンパイラ全体を移植するよりも簡単です。これにより、コンパイラ本体は修正なしで異なる環境に移行できます(再コンパイルは必要)。
実装の単純化: コンパイラ作成において、機械語生成部分は最も複雑な部分の一つです。pコードを生成する方が簡単であるため、コンパイラ開発の効率が向上します。
コードサイズの縮小: pコードは理想的な仮想機械に基づいているため、生成されたpコードは実機の機械語コードよりも小さくなることが多く、メモリ効率に優れます。
デバッグの容易性: pコードはインタプリタで実行されるため、インタプリタに各種実行時チェックを組み込むことで、本来の機械語では困難なデバッグが容易になります。
JITコンパイル: Pascalの一部の実装では、pコードの解釈実行にジャストインタイムコンパイル(JIT)方式を利用しています。ニクラウス・ヴィルトは、Pascalの後継であるModula-2向けのm-codeについても言及しています。

1980年代には、イギリスでpコードのプログラムを実行するクロスプラットフォームのオペレーティングシステムBusiness Operating System (BOS)が開発されました。また、UCSD p-Systemは、pコードを利用したマシン非依存で移植性の高いオペレーティングシステムとして知られています。

欠点


pコードの最大の欠点は、実行速度が遅いことです。しかし、実行時コンパイラ(JITコンパイラ)を使用することで、この欠点をある程度補うことができます。

UCSD p-Machine



アーキテクチャ


pコードマシンはスタックマシンであり、ほとんどの命令がスタックからオペランドを取り出し、結果をスタックに戻します。例えば、add命令はスタックの先頭2要素を取り出し、加算結果をスタックに戻します。一部の命令は即値オペランドを持ちます。

Pascalと同様、pコードも型を持ち、ブーリアン(b)、文字(c)、整数(i)、実数(r)、集合(s)、ポインタ(a)が定義されています。

以下に、pコードの簡単な命令例を示します。各命令について、命令名、実行前のスタック状態、実行後のスタック状態、そして簡単な説明が記述されています。

`adi`: (i1 i2) ⇒ (i1 + i2) - 2つの整数の加算
`adr`: (r1 r2) ⇒ (r1 + r2) - 2つの実数の加算
`dvi`: (i1 i2) ⇒ (i1 / i2) - 整数の除算
`inn`: (i1 s1) ⇒ (b1) - メンバーシップ設定。b1にはi1がs1のメンバーかどうかが設定される(true/false)。
`idci i1`: () ⇒ (i1) - 整数定数をスタック上にロード
`mov`: (a1 a2) ⇒ () - メモリ間のコピー
`not`: (b1) ⇒ (~b1) - ブーリアンの否定

環境


FORTHやJava仮想マシンなどの他のスタックベースの環境とは異なり、pコードマシンはコールスタックとしても使用される1つのスタックしか持ちません。マシンには3つのレジスタがあり、それぞれスタック内の位置を指します。

SP (stack pointer): スタックのトップを指します。
MP (mark pointer): 現在のスタックフレームの開始位置を指します。
* EP (extreme pointer): 現在のプロシージャが使用しているスタックのトップ位置を指します。

また、定数領域と動的メモリアロケーション領域が存在します。NPレジスタ (new pointer) はヒープの先頭(最低位アドレス)を指します。EPがNPより大きくなった場合、メモリが使い尽くされたことを示します。

PCレジスタは、コード領域で現在実行中の命令のアドレスを指します。

呼出規約


スタックフレームは次の構造を持っています。


EP →
ローカルスタック
SP → ...
ローカル変数
...
引数
...
リターンアドレス(前のPC)
前のEP
動的リンク(前のMP)
静的リンク(外側のプロシージャのMP)
MP → 関数のリターン値


プロシージャ呼び出しは以下の手順で行われます。

1. `mst n`命令で、スタックにマークを付けます。nは入れ子レベルを示します。この命令は、現在のスタックフレームの上に5要素分の領域を予約し、以前のEP、動的リンク、静的リンクなどを設定します。
2. 呼び出し側は、必要な引数を計算してスタックにプッシュします。
3. `cup n, p`命令で、ユーザープロシージャを呼び出します。nは引数の個数、pはプロシージャのアドレスです。この命令は、PCをリターンアドレスとしてスタックに保存し、プロシージャのアドレスを新しいPCとして設定します。
4. ユーザープロシージャは、`ent 1, i`および`ent 2, j`命令で開始します。これらの命令は、SPとEPをそれぞれ調整し、ローカル変数とスタック領域を確保します。メモリ不足のチェックもここで行われます。
5. `retC`命令で呼び出し元に復帰します。Cはリターン型を表します。リターン値はスタックに残されたまま、呼び出し元に制御が戻ります。
6. 標準プロシージャを呼び出す場合は、`csp q`命令を使用します。標準プロシージャとは、Pascalの標準ライブラリのようなものです(例:`readln()`は`csp rln`、`sin()`は`csp sin`)。



1976年、ニクラウス・ヴィルトは自身の著書『Algorithms + Data Structures = Programs』で、シンプルなpコードマシンを定義しました。このマシンには3つのレジスタ(プログラムカウンタ(p)、スタックベースレジスタ(b)、スタックポインタ(t))と、8種類の命令(うち1種は複数形式を持つopr命令)が存在します。

このマシンは、ヴィルトのプログラミング言語PL/0の実行に使用されました。PL/0は、コンパイラ開発の教育用に使われたPascalのサブセットです。

まとめ



pコードマシンは、特定のアーキテクチャに依存しない中間コード(pコード)を解釈・実行する仮想マシンであり、ソフトウェアの移植性、開発効率、およびデバッグの容易性を向上させる上で重要な役割を果たします。その一方で、実行速度の面で課題がありますが、JITコンパイラなどの技術で改善が図られています。

もう一度検索

【記事の利用について】

タイトルと記事文章は、記事のあるページにリンクを張っていただければ、無料で利用できます。
※画像は、利用できませんのでご注意ください。

【リンクついて】

リンクフリーです。