中間表現

中間表現は、コンピュータがデータをクロスプラットフォームで扱うため、あるいはその他多数のさまざまな目的のために使用されるデータ構造の表現である。

中間表現を用いたデータの抽象化は、コンピューティング分野では一般的な手法である。異なるプラットフォームで同等の情報を保持するデータを異なるフォーマットで扱う場合に、データを中間表現で表現することで複数フォーマットへの変換処理を効率化することを手助けできる、というのは、この手法のあまたある利点のごく1つである。

1. 表現形態
-------

中間表現はテキストデータやバイナリデータなどのデータ構造をとる。すなわち任意のデータ構造をとる。Javaバイトコードなどの「バイトコード」はフィールドの区切りや構造の長さなどがバイト指向な中間表現を指す語である。内部的な場合にはアドレスを指すポインタによって要素を指し示すことで効率的な表現が可能だが、ファイルに書き出すなど外部的な場合には何らかの方法で永続化が必要である。内部的な表現としても、ポインタのような密結合で短命な表現ではなく、粗結合な表現を使うこともある。

プログラミング言語やその他の形式言語を扱うコンピュータ・プログラムでは、ソースコード等の入力にある文字列を字句解析器による字句解析によりトークンに切り分けた後、構文解析器による構文解析により、トークンを葉とする構文木と呼ばれる木構造を構築することが多い。

構文木で表すことができるデータは抽象構文木の中間表現を用いる。抽象構文木は構文木の抽象化を目的として定義され、特定プラットフォームに依存した構文木から不特定多数プラットフォームに対応できる構文木で表現される。いずれの構文木も同等の情報を保持するデータを表す。命令セットを持つデータは中間表現で抽象化した上で、特定プラットフォームの命令セットと中間表現の命令セットをシンボルテーブルで紐付ける。中間表現とシンボルテーブルをセットで定義し、各プラットフォームの命令セットを中間表現を介して変換する。

2. 中間言語
-------

「中間言語」という語は、中間表現が言語の形態をしている場合にそう呼ばれるものである。ここでは主に、プログラミング言語に関係する場合について述べる。

コンパイラでは、ソースコードから直接ターゲットアーキテクチャの機械語コードを生成するのではなく、中間表現を経由するものがある。インタプリタでは、ソースコードを直接解釈実行するのではなく、中間表現を生成してそれを解釈実行するものがある。コンパイラにしてもインタプリタにしても、あるいはそういったコンピュータ・プログラミング言語の処理系に限らず、入力があって出力がある任意のコンピュータ・プログラムにおいて、そういった何らかの中間表現は1段階とは限らず、複数種類があって複数段階のこともある。

歴史的には、言語処理系では1970年代頃までは単一言語、単一ターゲットが普通であった。しかし発想自体は以前からあり、1958年のメルヴィン・コンウェイの文献//Proposal for an UNCOL//で提案されているUNCOLがある。UNCOLは概念の提案のためのものであり、完備された仕様として示されたものではない。また、初期の日本のコンピュータにおいて森口繁一の提案による、素朴なものであるが命令名の共通化などをはかったSIPというものがあった。

1980年代には一般的に見られるようになり、GNU Compiler Collectionでは、中間表現の内部形式はRegister Transfer Languageなどの非依存なものであり、Machine Descriptionsと呼んでいるターゲットの記述によってコードを生成することにより、複数の言語やターゲットに対応している。ただし、中間表現を外部のファイルに書き出したり、外部のファイルから読み込んだり、ということが簡単にできるようにはなっておらず、これは意図的なものである。

その後2000年頃から日本において、GPLでなく利用できるそのようなインフラストラクチャが必要だという考えから整備された「COINSコンパイラ・インフラストラクチャ」がある。COINSには高水準中間表現と低水準中間表現があるなど、対応を広げるためと現代的な最適化のための充実した中間表現を持っている。またその少し後にあらわれたLLVMも同様に、ライセンスがGPLでなくバージョン9では「Apache License v2.0 with LLVM Exceptions」である。}}、中間言語を活用しており、フロントエンドが異なるプログラミング言語のソースコードを1つの中間言語に変換し、バックエンドが1つの中間言語を異なるターゲットのコードへ変換する。LLVMはclang・rustc・Emscriptenなどの「コンパイラ基盤」として使われている。

初期のBASIC処理系の実装で用いられた中間言語は、バイトコードのようなものではなく、字句解析のみを行い、BASICの各ステートメントを単にコードに直接変換するだけといったものもあった。実行時の字句解析が不要になるが、ソースコードに戻して表示、編集することもできる。GOTOの飛び先をキャッシュするなどの工夫がされたものもあった。高度な中間言語を利用する実装もあり、N88-日本語BASICのコンパイラは、実行ファイルを出力するが、その内部は中間言語コードでありまたその実行には外部に、中間言語のインタプリタを含むランタイムライブラリが必要だった。

いくつかのバーチャルマシンやインタプリタは中間言語を直接実行する。しかし、実行時コンパイルにより、中間言語から実行環境に応じた機械語を生成してから実行するほうが高速である。

GPU上で動作するシェーダープログラムやコンピュートカーネルも、移植性の高い中間言語や中間表現が利用されることが多い。

Microsoft DirectXでは、DirectX 8でプログラマブルシェーダーが導入された当初はアセンブリ言語を利用してシェーダープログラムを記述していたが、このときすでにDXBCと呼ばれるGPUベンダー非依存の中間表現を採用していた。まずDirect3Dランタイムがシェーダーコードのバリデーションを実行し、妥当性や有効性を事前に評価してから、Direct3Dドライバーがシェーダーコードを解釈する。この方式はグラフィックスドライバー側の負担を減らし、プログラムの品質を確保することにもつながる。DirectX 9で採用された高水準シェーディング言語であるHLSLのコンパイラ「fxc.exe」はDXBCを出力する。DirectX 12で追加されたシェーダーモデル6.0以降は、新たな中間表現DXILを採用し、LLVM/ClangベースのHLSLコンパイラ「dxc.exe」を使用する。

一方、OpenGLでは長らく中間表現が標準化されておらず、GLSLのシェーダーソースコード文字列をアプリケーションに含めて、ドライバーに毎回API経由で文字列を渡してコンパイルさせる必要があったが、OpenCLやVulkanにおける中間表現としてStandard Portable Intermediate Representationが標準化されたことを受けて、OpenGL 4.6でもSPIR-Vのサポートがコア機能として追加された。中間表現形式を採用することで、シェーダープログラムのオフラインコンパイルができるようになるだけでなく、フロントエンドの言語選択に自由度を持たせることができるようになるというメリットもある。Vulkanの公式SDKにはSPIR-Vを出力するオフラインコンパイラとして「glslangValidator」が付属し、GLSLだけでなくHLSLも入力として使用することができるため、Vulkanではシェーダーの記述に必ずしもGLSLを使う必要はなく、HLSLを使うこともできる。

CUDAはNVIDIA製GPU専用のプログラミング環境だが、GPUアーキテクチャに直接依存しないカーネル中間表現としてParallel Thread Executionを利用することもできる。

3. その他の事例
---------

C言語などの相対的に下位水準に位置する言語を、相対的に上位水準に位置する別言語からのコンパイル先として利用することがある例えば、C++がかつて「C with Classes」と呼ばれていた頃、コンパイラはCのコードを出力していた。}}。そういった場合のC言語は「ある種の中間言語として使われている」ということができる。TypeScriptの実装も、通例JavaScriptコードを出力するトランスコンパイラである。

Unix系の環境では、多くのコンパイラが、オブジェクトファイルを直接生成するのではなく、アセンブリ言語を中間言語として利用し、アセンブラがオブジェクトファイルを生成している。コンパイラドライバに実行ファイル名を指定しないとデフォルトの実行ファイル名が a.out になるという慣習は、大昔はそれがアセンブラからの出力だったからである。

4. 脚注
-----

4.1. 注釈
-------

4.2. 出典
-------

5. 関連項目
-------

LLVMアセンブリ
CIL - .NET環境における中間言語。C#やVB.NETなどの.NET言語のコンパイラが出力する。
C--
WebAssembly
Javaバイトコード
PASCAL P-Code