diff options
| author | Jun Kawasaki <jun784@gmail.com> | 2026-10-06 13:01:11 +0900 |
|---|---|---|
| committer | Jun Kawasaki <jun784@gmail.com> | 2026-10-06 13:01:11 +0900 |
| commit | 863a6ed0ad723e06c76f392d4fb3a5f6bd22863c (patch) | |
| tree | cb0c42e19afbd06c455e977fa05980042da2e1d7 | |
| parent | 81b654d76444d747fdf1d053760e507b50f26e18 (diff) | |
| download | Simple-863a6ed0ad723e06c76f392d4fb3a5f6bd22863c.tar.gz Simple-863a6ed0ad723e06c76f392d4fb3a5f6bd22863c.zip | |
docs: add Japanese translations for all chapters
116 files changed, 10261 insertions, 0 deletions
diff --git a/README.ja.md b/README.ja.md new file mode 100644 index 00000000..a6b543b4 --- /dev/null +++ b/README.ja.md @@ -0,0 +1,102 @@ +# Simple + +[English](README.md) | 日本語 + +この文書は [Simple の README](README.md) の日本語訳です。 + +Sea of Nodes コンパイラ中間表現(IR)を紹介する Simple + +このリポジトリは、Sea of Nodes コンパイラ IR を実演するためのものです。 + +Sea of Nodes は、HotSpot の C2 コンパイラ、Google の V8 コンパイラ、 +Sun/Oracle の Graal コンパイラで使われる中核的な IR です。 + +SoN IR の紹介が目的なので、実装する*言語*そのものの重要性は高くありません。 +C や Java に似た、機能がずっと少ない非常に単純な言語を使います。 +Simple は強い型付けを持つオブジェクト指向言語で、*クロージャ*ではなく*関数*が第一級の値です。 +オブジェクト参照はポインタであり、型システムによって null ポインタ例外を禁止します。 +配列にもいずれ範囲チェックが導入されると考えられ、その場合 Simple は完全に安全な言語になります。 +Simple の構文は最小限で、再帰下降パーサーで解析できます。 + +実用システムでは Sea of Nodes を機械語生成に使いますが、 +この実演ではバックエンドの実装は難しく、重要性も比較的低くなります。 +このリポジトリは x86-64、RISC-V、ARM64 を対象に事前(AOT)コンパイルを行いますが、 +JIT コンパイルも視野に入れています。 + +このリポジトリは、どの意味でも完全な言語を目指してはいません。 +そのため、バックエンドは SoN IR を直接解釈する Java の評価器から始まります +(第8章で初めて導入します)。コード生成は第19章で初めて登場します。 + +## 各章 + +以下は大まかな計画であり、変更されることがあります。 + +各章は自己完結した完全な実装です。Simple 言語の一部を完全に実装し、 +前の章で作成したものをすべて含みます。また、各章には Sea of Nodes +中間表現の関連する側面について詳しい解説を付けます。 + +Simple 言語のスタイルは、C または Java のサブセットに倣っています。 + +* [第1章](chapter01/README.ja.md):整数リテラルを返すスクリプト。引数を受け取らず、整数値を1つ返す空の関数。`return` 文。 +* [第2章](chapter02/README.ja.md):定数を使う加算、減算、乗算、除算などの単純な二項算術演算。ピープホール最適化と単純な定数畳み込み。 +* [第3章](chapter03/README.ja.md):ローカル変数と代入文。右辺での読み取り、SSA、ローカル変数が定数の場合の追加のピープホール最適化。 +* [第4章](chapter04/README.ja.md):`arg` という名前の、定数ではない外部入力変数。定数と `arg` を使う二項演算と比較演算。ゼロ以外の値は真とみなします。代数的簡約によるピープホール最適化。 +* [第5章](chapter05/README.ja.md):`if` 文。制御フローグラフ(CFG)の構築。 +* [第6章](chapter06/README.ja.md):実行されない制御フローに対するピープホール最適化。 +* [第7章](chapter07/README.ja.md):`while` 文。ループ構造と先行的な Phi の導入。 +* [第8章](chapter08/README.ja.md):ループ構造の続き。必要時の Phi の生成、`break` 文と `continue` 文。 +* [第9章](chapter09/README.ja.md):大域的値番号付け。不動点に達するまでのピープホール最適化の反復。ワークリスト。 +* [第10章](chapter10/README.ja.md):ユーザー定義の構造体、ポインタ、null 解析。SSA で表す単一のメモリ値。ロード、ストア、実行可能な評価器。 +* [第11章](chapter11/README.ja.md):同値類によるエイリアス解析。`MemMerge`、`MemPhi`、`BulkMemPhi` による必要時のメモリ分割。 +* [第12章](chapter12/README.ja.md):参照フィールド、前方参照、再帰的な構造体。 +* [第13章](chapter13/README.ja.md):大域的コード移動とスケジューリング。 +* [第14章](chapter14/README.ja.md):数値型。浮動小数点数、狭い整数型、範囲、`f32` への丸め。 +* [第15章](chapter15/README.ja.md):静的な長さを持つ一次元配列型。配列のロードとストア。 +* [第16章](chapter16/README.ja.md):コンストラクタ。 +* [第17a章](chapter17a/README.ja.md):束縛の可変性、参照の権限、深い読み取り専用ビュー。 +* [第17b章](chapter17b/README.ja.md):糖衣構文。`var`、`val`、`x+=y`、`for(init; test; next) body`。 +* [第18章](chapter18/README.ja.md):関数と呼び出し。 +* [第19章](chapter19/README.ja.md):命令選択と移植可能なコンパイル。 +* [第20章](chapter20/README.ja.md):グラフ彩色によるレジスタ割り当て。 +* [第21章](chapter21/README.ja.md):命令エンコーディングと ELF。 +* [第22章](chapter22/README.ja.md):Simple の Hello, World! +* [第23章](chapter23/README.ja.md):メソッドと型の再検討。 +* [第24章](chapter24/README.ja.md):連鎖した条件分岐と SCCP。 +* [第25章](chapter25/README.ja.md):モジュール、分割コンパイル、不完全な型を使う SSA の構築。 + +## 章をまたぐビルド + +任意で使える[対話的なグラフビューア(英語)](graph/README.md)は `graph/` で共有しています。 +第1〜25章では、章のディレクトリで `make view` を実行して起動します。 + +[デバッグ用プリンタ(英語)](print/README.md)は `print/` で共有しています。 +第2章で必要なのは1行の式形式だけです。共通の `BaseNode` が、プリンタとビューアの両方に +ノードの同一性とエッジへのアクセスを提供します。IR が拡張されるにつれて、 +各章のアダプタが意味に関する詳細を追加します。 + +[命令エンコーダ(英語)](isa/README.md)は、第21〜25章の x86-64、ARM、RISC-V の +バイト出力処理と、ARM/RISC-V のテスト用評価器を共有します。 +機械命令ノードの選択、レジスタ割り当て、コード配置は各章に残ります。 +共通層は具体的なオペランドを受け取り、バイト列を書き出します。 + +最上位の Makefile は、各章の `tests`、`tags`(または `tag`)、`release`、`lib` ターゲットを実行します。 +新しく取得したチェックアウトでは、まず `make lib`、続いて `make tests` を実行してください。 + +CI は Linux と JDK 21 で `make lib tests CTAGS=` を実行します。 +Make のターゲットは、それらを使うテストの実行前に、第25章のネイティブランタイムと +`sys` ライブラリもビルドします。Maven の Java ビルドだけでは、これらの前提となる成果物は生成されません。 + +[章へのバックポート一覧(英語)](docs/chapter-backports.md)には、提案された小さな修正と +各章のテスト・レビューワークフローを記録しています。大規模なアーキテクチャ変更は、そこで別に管理します。 +第10〜14章は現在、メモリ全体、必要時のエイリアス分割、参照、GCM、数値型の順に構成されています。 +このメモリ表現は第25章まで引き継がれています。 + +メモリに関する章だけをビルドしてテストするには、次を実行します。 + +```sh +make lib tests release CHAPTERS="chapter10 chapter11 chapter12" +``` + +ルートの Maven リアクターはすべての章を含みます。 +第10〜14章には、共通の `graph` と `print` モジュールに依存する、移植可能な IDEA モジュール記述子があります。 +Make と直線的な履歴を作るワークフローは、番号付きの章ディレクトリを検出します。 @@ -1,4 +1,6 @@ # Simple +English | [日本語](README.ja.md) + A Simple showcase for the Sea-of-Nodes compiler IR This repo is intended to demonstrate the Sea-of-Nodes compiler IR. diff --git a/chapter01/README.ja.md b/chapter01/README.ja.md new file mode 100644 index 00000000..5f5c7f0a --- /dev/null +++ b/chapter01/README.ja.md @@ -0,0 +1,197 @@ +# 第1章:導入 + +[English](README.md) | 日本語 + +この文書は [Chapter 1: Introduction](README.md) の日本語訳です。 + +[はじめに](../README.ja.md) | +[次の章:第2章](../chapter02/README.ja.md) + +# 目次 + +1. [実装言語](#実装言語) +2. [前提](#前提) +3. [アーキテクチャ](#アーキテクチャ) +4. [ノードのグラフとしての中間表現](#ノードのグラフとしての中間表現) +5. [ノードはグラフを構成する](#ノードはグラフを構成する) +6. [ノードの種類](#ノードの種類) +7. [一意なノード ID](#一意なノード-id) +8. [Start ノード](#start-ノード) +9. [Constant ノード](#constant-ノード) +10. [Return ノード](#return-ノード) +11. [表現](#表現) +12. [参考文献](#参考文献) + +[linear](https://github.com/SeaOfNodes/Simple/tree/linear) ブランチの直線的な Git の変更履歴をたどって +[この章](https://github.com/SeaOfNodes/Simple/tree/linear-chapter01)を読むこともできます。 + +この章では、次のような単純なスクリプトをコンパイルすることを目指します。 + +``` +return 1; +``` + +`return` 文を実装します。`return` 文は引数として `expression` を受け取ります。 +上の例のように、扱える `expression` の種類は整数リテラルだけです。 + +この単純な言語を実装するために、いくつかの主要な構成要素とデータ構造を導入します。 + +この章の[言語の文法全体](docs/01-grammar.ja.md)はこちらです。 + +## 実装言語 + +実装言語は Java です。広く利用でき、多くの人に知られているため、Java を選びました。 + +## 前提 + +読者が従来の線形な中間表現を理解し、基本ブロック(Basic Block)や +制御フローグラフ(Control Flow Graph)などの用語に馴染みがあることを前提としています。 +ここではこれらの話題を説明しません。必要であれば、標準的なコンパイラの教科書を参照してください。 + +## アーキテクチャ + +言語を解析しながら、Sea of Nodes(SoN)の中間表現を直接構築します。 +抽象構文木(AST)による表現は作りません。これは SoN IR の大きな利点の1つ、 +つまり言語の解析中にさまざまな悲観的ピープホール最適化を行えることを示すためです。 +この点は[第2章](../chapter02/README.ja.md)以降でさらに詳しく説明します。 +最終的には、型解析のパスを必要とせずに、プログラム全体の型付けができるようになります。 + +### ノードのグラフとしての中間表現 + +中間表現は Node オブジェクトのグラフです。`Node` クラスは、IR グラフ内のオブジェクトの基底型です。 +`Node` クラスは、すべてのサブタイプに継承される共通機能を提供します。 +サブタイプは、それぞれに対応する意味を実装します。たとえば、AddNode は加算し、MulNode は乗算します。 + +各 `Node` は、従来の IR に現れるような「命令」を表します。 + +### ノードはグラフを構成する + +Sea of Nodes IR の中心的な考え方は、各ノードが定義と使用(def-use)の依存関係によって +ほかのノードと結ばれていることです。これは IR の非常に重要で基本的な側面なので、 +その実装方法と、グラフ図での表し方を理解することが重要です。 + +基底クラス `Node` は、そのノードへの入力となるノードのリストを保持します。 +入力とは、「使用(use)」から「定義(def)」へのエッジです。 +つまり、`B` が定義で、`A` が `B` を使用する場合、`B` には `B` から `A` への def→use エッジがあり、 +それに対応して `A` には `A` から `B` への入力の use→def エッジがあります。 + +図では、「使用」から「定義」への矢印を描きます。次はその例です。 + + + +実装では、`Node` 型は両方向のリンクを保持します。上の例では、次のようになります。 + +* `A` は `B` の「使用」なので、`B` は `A` の入力リストに含まれます。 +* 逆に、`B` は出力リストを保持し、そのリストには `A` が含まれます。 +* これらのエッジが常に対応していることが、重要な不変条件です。 + +`Node` クラスでは、次のようになっています。 + +```java +public abstract class Node { + + /** + * Inputs to the node. These are use-def references to Nodes. + * <p> + * Generally fixed length, ordered, nulls allowed, no unused trailing space. + * Ordering is required because e.g. "a/b" is different from "b/a". + * The first input (offset 0) is often a Control node. + */ + public final ArrayList<Node> _inputs; + + /** + * Outputs reference Nodes that are not null and have this Node as an + * input. These nodes are users of this node, thus these are def-use + * references to Nodes. + * <p> + * Outputs directly match inputs, making a directed graph that can be + * walked in either direction. These outputs are typically used for + * efficient optimizations but otherwise have no semantic meaning. + */ + public final ArrayList<Node> _outputs; +} +``` + +### ノードの種類 + +中間表現には、2種類のノードがあります。 + +* **制御ノード**:コンパイルしたプログラムの制御フローグラフ(CFG)を表します。 +* **データノード**:データの意味を表します。 + +この章には、次の制御ノードとデータノードが登場します。 + +| ノード名 | 種類 | 説明 | 入力 | 値 | +|----------|------|------|------|----| +| Start | 制御 | 関数の開始 | なし | まだ関数の引数がないため、現時点ではなし | +| Return | 制御 | 関数の終了を表す | 先行する制御ノードと、値を供給するデータノード | 関数の戻り値 | +| Constant | データ | 整数リテラルなどの定数を表す | なし。ただし、グラフをたどれるように Start ノードを入力に設定する | 定数の値 | + +従来の基本ブロックでは、命令は順番に実行されます。Sea of Nodes モデルでは、 +グラフのエッジとして明示されているノード間の依存関係(制御依存関係を含む)だけに基づく +スケジューリングアルゴリズムが、命令の正しい順序を決めます。 +すべての依存関係を常に参照できるため、非常に低いコスト +(ほぼ常に小さな定数時間)で多くの最適化を行えます。 + +### 一意なノード ID + +各ノードには、作成時に一意な、密に割り当てられた整数のノード ID を付けます。 +この ID はデバッグや効率的な等価性の判定、たとえばビットベクトルへの添字として役立ちます。 +ビットベクトルを使うと、循環を含む可能性のあるグラフを効率的に訪問できます。 +ノードの等価性については[第9章](../chapter09/README.ja.md)で説明します。 + +### Start ノード + +Start ノードは、関数の開始を表します。関数がまだパラメータを受け取らないため、現時点では Start に入力はありません。 +パラメータを追加すると、Start の値はタプルになり、その値を取り出すために射影(Projection)が必要になります。 +これについては[第4章](../chapter04/README.ja.md)で詳しく説明します。 + +### Constant ノード + +Constant ノードは定数値を表します。現時点で許す定数は整数リテラルだけなので、Constant は整数値を保持します。 +ほかの種類の定数を追加する際には、Constant の表現方法をリファクタリングします。 + +定数には意味上の入力はありません。ただし、グラフを順方向にたどれるようにするために、 +Start を Constant の入力に設定します。このエッジには意味上の役割はなく、 +ノードを訪問できるようにするため*だけ*に存在します。 + +Constant の値は、そこに格納されている値です。 + +### Return ノード + +Return ノードには2つの入力があります。第1の入力は制御ノードで、 +第2の入力は戻り値を供給するデータノードです。 + +この章では複数の `return` 文を使えないため、Return がプログラムを終了します。 +Stop ノードは、`if` 文を実装する[第5章](../chapter05/README.ja.md)で導入します。 + +Return の出力は、データノードから得られる値です。 + +### 表現 + +次のプログラムを可視化します。 + +``` +return 1; +``` + + + +* 制御ノードは、背景が薄い黄色の長方形で示しています。 +* 制御エッジは赤色で、データエッジより太く描いています。 +* Constant から Start へのエッジは、本当の制御エッジではないため、灰色の破線で示しています。 +* 各エッジには、`_inputs` 配列内での位置をラベルとして付けています。したがって `0` は、そのエッジが `_inputs[0]` であることを意味します。 + +## 参考文献 + +データ構造は、次の論文で説明されている内容に基づいています。 + +* [From Quads to Graphs: An Intermediate Representation's Journey](http://softlib.rice.edu/pub/CRPC-TRs/reports/CRPC-TR93366-S.pdf) +* [Combining Analyses, Combining Optimizations](https://dl.acm.org/doi/pdf/10.1145/201059.201061) +* [A Simple Graph-Based Intermediate Representation](https://www.oracle.com/technetwork/java/javase/tech/c2-ir95-150110.pdf) +* [Global Code Motion Global Value Numbering](https://courses.cs.washington.edu/courses/cse501/06wi/reading/click-pldi95.pdf) +* [EasySSA](https://www.dropbox.com/scl/fi/0ww4sgl3ynep9hhe3i4xn/EasySSA.pdf?rlkey=2cp78hzxke62flkmyneiebzoz&dl=0) +* [SeaOfNodes](https://www.dropbox.com/scl/fi/cxykfvlzsmlcatyg6rlbt/SeaOfNodes.pdf?rlkey=z6o7y3rwr6atrejilcze6r8x0&e=1&dl=0) + +これらに倣い、中間表現をオブジェクト指向のデータモデルで表します。 +以降で、この表現の詳細を説明します。 diff --git a/chapter01/README.md b/chapter01/README.md index b0950913..9738ae59 100644 --- a/chapter01/README.md +++ b/chapter01/README.md @@ -1,5 +1,7 @@ # Chapter 1: Introduction +English | [日本語](README.ja.md) + [Introduction](../README.md) | [Next: Chapter 2](../chapter02/README.md) diff --git a/chapter01/docs/01-grammar.ja.md b/chapter01/docs/01-grammar.ja.md new file mode 100644 index 00000000..86e5ab71 --- /dev/null +++ b/chapter01/docs/01-grammar.ja.md @@ -0,0 +1,32 @@ +# 第1章の文法 + +[English](01-grammar.md) | 日本語 + +この文書は [第1章の文法定義](01-grammar.md) の日本語版です。文法定義そのものは原文と同一です。 + +[第1章:導入に戻る](../README.ja.md) + +```antlrv4 +grammar SimpleLanguage; + +program + : statement EOF + ; + +statement + : 'return' expression ';' + ; + +expression + : primaryExpression + ; + +primaryExpression + : INTEGER_LITERAL + ; + +INTEGER_LITERAL + : [1-9][0-9]* + | [0] + ; +``` diff --git a/chapter01/docs/01-grammar.md b/chapter01/docs/01-grammar.md index c5ddae2c..1fe965cb 100644 --- a/chapter01/docs/01-grammar.md +++ b/chapter01/docs/01-grammar.md @@ -1,5 +1,7 @@ # Grammar for Chapter 1 +English | [日本語](01-grammar.ja.md) + ```antlrv4 grammar SimpleLanguage; diff --git a/chapter02/README.ja.md b/chapter02/README.ja.md new file mode 100644 index 00000000..fd1c5a1c --- /dev/null +++ b/chapter02/README.ja.md @@ -0,0 +1,288 @@ +# 第2章:算術演算 + +[English](README.md) | 日本語 + +この文書は [Chapter 2: Arithmetic](README.md) の日本語訳です。 + +[前の章:第1章](../chapter01/README.ja.md) | +[次の章:第3章](../chapter03/README.ja.md) + +# 目次 + +1. [中間表現の拡張](#中間表現の拡張) +2. [ピープホール最適化](#ピープホール最適化) +3. [定数畳み込みと定数伝播](#定数畳み込みと定数伝播) +4. [ピープホール最適化前のノード](#ピープホール最適化前のノード) +5. [ピープホール最適化後](#ピープホール最適化後) +6. [実例](#実例) +7. [実装上の注意](#実装上の注意) + +[linear](https://github.com/SeaOfNodes/Simple/tree/linear) ブランチの直線的な Git の変更履歴をたどって +[この章](https://github.com/SeaOfNodes/Simple/tree/linear-chapter02)を読むことも、 +前の章との[差分](https://github.com/SeaOfNodes/Simple/compare/linear-chapter01...linear-chapter02)を見ることもできます。 + +この章では言語の文法を拡張し、加算、減算、乗算、除算、単項マイナスといった算術演算を扱えるようにします。 +これにより、次のような文を書けるようになります。 + +``` +return 1 + 2 * 3 + -5; +``` + +この章の[言語の文法全体](docs/02-grammar.ja.md)はこちらです。 + +## 中間表現の拡張 + +次のノード型を追加して、ノードの種類を増やします。 + +| ノード名 | 種類 | 説明 | 入力 | 値 | +|----------|------|------|------|----| +| Add | データ | 2つの値を加算する | 2つのデータノード。値を加算する。順序は問わない | 加算の結果 | +| Sub | データ | ある値から別の値を減算する | 2つのデータノード。値を減算する。順序が重要 | 減算の結果 | +| Mul | データ | 2つの値を乗算する | 2つのデータノード。値を乗算する。順序は問わない | 乗算の結果 | +| Div | データ | ある値を別の値で除算する | 2つのデータノード。値を除算する。順序が重要 | 除算の結果 | +| Minus | データ | 値の符号を反転する | 1つのデータノード。値の符号を反転する | 単項マイナスの結果 | + +[第1章](../chapter01/README.ja.md)のノードである Start、Return、Constant は引き続き使います。 + +## ピープホール最適化 + +グラフ内のノードには、ピープホール最適化を適用できます。 +グラフの小さな部分を「のぞき穴(peephole)」から見るように調べ、 +特定のパターンが見つかれば、その部分のグラフを書き換えます。 + +構文解析中は、こうしたピープホール最適化の条件を調べて適用することが特に簡単です。 +解析したばかりの構文から作られたノードには、まだそれを使うノードがないため、 +「書き換え」の手間がかからないからです。グラフに組み込む前に、その場でノードを置き換えるだけです。 + +この置き換えによって、置き換え前のノードの入力のうち、使われなくなったものを*削除(kill)*できる場合があります。 +つまり、構文解析中に小規模なデッドコード除去を行うことになります。 + +たとえば、すでに定数 `1` と定数 `2` を解析したとします。 +続いて `Add(1,2)` を解析すると、定数の計算に対するピープホール規則が Add を定数 `3` に置き換えます。 +この時点で、使われなくなった `Add` も*削除*します。 +その結果、使われなくなった定数 `1` と `2` も再帰的に*削除*できる場合があります。 + +## 定数畳み込みと定数伝播 + +この章と次の章では、ピープホール最適化の中でも定数畳み込みと定数伝播に注目します。 +[第4章](../chapter04/README.ja.md)までは定数以外の値が登場しないため、 +ここで主に示すのは定数畳み込みです。ただし、第4章への準備として、 +この段階でコンパイラにいくつかの考え方を導入します。 + +プログラムのさまざまな箇所で、ノードの値が定数かどうかをコンパイラが把握していると便利です。 +その情報を使うと、次のような最適化を行えます。 + +* コンパイル時に式を評価し、その式を定数に置き換える。 + この考え方はさまざまな形に拡張でき、[部分評価(Partial Evaluation)](https://en.wikipedia.org/wiki/Partial_evaluation)と呼ばれます。 +* 条件分岐で常に同じ側の分岐が選ばれる場合などに、実行されず不要になったコード領域を特定する + ([第6章](../chapter06/README.ja.md))。 +* ポインタが null でないと分かっている場合に、null チェックを省略する + ([第10章](../chapter10/README.ja.md))。 +* 配列の添字が範囲内にあると分かっている場合に、範囲チェックを省略する。 +* ノードが取り得る値の集合について詳しく分かれば、さらに多くの最適化が可能になる。 + +これらを実現するには、ノードが実行時にどのような値を取り得るのかを表す方法が必要です。 +これを型(Type)と呼び、ノードに型を付与します。 + +型の付与には、2つの目的があります。 + +* ノードに対して許される操作の集合を定義する。 +* ノードが取り得る値の集合を定義する。 + +型そのものは Java のクラスで表します。Java のクラスを使うと、型の構造を簡単に表せます。 +すべての型は `Type` クラスのサブタイプです。現時点では、型の階層は次のものだけです。 + +``` +Type ++-- TypeInteger +``` + +あるノードの型に対応する値の集合は、[束(lattice)](https://en.wikipedia.org/wiki/Lattice_(order))を使うと +扱いやすくなります。ここで使う束は次の構造を持ちます。 + + + +この束の要素は、次の3種類に分かれます。 + +* 最上位の要素は「トップ(top)」で、⊤ と表します。 + ⊤ を付与することは、そのノードの値がコンパイル時定数かもしれないし、そうでないかもしれないことを意味します。 +* 中間の要素はすべて定数です。 +* 最下位の要素は「ボトム(bottom)」で、⊥ と表します。 + ⊥ を付与することは、そのノードの値がコンパイル時定数では**ない**と分かっていることを意味します。 + +ピープホール最適化の不変条件の1つは、ノードの型が常に束の*上方向*(「トップ」の方向)に移動することです。 +ピープホール最適化は*悲観的*で、より良い状態を証明できるまでは最悪の状態を仮定します。 +後の章で扱う*楽観的*な最適化では、すべてのノードをトップから始め、 +楽観的な仮定が誤りだと分かるにつれて、型を束の下方向に移動させます。 + +束はコンパイラに行わせたい主要な最適化の中核になることが多いため、 +後の章ではこの束を拡張する方法を探ります。 + +すべてのノードに `_type` フィールドを追加し、現時点で計算された最も精度の高い `Type` を格納します。 +このフィールドは、最適化処理の実行時間を線形に保つために必要です。 +また、後の章で楽観的な定数伝播 +([疎な条件付き定数伝播(Sparse Conditional Constant Propagation)](https://en.wikipedia.org/wiki/Sparse_conditional_constant_propagation))を行う際にも必要になります。 + +`Start` と `Return` にも `_type` フィールドを追加し、後にはすべての制御ノードに追加します。 +Sea of Nodes は制御とデータを区別しないからです。 +どちらのノードにも同じようにピープホール最適化が適用されます。 +これについては[第4章](../chapter04/README.ja.md)と[第5章](../chapter05/README.ja.md)から説明します。 + +束には、「meet(交わり)」や「join(結び)」という演算子とその規則など、ほかにも重要な性質があります。 +これらは[第4章](../chapter04/README.ja.md)と[第10章](../chapter10/README.ja.md)で説明します。 + +## ピープホール最適化前のノード + +次の模式図は、ピープホール最適化による畳み込み前の演算を示しています。 +パーサーは各ノードを構築した時点で最適化するので、この中間グラフ全体が保持されるわけではありません。 + +```java +return 1 + 2 * 3 + -5; +``` + + + +* 制御ノードは、背景が薄い黄色の長方形で示しています。 +* 制御エッジは赤色です。 +* Constant から Start へのエッジは、本当の制御エッジではないため、灰色の破線で示しています。 +* 各エッジには、そのノードの入力リスト内での位置をラベルとして付けています。 + +## ピープホール最適化後 + + + +## 実例 + +最適化の動作を示すために、次のコードを考えます。 + +``` +return 1+2; +``` + +これは次のように解析されます。 + +```java + var lhs = parseMultiplication(); + if (match("+")) return new AddNode(lhs, parseAddition()).peephole(); +``` + +AddNode ノードの `peephole` が呼ばれると、まず `compute` 関数を呼び出します。 + +```java + public final Node peephole( ) { + // Compute initial or improved Type + Type type = _type = compute(); +``` + +```java + public Type compute() { + if( in(1)._type instanceof TypeInteger i0 && + in(2)._type instanceof TypeInteger i1 ) { + if (i0.isConstant() && i1.isConstant()) + return TypeInteger.constant(i0.value()+i1.value()); + } + return Type.BOTTOM; + } +``` + +この例では、両方のオペランド(`1` と `2`)が定数であり、その型は `TypeInteger` のインスタンスです。 +`compute` は型を返します。両方のオペランドが定数だと分かっているので、 +式の結果を計算する定数伝播・定数畳み込みを行い、`1+2` を `3` に置き換えられます。 + +`TypeInteger` クラスは `Type` を継承していますが、その型で確認された値を格納するフィールドも持っています。 + +```java + private final long _con; +``` + +AddNode は次の構造を持ちます。 + + + +両方のオペランドの値が分かっているので、それらを加算し、 +元のノードを、式の結果だけを保持する定数ノードに置き換えられます。 + +そのためには、ノードについて計算した型がすでに定数である一方、 +ノード自体はまだ定数ノードのインスタンスではないことを検出する必要があります。 + +これは次のように判定できます。 + +```java +// Replace constant computations from non-constants with a constant node +if (!(this instanceof ConstantNode) && type.isConstant()) { + .... +} +``` + +定数畳み込みの際に、定数を表す型を返していることに注目してください。 + +```java +return TypeInteger.constant(i0.value()+i1.value()); +``` + +定数ノードは次のように作成できます。 + +```java +return new ConstantNode(type).peephole(); +``` + +これによって、ピープホールのアルゴリズムが再帰的に呼び出されます。 +結果は次のようになります。 + + + +定数値 `3` を保持する定数ノードが追加されたことに注目してください。 +残っている加算ノードは使われていないので、削除できます。 + +先に `kill` 関数を呼び出すことができます。 + +```java +// Replace constant computations from non-constants with a constant node +if (!(this instanceof ConstantNode) && type.isConstant()) { + kill(); // Kill `this` because replacing with a Constant + return new ConstantNode(type).peephole(); +``` + +これによって、取り残された加算ノードを削除します。 + +```java +public void kill( ) { + assert isUnused(); // Has no uses, so it is dead + for( int i=0; i<nIns(); i++ ) + setDef(i,null); // Set all inputs to null, recursively killing unused Nodes + + _inputs.clear(); + _type=null; // Flag as dead + assert isDead(); // Really dead now +} +``` + +`isUnused` は、現在のノードに出力(このノードを使うノード)がないことを確認します。 +この例では、return 文が加算式を使うため、通常なら出力が1つあるはずです。 +しかし、最適化は Return ノードを作る前に行われるので、 +この時点では加算ノードを使うノードはありません。 + +したがって、そのノードを問題なく削除できます。 + +```java +for(int i=0; i<nIns(); i++ ) + setDef(i,null); // Set all inputs to null, recursively killing unused Nodes + +_inputs.clear(); +_type=null; +``` + +最終的に、次のようになります。 + + + +## 実装上の注意 + +### *値*の等価性と*参照*の等価性 + +Simple の大部分では、ノードの検索(`find()` の呼び出し)に*参照*の等価性を使います。 +これが圧倒的に多いケースです。[第9章](../chapter09/README.ja.md)で初めて*値*の等価性を導入します。 +どちらの場合も、値の等価性と参照の等価性のどちらを使うかは意図的に選んでいます。 +「どちらか適当に選ぶ」ことが正しいケースは*決して*ありません。 +迷ったら文脈を確認してください。値の等価性を使うのは*大域的値番号付け(Global Value Numbering)*だけです。 +それ以外では、参照の等価性を意味しています。 diff --git a/chapter02/README.md b/chapter02/README.md index f29c1cfd..fd3046ef 100644 --- a/chapter02/README.md +++ b/chapter02/README.md @@ -1,5 +1,7 @@ # Chapter 2: Arithmetic +English | [日本語](README.ja.md) + [Previous: Chapter 1](../chapter01/README.md) | [Next: Chapter 3](../chapter03/README.md) diff --git a/chapter02/docs/02-grammar.ja.md b/chapter02/docs/02-grammar.ja.md new file mode 100644 index 00000000..4a36b68e --- /dev/null +++ b/chapter02/docs/02-grammar.ja.md @@ -0,0 +1,48 @@ +# 第2章の文法 + +[English](02-grammar.md) | 日本語 + +この文書は [第2章の文法定義](02-grammar.md) の日本語版です。 +文法定義そのものは原文と同一です。原文の見出しは「Grammar for Chapter 1」ですが、 +このファイルは第2章の算術演算の文法を定義しているため、日本語版の見出しでは第2章としています。 + +[第2章:算術演算に戻る](../README.ja.md) + +```antlrv4 +grammar SimpleLanguage; + +program + : statement EOF + ; + +statement + : 'return' expression ';' + ; + +expression + : additiveExpression + ; + +additiveExpression + : multiplicativeExpression (('+' | '-') multiplicativeExpression)* + ; + +multiplicativeExpression + : unaryExpression (('*' | '/') unaryExpression)* + ; + +unaryExpression + : ('-') unaryExpression + | primaryExpression + ; + +primaryExpression + : INTEGER_LITERAL + | '(' expression ')' + ; + +INTEGER_LITERAL + : [1-9][0-9]* + | [0] + ; +``` diff --git a/chapter02/docs/02-grammar.md b/chapter02/docs/02-grammar.md index ce875eda..762bed5d 100644 --- a/chapter02/docs/02-grammar.md +++ b/chapter02/docs/02-grammar.md @@ -1,5 +1,7 @@ # Grammar for Chapter 1 +English | [日本語](02-grammar.ja.md) + ```antlrv4 grammar SimpleLanguage; diff --git a/chapter03/README.ja.md b/chapter03/README.ja.md new file mode 100644 index 00000000..80601db4 --- /dev/null +++ b/chapter03/README.ja.md @@ -0,0 +1,167 @@ +# 第3章: 変数宣言 + +[English](README.md) | 日本語 + +この文書は [原文](README.md) の日本語訳です。 + +[前の章: 第2章](../chapter02/README.ja.md) | +[次の章: 第4章](../chapter04/README.ja.md) + +# 目次 + +1. [中間表現の拡張](#中間表現の拡張) +2. [シンボルテーブル](#シンボルテーブル) +3. [ScopeNode](#scopenode) +4. [実例](#実例) +5. [ピープホール最適化の有効化](#ピープホール最適化の有効化) + +[linear](https://github.com/SeaOfNodes/Simple/tree/linear) ブランチの直線的な Git のリビジョン履歴で [この章](https://github.com/SeaOfNodes/Simple/tree/linear-chapter03) を読み、前の章と [比較](https://github.com/SeaOfNodes/Simple/compare/linear-chapter02...linear-chapter03) することもできます。 + +この章では、変数宣言を可能にするために言語の文法を拡張します。これにより、次のように書けます。 + +``` +int a=1; +int b=2; +int c=0; +{ + int b=3; + c=a+b; +} +return c; +``` + +この章の [完全な言語文法](docs/03-grammar.ja.md) はこちらです。 + +## 中間表現の拡張 + +この章では次のノードを導入します。 + +| ノード名 | 種類 | 説明 | 入力 | 値 | +|-----------|--------------|--------------------------------|---------------------------------|-------| +| Scope | シンボルテーブル | グラフ内のスコープを表す | 変数を定義するすべてのノード | なし | + +前の章までのすべてのノードも引き続き使います。 + +## シンボルテーブル + +変数宣言とレキシカルスコープをサポートするため、パーサーにシンボルテーブルを導入します。シンボルテーブルは名前をノードに対応付けます。 + +シンボルテーブルの *スタック* を管理します。新しいレキシカルスコープが作られると、新しいシンボルテーブルをスタックにプッシュします。スコープを抜けると、スタックの最上部にあるシンボルテーブルをポップします。 + +名前を宣言すると、現在のシンボルテーブルに追加されます。名前に代入すると、最も新しいシンボルテーブル内の対応付けが更新されます。名前を参照すると、シンボルテーブル内でその対応付けを検索します。検索はレキシカルな順序で、シンボルテーブルのスタックをさかのぼります。 + +## ScopeNode + +ScopeNode は、レキシカルスコープの実装に使うシンボルテーブルのスタックをカプセル化します。 + +* レキシカルスコープ内の名前が参照するノードは、ScopeNode の入力になります。これにより ScopeNode がそれらの値の利用側となり、そのレキシカルスコープが生きている間、値も生存します。レキシカルスコープが終わると、これらの入力は取り除かれます。ピープホール最適化は死んだノードを削除し、新しいノードは生成直後には実質的に死んでいるため、これは極めて重要です。 +* 分岐を実装する [第5章](../chapter05/README.ja.md) では、分岐点で ScopeNode を複製し、分岐が合流するときにマージします。これにより、それぞれの分岐で名前を独立して更新できます。マージ地点では、異なる値になった名前に Phi ノードが必要です。 +* [第7章](../chapter07/README.ja.md) では、バックエッジからの Phi を扱う追加のロジックを導入します。 +* ScopeNode は、[第4章](../chapter04/README.ja.md) からは制御トークン、[第10章](../chapter10/README.ja.md) からはメモリ全体など、ほかの値の追跡にも使われます。これらには特別な名前の束縛を使い、それぞれの名前を分岐をまたいで追跡し、マージ地点でマージします。 +* [第10章](../chapter10/README.ja.md) では ScopeNode を拡張し、各名前の宣言された型も管理します。 + +実装上、各シンボルテーブルは `HashMap<String,Integer>` で表します。`String` は変数名で、`Integer` は ScopeNode の入力へのインデックスです。名前に対応するノードを取得するには、まずシンボルテーブルを調べ、次に ScopeNode の入力から必要なノードを取得します。シンボルテーブルのスタックは `Stack<HashMap<String,Integer>>` で表します。 + +レキシカルスコープを抜けるときは、そのスコープを表すシンボルテーブルをスタックからポップするだけでなく、対応する入力ノードも取り除きます。変数の式が Scope の対応付けのためだけに存在している場合(たとえば、その変数を使うコードをパーサーが解析する「かもしれない」ために保持していた場合)、この時点で式は死にます。 + +``` + int a=1; + { + int b=a*a*a; + } // When this scope closes and ends, b (and the cube of a) dies + return a; +``` + +可視化では、ScopeNode を独立した紫色の箱で示します。各束縛には、そのレキシカルスコープの階層をラベルとして付けます。シンボルテーブルの各変数から、グラフ内の定義ノードへ向かうエッジを示します。 + +ScopeNode はデータノードでも制御ノードでもありません。シンボルテーブルの管理と名前の検索のための補助機構であり、どこに Phi ノードが必要かを決める重要な役割を果たします。ノードとして表すことで、Sea of Nodes の定義・使用(def-use)アーキテクチャを利用して、スコープ内の値の生存性を維持できます。 + +ScopeNode に出力はありません。 + +## 実例 + +次のコードから得られる出力を示します。 + +``` +01 int a=1; +02 int b=2; +03 int c=0; +04 { +05 int b=3; +07 c=a+b; +08 } +09 return c; +``` + +* この例の解析を始めるとき、階層0のシンボルテーブルから始めます。変数 `a`、`b`、`c` をこのテーブルに登録します。 +* 4行目では、入れ子のスコープを開始します。この時点で新しいシンボルテーブルを階層1にプッシュします。 +* 5行目では、変数 `b` を宣言します。`b` を階層1のシンボルテーブルに登録し、2行目の同名の変数を隠します。 +* 7行目では、変数 `c` を更新します。変数の検索では、階層0のシンボルテーブルにある `c` が見つかります。 + +次の模式図は、定数畳み込み前の変数の束縛と算術演算を示します。コンパイラはグラフを構築しながらピープホール最適化を適用するため、この例の実際の結果は `return 4;` です。 + + + +この図は ScopeNode の束縛をスコープの階層とともに示します。`b` の2つの束縛は、異なるレキシカルスコープに属しています。 + +* 8行目で入れ子のスコープを抜けると、階層1のシンボルテーブルをポップします。 +* 9行目では、グラフは次のようになります。 + + + +この時点では、シンボルテーブルが1つだけ存在することがわかります。 + +プログラムの実行中には同じ階層のスコープが複数存在することもあります。ただし、各スコープ階層にはシンボルテーブルを1つしか保持できないため、同時に有効になるのは1つだけです。 + +たとえば、次のようになります。 + +``` +01 int a=1; +02 int b=2; +03 int c=0; +04 { +05 int b=5; +06 c=a+b; +08 } +09 { +10 int e=6; +11 c = a+e; +13 } +14 return c; +``` + +ここでは `(04-08)` と `(09-13)` の2つのスコープは同じ階層にあります。 + +```java +public final Stack<HashMap<String, Integer>> _scopes; +``` + +`_scopes` スタックは入れ子のスコープを保持します。インデックス0が最上位の(グローバル)スコープを表し、より大きなインデックスがより深い入れ子のスコープを表します。ポップすると現在のスコープを返し、外側のスコープに移ります。各インデックスと対応するスコープには、直接の1対1の対応があります。 + +最初の入れ子のスコープ(04-08): + + + +2つ目の入れ子のスコープ(09-13): + + + +* どちらの場合も、スコープの深さを表すインデックスは1です。 + +## ピープホール最適化の有効化 + +ピープホール最適化を有効にすると、パーサーはすべての式を定数に簡約できます。次の例がその様子をよく示しています。 + +``` +01 int x0=1; +02 int y0=2; +03 int x1=3; +04 int y1=4; +05 return (x0-x1)*(x0-x1) + (y0-y1)*(y0-y1); +``` + +この例のグラフを次に示します。 + + + +Return ノードのデータ値は定数 `8` です! 解析が終わって階層0のスコープが取り除かれると、`return 8` だけが残ります。 diff --git a/chapter03/README.md b/chapter03/README.md index 799d8c0a..d00caab5 100644 --- a/chapter03/README.md +++ b/chapter03/README.md @@ -1,5 +1,7 @@ # Chapter 3: Variable Declarations +English | [日本語](README.ja.md) + [Previous: Chapter 2](../chapter02/README.md) | [Next: Chapter 4](../chapter04/README.md) diff --git a/chapter03/docs/03-grammar.ja.md b/chapter03/docs/03-grammar.ja.md new file mode 100644 index 00000000..842e7cb4 --- /dev/null +++ b/chapter03/docs/03-grammar.ja.md @@ -0,0 +1,77 @@ +# 第3章の文法 + +[English](03-grammar.md) | 日本語 + +この文書は [原文](03-grammar.md) の日本語訳です。 + +> 訳注: 原文の見出しは「Grammar for Chapter 1」ですが、このファイルは第3章に属するため、訳の見出しは第3章としています。文法は原文のまま保持しています。 + + +[第3章に戻る](../README.ja.md) + +```antlrv4 +grammar SimpleLanguage; + +program + : statement+ EOF + ; + +statement + : returnStatement + | declStatement + | blockStatment + | expressionStatement + ; + + +expressionStatement + : IDENTIFIER '=' expression ';' + ; + +blockStatement + : '{' statement+ '}' + ; + +declStatement + : 'int' IDENTIFIER '=' expression ';' + ; + +returnStatement + : 'return' expression ';' + ; + +expression + : additiveExpression + ; + +additiveExpression + : multiplicativeExpression (('+' | '-') multiplicativeExpression)* + ; + +multiplicativeExpression + : unaryExpression (('*' | '/') unaryExpression)* + ; + +unaryExpression + : ('-') unaryExpression + | primaryExpression + ; + +primaryExpression + : IDENTIFIER + | INTEGER_LITERAL + | '(' expression ')' + ; + +INTEGER_LITERAL + : [1-9][0-9]* + | [0] + ; + +IDENTIFIER + : NON_DIGIT (NON_DIGIT | DIGIT)* + ; + +NON_DIGIT: [a-zA-Z_]; +DEC_DIGIT: [0-9]; +``` diff --git a/chapter03/docs/03-grammar.md b/chapter03/docs/03-grammar.md index 1d36b532..f19b8dfc 100644 --- a/chapter03/docs/03-grammar.md +++ b/chapter03/docs/03-grammar.md @@ -1,5 +1,7 @@ # Grammar for Chapter 1 +English | [日本語](03-grammar.ja.md) + ```antlrv4 grammar SimpleLanguage; diff --git a/chapter04/README.ja.md b/chapter04/README.ja.md new file mode 100644 index 00000000..1c6faa4b --- /dev/null +++ b/chapter04/README.ja.md @@ -0,0 +1,342 @@ +# 第4章: 外部からの引数と比較 + +[English](README.md) | 日本語 + +この文書は [原文](README.md) の日本語訳です。 + +[前の章: 第3章](../chapter03/README.ja.md) | +[次の章: 第5章](../chapter05/README.ja.md) + +# 目次 + +1. [中間表現の拡張](#中間表現の拡張) +2. [射影ノード](#射影ノード) +3. [可視化](#可視化) +4. [初期値](#初期値) +5. [制御が null でないのはいつか](#制御が-null-でないのはいつか) +6. [可視化についての注記](#可視化についての注記) +7. [型システムの変更](#型システムの変更) +8. [タプル型](#タプル型) +9. [$ctrl の名前の束縛](#ctrl-の名前の束縛) +10. [ピープホール最適化の追加](#ピープホール最適化の追加) +11. [ピープホール最適化の処理を追う](#ピープホール最適化の処理を追う) +12. [デッドコード除去(DCE)](#デッドコード除去dce) + +[linear](https://github.com/SeaOfNodes/Simple/tree/linear) ブランチの直線的な Git のリビジョン履歴で [この章](https://github.com/SeaOfNodes/Simple/tree/linear-chapter04) を読み、前の章と [比較](https://github.com/SeaOfNodes/Simple/compare/linear-chapter03...linear-chapter04) することもできます。 + +この章では、言語の文法を拡張して次の機能を追加します。 + +* プログラムは、外部環境から整数型の引数 `arg` を1つ受け取ります。 +* 式で比較演算子をサポートします。 +* 現在入ってくる制御ノードに対し、スコープを考慮する名前の束縛 `$ctrl` を導入します。これにより Return は Start に固定的につながるのではなく、スコープ内の `$ctrl` の束縛を追跡します。 + +この章の [完全な言語文法](docs/04-grammar.ja.md) はこちらです。 + +## 中間表現の拡張 + +この章では、新しいノードを追加し、既存のノードの一部を変更します。変更するノードと新しいノードは次のとおりです。 + +| ノード名 | 種類 | 説明 | 入力 | 値 | +|-----------|----------------|------------------------------------------------|-----------------------|----------------------------------------------------------------------------| +| MultiNode | 抽象クラス | 結果がタプルであるノード | | タプル | +| Start | 制御 | 関数の開始点。今回から MultiNode になる | | 制御トークンと `arg` データノードを含むタプル | +| Proj | データ / 制御 | MultiNode から値を取り出す射影ノード | MultiNode とインデックス | 入力 MultiNode のインデックス位置から取り出した値 | +| Bool | データ | 比較演算子の結果を表す | 2つのデータノード | 比較結果を整数値で表す。1=true、0=false | +| Not | データ | 論理否定 | 1つのデータノード | 0を1に、1を0に変換する | + +> `Proj` は制御スロットを射影するとき、すなわち `Start` の `Proj#0` のときに制御ノードです。それ以外の射影はすべてデータノードです。`ProjNode.isCFG()` を参照してください。 + +前の章までのほかのすべてのノードも引き続き使います。 + +## 射影ノード + +複数の値を返すノードから、特定のタプル要素を取り出す射影ノードを追加します。各射影ノードは、入力ノードから取り出すフィールドのインデックスを持ちます。射影ノードによって、ラベル付きの使用・定義(use-def)エッジを単純なノード参照として管理できます。 + +図では、射影ノードを、接続先ノードの内側にある長方形の箱として示します。 + +### 可視化 + + + +この図では、Start ノードの出力に対応する名前を射影ノードのラベルにしています。制御を含む MultiNode と制御ノードは、どちらも黄色です。`$ctrl` と `arg` は StartNode(`START`)の出力(利用側)であり、唯一の入力としてスロット0に MultiNode を持ちます。`label` に意味上の役割はなく、デバッグ出力で使います。 + +```java +public ProjNode(MultiNode ctrl, int idx, String label) { + super(ctrl); +``` + +```java +_scope.define(ScopeNode.CTRL, new ProjNode(START, 0, ScopeNode.CTRL).peephole()); +_scope.define(ScopeNode.ARG0, new ProjNode(START, 1, ScopeNode.ARG0).peephole()); +``` + +これらの射影ノードは、`START` MultiNode から `CTRL` と `ARG0` の型を取り出します。MultiNode は型を TypeTuple として保持します。 + +```java +public StartNode(Type[] args) { + super(); + _args = new TypeTuple(args); +``` + +型の取り出しには `_idx` を使います。 + +```java +return t instanceof TypeTuple tt ? tt._types[_idx] : Type.BOTTOM; +``` + +### 初期値 + +引数は常に与えられ、デフォルトは `TypeInteger.BOT` です。個々のテストでは、引数を指定して `new Parser` を呼ぶことで、ほかの整数値を渡すことができますし、実際にそうしています。 + +```java +Parser parser = new Parser("return arg; ", TypeInteger.constant(2)); +``` + +グラフは次のようになります。 + + + +* `$ctrl` と `arg` はどちらも最外側のシンボルテーブル、レキシカルな深さ0で定義されます。 +* 深さ1には空のグローバルスコープのシンボルテーブルを作ります。将来、ここでグローバル変数を定義します。 +* return の後では制御が死ぬため、スコープの `$ctrl` の線はありません。明示的に null を設定すると `setDef` が呼ばれてノードが削除されます(削除されたノードはプログラムの一部ではなく、可視化にも現れません)。 + +``` + ctrl(null); // Kill control +``` + +* `arg` は定数なので、`arg` 射影ノードはクラスタに含まれません。もともと `arg` は ProjNode により定義されていましたが、ピープホール最適化の呼び出しで計算された型が定数となるため、ProjNode を ConstantNode に置き換えます。 + +```java +public final Node peephole( ) { + Type type = _type = compute(); + /* type = {TypeInteger@1527} "2" */ + + // ProjNode is not a constant unlike its type. + if (!(this instanceof ConstantNode) && type.isConstant()) + /* Create Constant(2) */ + return deadCodeElim(new ConstantNode(type).peephole()); + + /* won't get called */ + ... +``` + +引数を渡さなければ、`TypeInteger.BOT` になります。 + +```java + Parser parser = new Parser("return arg; "); + ... + +public Parser(String source) { + this(source, TypeInteger.BOT); +} + +``` + +これが定数ではないことは明らかです。 + +```java +public final static TypeInteger BOT = new TypeInteger(false, 1); + /* _is_con = false */ +``` + +そのため、`arg` 射影ノードはクラスタ内に残ります。 + + + +#### 制御が null でないのはいつか + +後の章では、`$ctrl` は `If` や `Region`(さらに `Loop`)などのほかの制御ノードを指すようになります。現時点ではほかの制御フローがないため、`Return` の後には必ず null を設定します。 + +#### 可視化についての注記 + +この章以降は、定数から Start ノードへのエッジを省略します。主な目的は図の混雑を減らし、グラフのより重要な部分に読者の注意を向けることです。 + +## 型システムの変更 + +[第2章](../chapter02/README.ja.md) で型システムを導入しました。 + +ノードには型を付けています。この型の注釈には2つの目的があります。 + +* そのノードで許される演算の集合を定義すること。 +* そのノードが取りうる値の集合を定義すること。 + +[第2章](../chapter02/README.ja.md) では、特定のノードにおいて型に対応する値の集合を、[束](https://en.wikipedia.org/wiki/Lattice_(order)) として便利に表せることを述べました。 + +型そのものは Java の `Type` クラスとそのサブタイプで識別します。型の実装では、型の内部構造を扱う便利な方法として Java のクラスを使っています。ただし、Java のクラスと束の中での型の位置に関連はありません。 + +この章では、型の階層を次のように拡張します。 + +``` +Type (Enhanced - now has Control, and a global Top and Bottom) ++-- TypeInteger (Enhanced - now has Top and Bottom types) ++-- TypeTuple (New - represents multi-valued result) +``` + +拡張した束は次のようになります。 + + + +この章では、`arg` という名前のプログラム入力変数を導入します。この変数についてわかるのは、何らかの整数値だということだけで、定数かどうかはわかりません。 + +定数でない整数値を扱うため、`TypeInteger` を拡張し、これまでの定数値に加えて整数型 `IntTop` と `IntBot` も表せるようにします。 + +整数値が定数の場合と定数でない場合を持つようになったため、束に *meet* 演算子を導入する必要があります。`meet` 演算子は、整数値を組み合わせたときの結果の型を決める規則を表します。束の図では、`meet` を取る2つの要素から始めて、矢印を下向きにたどり、最初に合流する地点を見つけます。 + +| | IntBot | Con1 | Con2 | IntTop | +|--------|--------|--------|--------|--------| +| IntBot | IntBot | IntBot | IntBot | IntBot | +| Con1 | IntBot | Con1 | IntBot | Con1 | +| IntTop | IntBot | Con1 | Con2 | IntTop | + +この表の `Con1` と `Con2` は2つの異なる整数値を表し、次の規則を説明するためのものです。 + +* `IntTop` と任意の整数定数の `meet` は、その定数です。 +* `IntBot` と任意の整数値の `meet` は `IntBot` です。 +* 任意の整数とそれ自身の `meet` は、その整数です。 +* 互いに異なる2つの整数定数の `meet` は `IntBot` です。 + +現時点では、整数値を持つノードはすべて、定数か `IntBot` 整数型のどちらかです。ループの最適化を始めると、`IntTop` の値も現れるようになります。 + +### タプル型 + +タプル型にはもう少し説明が必要です。型を固定数まとめたもので、文字どおり `Type[]` です。タプルは、ほかの点では互いに無関係な型の集まりを表し、`MultiNode` から得られます。`ProjNode` は、タプルから適切な `Type` 配列要素を取り出します。StartNode は今回から、制御と `arg` の型からなる2要素の `TypeTuple`、`[ctrl, TypeInteger.INTBOT]` を生成します。 + +タプルに対する束の `meet` 演算子は、要素ごとに適用します。各配列要素で再帰的に `meet` を呼び出します。異なるサイズのタプルを組み合わせるのはコンパイラ内部エラーであり、その場合 `meet` はボトムを使います。 + +## `$ctrl` の名前の束縛 + +前の章までは、Start から Return への制御入力エッジを固定していました。この章では、そのような固定エッジを使いません。代わりに `$ctrl` という名前で、現在のスコープ内の制御ノードを追跡します。したがって、先行する制御ノードへのエッジを作る必要があるときは、現在のスコープでこの名前を検索するだけです。 + +## ピープホール最適化の追加 + +定数でない整数値を扱えるようになったので、代数式を並べ替えて定数畳み込みを可能にする追加の最適化を行えます。たとえば、次の式です。 + +``` +return 1 + arg + 2; +``` + +ここではコンパイラに `arg+3` を出力してほしいのですが、現状では次のようになります。 + + + +よりよい結果を得るには、代数的な簡約が必要です。たとえば、式を次のように並べ替えます。 + +``` +arg + (1 + 2) +``` + +これで定数畳み込みが可能になり、最終的な結果は次のようになります。式を正規形にすることで、よく使われるアドレス計算の定数を畳み込み、代数的な恒等式を取り除き、全般的にコードを簡単にします。 + + + +この章で導入するピープホール最適化を次に示します。後の章ではさらに追加します。 + +| 変更前 | 変更後 | 説明 | +|-----------------------|-----------------------|------------------------------------------------| +| (arg + 0 ) | arg | 0の加算の恒等式 | +| (arg - 0 ) | arg | 0の減算の恒等式 | +| (arg * 1 ) | arg | 1の乗算の恒等式 | +| (con + arg) | (arg + con) | 畳み込みを促すため定数を右に移す | +| (con * arg) | (arg * con) | 畳み込みを促すため定数を右に移す | +| (con1 + (arg + con2)) | (arg + (con1 + con2)) | 畳み込みを促すため定数を右に移す | +| ((arg1 + con) + arg2) | ((arg1 + arg2) + con) | 畳み込みを促すため定数を右に移す | +| (arg + arg) | (arg * 2) | 積の和の形式 | +| (arg - arg) | 0 | 同じ値の減算は0 | +| (0 - arg) | (-arg) | 0からの減算は符号反転 | +| (arg1 - (-arg2)) | (arg1 + arg2) | 符号反転した値の減算は加算 | +| ((-arg1) - arg2) | -(arg1 + arg2) | 符号反転した値からの減算は和の符号反転 | +| (con / 1) | con | 1による除算 | +| (arg == arg) | 1 | 同じ値の比較 | +| (arg != arg) | 0 | 同じ値の比較 | +| (arg < arg) | 0 | 同じ値の比較 | +| (arg > arg) | 0 | 同じ値の比較 | +| (arg <= arg) | 1 | 同じ値の比較 | +| (arg >= arg) | 1 | 同じ値の比較 | + +## ピープホール最適化の処理を追う + +この章で導入するピープホール最適化は、すべて局所的です。解析中に新しいノードを作ったとき、そのノードがまだどこからも使われていない段階で実行します。 + +たとえば、単項式を解析してその式のノードを作ると、新しく作った `MinusNode` に対して `peephole()` を呼び出します。 + +```java +private Node parseUnary() { + if (match("-")) return new MinusNode(parseUnary()).peephole(); + return parsePrimary(); +} +``` + +もう1つの例として、この章で導入した比較演算子の解析を示します。 + +```java +private Node parseComparison() { + var lhs = parseAddition(); + if (match("==")) return new BoolNode.EQNode(lhs, parseComparison()).peephole(); + if (match("!=")) return new BoolNode.NENode(lhs, parseComparison()).peephole(); + if (match("<=")) return new BoolNode.LENode(lhs, parseComparison()).peephole(); + if (match("<" )) return new BoolNode.LTNode(lhs, parseComparison()).peephole(); + if (match(">=")) return new BoolNode.GENode(lhs, parseComparison()).peephole(); + if (match(">" )) return new BoolNode.GTNode(lhs, parseComparison()).peephole(); + return lhs; +} +``` + +`peephole()` は基底の `Node` クラスで実装しているため、`Node` のすべてのサブクラスが共有します。 + +```java +public final Node peephole( ) { + // Compute initial or improved Type + Type type = _type = compute(); + + // Replace constant computations from non-constants with a constant node + if (!(this instanceof ConstantNode) && type.isConstant()) + return deadCodeElim(new ConstantNode(type).peephole()); + + // Ask each node for a better replacement + Node n = idealize(); + if( n != null ) // Something changed + // Recursively optimize + return deadCodeElim(n.peephole()); + + return this; // No progress +} +``` + +ピープホールメソッドは次の処理を行います。 + +* ノードの型を計算します。 +* 型が定数で、ノードが ConstantNode でなければ、ConstantNode に置き換え、新しい定数に対してピープホール最適化を再帰的に呼び出します。 +* そうでなければ、`idealize()` を呼び、ノードによりよい置き換えを求めます。「よりよい置き換え」とは、`(1+2)` を `3` に、`1+(x+2)` を `(x+(1+2))` にするようなものです。 +* `idealize()` の処理内容を決めるのは、各 Node サブタイプの責任です。よりよい置き換えがあれば、ノードは `null` ではない値を返します。 +* `null` ではない値が返ったら何かが変わったので、もう一度 `peephole()` を呼び、その後、(置き換えられて死んだ)`this` ノードにデッドコード除去を行います。 + +`peephole()` は、次に示す `deadCodeElim()` を呼び出します。 + +## デッドコード除去(DCE) + +グラフの編集時に、ノードの利用側が1つから0になるたびに `kill` を呼びます。ピープホール最適化による置き換えや、スコープを抜けるときによく起こります。この再帰的な `kill` は、ほかの多くのコンパイラでいうデッドコード除去に相当します。ここでは、それを非常に細かな単位で呼び出します。パーサーが作った多くのノードは、次の字句を解析するより前に削除されます。 + +詳しくは、デッドコード除去の [付録](appendix/dce.ja.md#付録) を読んでください。 + +名前が示唆するとおり、`deadCodeElim` 関数は `kill` の薄いラッパーです。利用側が一時的に1から0になるだけのノードを早まって削除しないよう、`peepholeOpt` が使います。 + +```java +// m is the new Node, self is the old. +// Return 'm', which may have zero uses but is alive nonetheless. +// If self has zero uses (and is not 'm'), {@link #kill} self. +private Node deadCodeElim(Node m) { + // If self is going dead and not being returned here (Nodes returned + // from peephole commonly have no uses (yet)), then kill self. + if( m != this && isUnused() ) { + // Killing self - and since self recursively kills self's inputs we + // might end up killing 'm', which we are returning as a live Node. + // So we add a bogus extra null output edge to stop kill(). + m.addUse(null); // Add bogus null use to keep m alive + kill(); // Kill self because replacing with 'm' + m.delUse(null); // Remove bogus null. + } + return m; +} +``` + +ノード `m` にダミーの利用側を一時的に追加していることに注目してください。`m` は新しい置き換え先であり、生きているとわかっていますが、まだグラフにつないでいないため、利用側がありません。ダミーの利用側を加えることで、死んだと誤認されて削除されるのを防ぎます。 diff --git a/chapter04/README.md b/chapter04/README.md index f9a2dd36..d0335cd6 100644 --- a/chapter04/README.md +++ b/chapter04/README.md @@ -1,5 +1,7 @@ # Chapter 4: External Argument and Comparisons +English | [日本語](README.ja.md) + [Previous: Chapter 3](../chapter03/README.md) | [Next: Chapter 5](../chapter05/README.md) diff --git a/chapter04/appendix/dce.ja.md b/chapter04/appendix/dce.ja.md new file mode 100644 index 00000000..4c461277 --- /dev/null +++ b/chapter04/appendix/dce.ja.md @@ -0,0 +1,187 @@ +# 付録 + +[English](dce.md) | 日本語 + +この文書は [原文](dce.md) の日本語訳です。 + +## デッドコード除去(DCE) + +デッドコード除去は、死んだ(使われていない)ノードを取り除く処理です。 + +この章には、2種類のデッドコード除去があります。 + +* スコープをポップするときに、使われていないノードを削除する。 +* よりよい置き換えを見つけた後(`idealize()` の呼び出し後)に、古いノードを削除する。 + +どちらの方法もノードを再帰的に削除します。ただし、最初のケースは `peephole` を呼ぶかどうかにかかわらず発生します。 + +### スコープをポップするときに使われていないノードを削除する + +出力(利用側)のないノードは、使われていません。 + +### 例1 + + ``` + int a = 12; // the only output it has is the scope itself + return 1; + ``` + +`a` が使われていないことは明らかです。これから得られる、DCE なしのグラフは次のとおりです。 + + + +`a` を削除するため、スコープのポップ時に次の方法を導入します。 + + ```java + var ret = (ReturnNode) parseBlock(); + _scope.pop(); + ``` + +現時点ではプログラムの終わりは return に固定されており、解析を終えたらスコープをポップします。ポップ前のスコープの状態は次のとおりです。 + + ``` + _scope = {ScopeNode@1422} "Scope[$ctrl:null, arg:arg][a:12]" + 1 = {ProjNode@1528} "arg" + 2 = {ConstantNode@1529} "12" + _inputs = {ArrayList@1523} size = 3 + ``` + +`a` の出力: + + ``` + _outputs = {ArrayList@1544} size = 1 + 0 = {ScopeNode@1422} "Scope[$ctrl:null, arg:arg][a:12]" + ``` + +`a` の唯一の出力はスコープ自身であることに注目してください。`a` を導入するとき、スコープの入力に追加するとともに、スコープを `a` の出力に追加します(使用・定義の連鎖を思い出してください)。 + +`a` は戻り値ではないため、使われておらず、削除できます。 + +`_scope.pop`: + +```java +public void pop() { popN(_scopes.pop().size()); } +``` + +`_scope.popN`(使われていないノードを *再帰的に* 削除する処理): + +```java +void popN(int n) { + for (int i = 0; i < n; i++) { + Node old_def = _inputs.removeLast(); // start from the back + if (old_def != null && // If it exists and + old_def.delUse(this)) { // If we removed the last use, the old def is now dead + old_def.kill(); // Kill the old def + } + } +} +``` + +まず、スコープの入力をループでたどります(スコープもほかと同じノードであり、スコープのシンボルが入力であることを *思い出してください*)。`removeLast` が返すノードが null でなく、`old_def` の唯一の利用側が現在のノードなら、削除できます。 + +`old_def.delUse(this)` は、`old_def` の出力から *this* を削除します。 + +```java +protected boolean delUse( Node use ) { + Utils.del(_outputs, Utils.find(_outputs, use)); + return _outputs.isEmpty(); + } +``` + +`use` の削除後に `_outputs` が空になれば true を返します。`old_def.kill()` は再帰的な削除処理を開始します。 + +`old_def.kill()`: + +```java +public void kill( ) { + assert isUnused(); // Has no uses, so it is dead + popN(nIns()); // Set all inputs to null, recursively killing unused Nodes + _type=null; // Flag as dead + assert isDead(); // Really dead now + } +``` + +再び `popN` を呼びます。目的は、`a` を解放した結果、同様に削除できるノードを探すことです。`a` の唯一の入力は、値12の定数ノードです。ほかのノードと同様に、`ConstantNode` も最初の入力として制御ノード(`Start`)を取ります。`ConstantNode` の入力から `Start` ノードを取り除いたら、 + +``` +Node old_def = _inputs.removeLast(); +``` + +`Start` の出力から `ConstantNode`(`this`)も取り除けます。 + +``` +old_def.delUse(this) +``` + +### 例2 + +``` +int a = 12; +int b = a + 1; +return 1; +``` + +* 現在のスコープで `popN` を呼びます。 +* `b = a + 1;` に利用側がないとわかるので、削除できます。 +* kill 関数では再び `popN` を呼べますが、今回は `b = a + 1;` の入力を対象にします(再帰的な削除を始めます)。 +* 末尾から削除処理を始め、まず「1」を調べます。これには `AddNode` 以外の出力がありません。`this = AddNode` なので、`delUse` を呼ぶと「1」が死んだ状態になります。 +* 「1」を削除し、1つの入力(制御ノード)に対して `popN` を呼べます。 +* 次に「a」を調べます。この時点ではスコープと `AddNode` の2つの利用側があります。`this = AddNode` なので、出力から `AddNode` を取り除きますが、まだスコープが利用側として残るため、ここでは削除できません。 +* `a = 12` に進みます。「a」の利用側は1つ(スコープ)だけで、`this = scope` なので、容易に削除できます。 +* 前と同じように「a」の入力をたどり、制御ノードの出力からそれらを削除します。 + + + +## よりよい置き換えを見つけた後に古いノードを削除する + +次のコードを考えます。 + +``` +return (2+1)+3; +``` + +`idealize` の呼び出し後に DCE を行わなければ、次のようになります。 + + + +式 (2 + 1) + 3 に対して、より最適な置き換え値(6)を見つけた後、その値を保持する定数ノードを作りました。しかし、元の式を取り除かなかったため、構造内に不要なノードが残っています。 + +これを直すため、使われていないノードを削除する `deadCodeElim` の呼び出しを追加します。 + +```java +public final Node peephole( ) { + ... + if (!(this instanceof ConstantNode) && type.isConstant()) + return new deadCodeElim(ConstantNode(type).peephole()); + + ... + + Node n = idealize(); + if( n != null ) // Something changed + // Recursively optimize + return deadCodeElim(n.peephole()); + + ... +} +``` + +> 訳注: 上の原文コードの `return new deadCodeElim(ConstantNode(type).peephole());` は、そのままでは Java の構文として正しくありません。コードは原文どおり保持しています。章本文の対応するコードでは `return deadCodeElim(new ConstantNode(type).peephole());` となっています。 + +```java +// m is the new Node, self is the old. +// Return 'm', which may have zero uses but is alive nonetheless. +// If self has zero uses (and is not 'm'), {@link #kill} self. +private Node deadCodeElim(Node m) { + // If self is going dead and not being returned here (Nodes returned + // from peephole commonly have no uses (yet)), then kill self. + if( m != this && isUnused() ) { + // Killing self - and since self recursively kills self's inputs we + // might end up killing 'm', which we are returning as a live Node. + // So we add a bogus extra null output edge to stop kill(). + m.addUse(null); // Add bogus null use to keep m alive + kill(); // Kill self because replacing with 'm' + m.delUse(null); // Remove bogus null. + } + return m; +} +``` diff --git a/chapter04/appendix/dce.md b/chapter04/appendix/dce.md index 1f9dfa7a..10ff6bf1 100644 --- a/chapter04/appendix/dce.md +++ b/chapter04/appendix/dce.md @@ -1,4 +1,6 @@ # Appendix + +English | [日本語](dce.ja.md) ## Dead Code Elimination(DCE) Dead Code Elimination is the process or removing nodes that are dead(unused). diff --git a/chapter04/docs/04-grammar.ja.md b/chapter04/docs/04-grammar.ja.md new file mode 100644 index 00000000..b8b6bff5 --- /dev/null +++ b/chapter04/docs/04-grammar.ja.md @@ -0,0 +1,81 @@ +# 第4章の文法 + +[English](04-grammar.md) | 日本語 + +この文書は [原文](04-grammar.md) の日本語訳です。 + +> 訳注: 原文の見出しは「Grammar for Chapter 1」ですが、このファイルは第4章に属するため、訳の見出しは第4章としています。文法は原文のまま保持しています。 + + +[第4章に戻る](../README.ja.md) + +```antlrv4 +grammar SimpleLanguage; + +program + : statement+ EOF + ; + +statement + : returnStatement + | declStatement + | blockStatment + | expressionStatement + ; + + +expressionStatement + : IDENTIFIER '=' expression ';' + ; + +blockStatement + : '{' statement+ '}' + ; + +declStatement + : 'int' IDENTIFIER '=' expression ';' + ; + +returnStatement + : 'return' expression ';' + ; + +expression + : comparisonExpression + ; + +comparisonExpression + : additiveExpression (('==' | '!='| '>'| '<'| '>='| '<=') additiveExpression)* + ; + +additiveExpression + : multiplicativeExpression (('+' | '-') multiplicativeExpression)* + ; + +multiplicativeExpression + : unaryExpression (('*' | '/') unaryExpression)* + ; + +unaryExpression + : ('-') unaryExpression + | primaryExpression + ; + +primaryExpression + : IDENTIFIER + | INTEGER_LITERAL + | '(' expression ')' + ; + +INTEGER_LITERAL + : [1-9][0-9]* + | [0] + ; + +IDENTIFIER + : NON_DIGIT (NON_DIGIT | DIGIT)* + ; + +NON_DIGIT: [a-zA-Z_]; +DEC_DIGIT: [0-9]; +``` diff --git a/chapter04/docs/04-grammar.md b/chapter04/docs/04-grammar.md index 0600679e..3ea27850 100644 --- a/chapter04/docs/04-grammar.md +++ b/chapter04/docs/04-grammar.md @@ -1,5 +1,7 @@ # Grammar for Chapter 1 +English | [日本語](04-grammar.ja.md) + ```antlrv4 grammar SimpleLanguage; diff --git a/chapter05/README.ja.md b/chapter05/README.ja.md new file mode 100644 index 00000000..0fc029f4 --- /dev/null +++ b/chapter05/README.ja.md @@ -0,0 +1,347 @@ +# 第5章: If 文、Phi、Region + +[English](README.md) | 日本語 + +この文書は [原文](README.md) の日本語訳です。 + +[前の章: 第4章](../chapter04/README.ja.md) | +[次の章: 第6章](../chapter06/README.ja.md) + +# 目次 + +1. [新しいノード](#新しいノード) +2. [`IfNode`](#ifnode) +3. [`PhiNode`](#phinode) +4. [`RegionNode`](#regionnode) +5. [固定されたデータノード](#グラフ内のすべてのデータノードに制御エッジを結び付けるわけではない) +6. [`Stop` ノード](#stop-ノード) +7. [`if` 文の解析](#if-文の解析) +8. [ScopeNode に対する操作](#scopenode-に対する操作) +9. [ScopeNode の複製](#scopenode-の複製) +10. [2つの ScopeNode のマージ](#2つの-scopenode-のマージ) +11. [例1](#例1) +12. [マージ前](#マージ前) +13. [マージ後](#マージ後) +14. [最後に](#最後に) +15. [例2](#例2) +16. [例3](#例3) +17. [Phi を通して加算を押し上げる](#phi-を通して加算を押し上げる) +18. [さらにいくつかの例](#さらにいくつかの例) + +> 訳注: 原文の目次には「Recap」がありますが、対応する節がないため、この目次では省いています。 + +[linear](https://github.com/SeaOfNodes/Simple/tree/linear) ブランチの直線的な Git のリビジョン履歴で [この章](https://github.com/SeaOfNodes/Simple/tree/linear-chapter05) を読み、前の章と [比較](https://github.com/SeaOfNodes/Simple/compare/linear-chapter04...linear-chapter05) することもできます。 + +この章では、言語の文法を拡張して次の機能を追加します。 + +* `if` 文を導入します。 +* 制御フローの分岐とマージをサポートするため、新しいノード `Region` と `Phi` を導入します。 +* 戻り地点が複数になりうるため、終端を表す `Stop` ノードも導入します。 + +この章の [完全な言語文法](docs/05-grammar.ja.md) はこちらです。 + +## 新しいノード + +この章では、次の新しいノードを導入します。 + +| ノード名 | 種類 | 章 | 説明 | 入力 | 値 | +|-----------|---------|---------|----------------------------------------------------|------------------------------------------------|----------------------------------------------------| +| If | 制御 | 5 | 分岐の判定。`MultiNode` のサブタイプ | 制御ノードと条件式のデータノード | true 用と false 用の2つの値のタプル | +| Region | 制御 | 5 | 複数の制御フローのマージ地点 | マージする制御フローそれぞれの入力 | マージされた制御 | +| Phi | データ | 5 | 制御フローに応じて値を選択する phi 関数 | Region と各制御経路のデータノード | 通過した制御フローの経路に依存する | +| Stop | 制御 | 5 | プログラムの終端 | 関数のすべての Return ノード | なし | + +前の章までのすべてのノードも引き続き使います。 + +#### `IfNode` + +`If` ノードは制御とデータ(条件式)の両方を入力とし、true と false の `Proj` ノードが表す2つの制御フローのどちらかに、制御トークンを振り分けます。 + +#### `PhiNode` + +`Phi` はデータと制御の両方を読み取り、データ値を出力します。`Phi` の制御入力は `Region` ノードを指します。`Phi` のデータ入力は、その `Region` の制御入力それぞれに1つずつ対応します。`Phi` が計算する結果は、データとそれに対応する制御入力の両方に依存します。`Region` の制御入力は同時には最大1つしか有効にならず、`Phi` はその入力に対応するデータ値を通します。[^1] + +型は、データ入力の型の meet です。異なる2つの整数定数をマージすると `TypeInteger.BOT` になります。値は不明でも、整数であることは変わりません。整数の算術演算も同様に、畳み込めない場合は整数の結果型を保持します。 + +#### `RegionNode` + +この Sea of Nodes には制御フローグラフが埋め込まれています。通常の CFG と同じように、2つの基本ブロックが1つへ流れ込むマージ地点があります。`Region` ノードは、先行する各制御(ブロック)の制御を入力として受け取り、マージされた制御を出力します。[^2] + +`Return` と `If` は、どちらもスロット0に制御を取ります。`PhiNode` は、データ値をいつマージするか判断するために対応する `RegionNode` が必要なので、やはりスロット0を設定し、スロット1以降にデータ値を置きます。`RegionNode` の制御入力は `PhiNode` と対応を保つため、制御入力をスロット1以降に置きます。 + +#### グラフ内のすべてのデータノードに制御エッジを結び付けるわけではない + +通常の命令が所属する基本ブロックを追跡したければ、制御入力(常にスロット0)に制御を生成するノードを設定します。通常はこれを追跡する必要も、追跡したい理由もないので、多くのデータ演算では制御を `null` にします。そのようなデータ演算の正しさは、残りのデータ依存関係だけで決まります。その演算と、それに依存するノード、それが依存するノードは、ほとんど制御構造のないノードの「海」に存在します。 + +ノードの「海」は最適化に便利ですが、CFG のような従来の中間表現ではありません。グラフを逐次的な順序に並べ、制御依存関係を復元する方法が必要です。[第13章](../chapter13/README.ja.md) では、単純な大域的コード移動(Global Code Motion)[^3] アルゴリズムによってこれを行います。 + +#### `Stop` ノード + +`StopNode` の入力は `ReturnNode` だけです。プログラムの終端を示します。 + +## `if` 文の解析 + +`if` 文を解析すると、その地点で制御フローが分岐します。`if` 文の各部分で更新される名前を追跡し、最後にマージする必要があります。実装は *Combining Analyses, Combining Optimizations* の説明に従います。[^4] + +処理は次のとおりです。 + +1. 現在の制御トークン、すなわち `$ctrl` に対応付けられたノードと、`if` の条件式を入力として `IfNode` を作ります。 +2. `True` 分岐用(`ifT` と呼びます)と `False` 分岐用(`ifF` と呼びます)の2つの `ProjNode` を追加します。これらは `IfNode` のタプルの結果から値を取り出します。 +3. 現在の `ScopeNode` を複製します。複製した `ScopeNode` は、元と同じシンボルテーブルすべてと、同じエッジを持ちます。 +4. 制御トークンを `True` 射影ノード `ifT` に設定し、`if` 文の true 側の分岐を解析します。 +5. 複製した `ScopeNode` を現在のスコープにします。 +6. 制御トークンを `False` 射影ノード `ifF` に設定し、`else` 文があれば解析します。 +7. この時点で2つの `ScopeNode` があります。元のスコープは `True` 分岐で更新されている可能性があり、複製は `False` 分岐で更新されている可能性があります。 +8. マージ地点を表す `Region` ノードを作ります。 +9. 2つの `ScopeNode` を *マージ* します。束縛が異なる名前に `Phi` ノードを作ります。`Phi` ノードは、制御入力として `Region` ノードを、データ入力として各 `ScopeNode` からの2つのデータノードを取ります。名前の束縛を更新して `Phi` ノードを指すようにします。 +10. 最後に、制御トークンを `Region` ノードに設定し、複製した `ScopeNode` を破棄します。 + +実装は [`Parser` の `parseIf` メソッド](https://github.com/SeaOfNodes/Simple/blob/main/chapter05/src/main/java/com/seaofnodes/simple/Parser.java#L149-L185) にあります。 + +## ScopeNode に対する操作 + +上で説明したように、ScopeNode を複製し、後でマージします。実装には、確認しておく価値のある細かな点があります。 + +### ScopeNode の複製 + +ScopeNode を複製するコードを以下で示します。 + +目標は次のとおりです。 + +1. スタックのすべての階層にわたって名前の束縛を複製する。 +2. 新しい ScopeNode を、束縛されたすべてのノードの利用側にする。 +3. マージを容易にするため、複製内の定義の順序が元と同じになるようにする。 + +実装は [`scopeNode.dup()`](https://github.com/SeaOfNodes/Simple/blob/main/chapter05/src/main/java/com/seaofnodes/simple/node/ScopeNode.java#L142-L154) を参照してください。 + +### 2つの ScopeNode のマージ + +マージ地点では2つの ScopeNode をマージします。目標は次のとおりです。 + +1. 2つのノード間で束縛が変わった名前をマージする。そのような名前ごとに、元の2つのデータノードを参照する Phi ノードを作る。 +2. マージされた制御フローを表す新しい Region ノードを作る。Phi は、この Region ノードを最初の入力に持つ。 +3. マージが完了したら、複製した `ScopeNode` をエッジとともに破棄する。 + +マージのロジックでは、2つの ScopeNode が入力リスト内で束縛されたノードを同じ順序で保持していることを利用します。これは ScopeNode の複製時に保証しています。名前の最も内側の出現に対応する束縛しか変更できませんが、入力リスト内のすべてのノードを走査し、束縛が変わっていないものを単に無視します。 + +実装は [`ScopeNode.mergeScopes()`](https://github.com/SeaOfNodes/Simple/blob/main/chapter05/src/main/java/com/seaofnodes/simple/node/ScopeNode.java#L164-L173) を参照してください。 + +## 例1 + +次のコード片のグラフを示します。 + +```java +int a = 1; +if (arg == 1) + a = arg+2; +else + a = arg-3; +return a; +``` + +### マージ前 + +次の図は、`if` 文の2つの分岐を `Region` ノードでマージする直前のグラフです。 + + + +グラフに2つの `ScopeNode` があることに注目してください。一方の `$ctrl` は `True` 射影を、もう一方の `$ctrl` は `False` 射影を指します。変数 `a` は `True` 分岐では `Add` ノードに、`False` 分岐では `Sub` ノードに束縛されています。そのため、2つのスコープをマージするときに `a` に `Phi` ノードが必要です。 + +### マージ後 + +次の図は、`Region` ノードを作り、`a` の2つの定義を `Phi` ノードでマージした後のグラフです。 + + + +複製した `ScopeNode` はもう一方にマージされ、破棄されました。両方のスコープで `a` の定義が異なっていたので、`Phi` ノードを作り、`a` はそれを参照するようになりました。`Phi` ノードの入力は `Region` ノード、`True` 分岐の `Add` ノード、`False` 分岐の `Sub` ノードです。`arg` には代入されておらず、両スコープで値が同じなので、`arg` に `Phi` は不要です。 + +### 最後に + +`return` 文を解析し処理した後のグラフを示します。 + + + +## 例2 + +次のコード片を考えます。 + +```java +int b = 0; +int c = 0; +if (arg == 1) { + b = 2; + c = 1; +} +else { + b = 1; +} +return b; +``` + +`b` の値は、制御フローがどの経路を通ったかに依存します。これを解決するため、Φ(Phi)関数を挿入します。 + +``` +b = Phi(Region,2, 1); // values vary +c = Phi(Region,1, 0); // values vary, but dead +``` + +`c` の `Phi` は、`c` が定義されたスコープが死ぬときに死にます。これはマージ時ではなく、`return` の後に起こります。`return` が `b` を使うため、`b` は生き続けます。 + +if 文の解析を進める前に、スコープを複製します。 + +```java +ScopeNode fScope = _scope.dup(); +/* Scope[$ctrl:$ctrl, arg:arg][b:0, c:0]*/ +``` + +続いて、if 文の最初の分岐を解析します。 + +```java +// Parse the true side +ctrl(ifT); // set ctrl token to ifTrue projection +parseStatement(); // Parse true-side +ScopeNode tScope = _scope; + +/* Scope[$ctrl:True, arg:arg][b:2, c:1]*/ +``` + +この分岐では、シンボルテーブルに存在する両方のシンボルを変更し、新しい値を設定したことに注目してください。ここで、スコープを最初の状態(`ifT` が行った変更を含まない状態)に戻します。 + +```java +_scope = fScope; +``` + +続いて、if 文の2つ目の分岐を解析します。 + +```java +if (matchx("else")) { + parseStatement(); + fScope = _scope; + + /* Scope[$ctrl:False, arg:arg][b:1, c:0]*/ +} +``` + +両方の分岐が `b` を変更しましたが、`c` を変更したのは true 側だけです。分岐の各部分もレキシカルスコープなので、現在の(外側の)スコープに新しいシンボルを導入することはできません。そのため、名前の束縛の順序は変わりません。最初の制御ノードを除いてループし、同じ名前の束縛に対応するノードを比較します。異なる場合は、この違いを表す phi ノードを作ります。 + +```java + /* in(i); 2 */ + /* that.in(i); 1 */ + if( in(i) != that.in(i) ) // No need for redundant Phis + setDef(i, new PhiNode(ns[i], r, in(i), that.in(i)).peephole()); +``` + + + +## 例3 + +Phi は、次の例が示すピープホール最適化を実装しています。 + +```java +int a=arg==2; +if( arg==1 ) +{ + a=arg==3; +} +return a; +``` + +次の2つの図は値の書き換えに焦点を当てています。変わらない `if` と Region は省略しています。Phi の入力1は true 側、入力2は false 側です。コンパイラのグラフでは、Phi は引き続き同じ Region を使います。 + +ピープホール最適化前: + + + +ピープホール最適化後: + + + +共通の演算子を外へ引き出し、Phi ノードをオペランドだけに適用しました。`==` の第2オペランドだけが変わり、第1オペランド(`arg`)は同じままであることに注目してください。実装は [`PhiNode.idealize()`](src/main/java/com/seaofnodes/simple/node/PhiNode.java) にあります。 + +全体を通じて `r` が同じ Region を表すとすると、グラフの記法では次のようになります。 + +```text +Phi(r, a+b, c+d) -> Phi(r, a, c) + Phi(r, b, d) +``` + +これを、Phi を通して **演算を下へ引き下げる** と呼びます。共通の演算を、流入する経路からマージ後に移します。新しい各 Phi は、同じ経路から対応するオペランドを選びます。上の `arg` のようにオペランドが共有されていれば、その Phi はそのオペランドに簡約されます。 + +## Phi を通して加算を押し上げる + +逆方向に移すと定数畳み込みの機会が現れることがあります。たとえば、次のコードです。 + +```java +int x = 1; +if( arg ) x = 2; +return (arg+x)+3; +``` + +マージによって `x` は定数の Phi になります。`x` 自体は定数ではありませんが、取りうる値それぞれへの3の加算はコンパイル時に計算できます。加算のピープホール最適化は式の結合を変え、流入する経路へ **加算を上に押し上げます**。 + +```text +(arg + Phi(r, 2, 1)) + 3 + -> arg + Phi(r, 2+3, 1+3) + -> arg + Phi(r, 5, 4) +``` + +この実装は、`(x + Phi(constants)) + constant` の形に対する [`AddNode.idealize()`](src/main/java/com/seaofnodes/simple/node/AddNode.java) として、ここで初めて登場します。最後のオペランドが別の定数の Phi でも構いません。ただし、対応する各分岐が同じ経路を表すよう、両方の Phi は **同じ Region** を使わなければなりません。 + +なぜすべての演算を Phi の向こうへ押し上げないのでしょうか。それでは流入する経路間で処理が重複し、引き下げるピープホール最適化がすぐに元に戻す可能性があります。2つの書き換えを繰り返すと、いつまでも終わりません。ここでは、結果の Phi を作る **前に** 新しい加算がすべて定数に畳み込まれます。その Phi が下へ引き戻せる新しい加算は残らず、実行時の加算が1つ消えます。畳み込めることという条件が、この最適化の利点と、逆向きの書き換えの組が停止する理由の両方を与えます。 + +## さらにいくつかの例 + +次の2つの単純な `if` の例でも、結果の値を強調するために制御フローを省略しています。Phi の入力は、上と同じ true/false の順序を保ちます。 + +```java +int c = 3; +int b = 2; +if (arg == 1) { + b = 3; + c = 4; +} +return c; +``` + + + +```java +int a=arg+1; +int b=arg+2; +if( arg==1 ) + b=b+a; +else + a=b+1; +return a+b; +``` + + + +入れ子の分岐では、各 Phi に流入する経路を明確にするため、制御経路と Region を示します。 + +```java +int a=1; +if( arg==1 ) + if( arg==2 ) + a=2; + else + a=3; +else if( arg==3 ) + a=4; +else + a=5; +return a; +``` + + + +[^1]: Click, C. (1995). + Combining Analyses, Combining Optimizations, 132. + +[^2]: Click, C. (1995). + Combining Analyses, Combining Optimizations, 129. + +[^3]: Click, C. (1995). + Combining Analyses, Combining Optimizations, 86. + +[^4]: Click, C. (1995). + Combining Analyses, Combining Optimizations, 102-103. diff --git a/chapter05/README.md b/chapter05/README.md index 1f7c240e..b706c45d 100644 --- a/chapter05/README.md +++ b/chapter05/README.md @@ -1,5 +1,7 @@ # Chapter 5: If Statement, Phi, and Region +English | [日本語](README.ja.md) + [Previous: Chapter 4](../chapter04/README.md) | [Next: Chapter 6](../chapter06/README.md) diff --git a/chapter05/docs/05-grammar.ja.md b/chapter05/docs/05-grammar.ja.md new file mode 100644 index 00000000..84c60522 --- /dev/null +++ b/chapter05/docs/05-grammar.ja.md @@ -0,0 +1,88 @@ +# 第5章の文法 + +[English](05-grammar.md) | 日本語 + +この文書は [原文](05-grammar.md) の日本語訳です。 + +> 訳注: 原文の見出しは「Grammar for Chapter 1」ですが、このファイルは第5章に属するため、訳の見出しは第5章としています。文法は原文のまま保持しています。 + + +[第5章に戻る](../README.ja.md) + +```antlrv4 +grammar SimpleLanguage; + +program + : statement+ EOF + ; + +statement + : returnStatement + | declStatement + | blockStatment + | expressionStatement + | ifStatement + ; + +ifStatement + : 'if' '(' expression ')' statement ('else' statement)? + ; + + +expressionStatement + : IDENTIFIER '=' expression ';' + ; + +blockStatement + : '{' statement+ '}' + ; + +declStatement + : 'int' IDENTIFIER '=' expression ';' + ; + +returnStatement + : 'return' expression ';' + ; + +expression + : comparisonExpression + ; + +comparisonExpression + : additiveExpression (('==' | '!='| '>'| '<'| '>='| '<=') additiveExpression)* + ; + +additiveExpression + : multiplicativeExpression (('+' | '-') multiplicativeExpression)* + ; + +multiplicativeExpression + : unaryExpression (('*' | '/') unaryExpression)* + ; + +unaryExpression + : ('-') unaryExpression + | primaryExpression + ; + +primaryExpression + : IDENTIFIER + | INTEGER_LITERAL + | 'true' + | 'false' + | '(' expression ')' + ; + +INTEGER_LITERAL + : [1-9][0-9]* + | [0] + ; + +IDENTIFIER + : NON_DIGIT (NON_DIGIT | DIGIT)* + ; + +NON_DIGIT: [a-zA-Z_]; +DEC_DIGIT: [0-9]; +``` diff --git a/chapter05/docs/05-grammar.md b/chapter05/docs/05-grammar.md index 8eba1581..86c5b007 100644 --- a/chapter05/docs/05-grammar.md +++ b/chapter05/docs/05-grammar.md @@ -1,5 +1,7 @@ # Grammar for Chapter 1 +English | [日本語](05-grammar.ja.md) + ```antlrv4 grammar SimpleLanguage; diff --git a/chapter06/README.ja.md b/chapter06/README.ja.md new file mode 100644 index 00000000..73e660c9 --- /dev/null +++ b/chapter06/README.ja.md @@ -0,0 +1,166 @@ +# 第6章: If のピープホール最適化 + +[English](README.md) | 日本語 + +この文書は [原文](README.md) の日本語訳です。 + +[前の章: 第5章](../chapter05/README.ja.md) | +[次の章: 第7章](../chapter07/README.ja.md) + +# 目次 + +1. [型システムの改訂](#型システムの改訂) +2. [`if` のピープホール最適化](#if-のピープホール最適化) +3. [`Region` と `Phi`](#region-と-phi) +4. [考察](#考察) +5. [定数式以外へのピープホール最適化の拡張](#定数式以外へのピープホール最適化の拡張) +6. [支配ノード](#支配ノード) + +[linear](https://github.com/SeaOfNodes/Simple/tree/linear) ブランチの直線的な Git のリビジョン履歴で [この章](https://github.com/SeaOfNodes/Simple/tree/linear-chapter06) を読み、前の章と [比較](https://github.com/SeaOfNodes/Simple/compare/linear-chapter05...linear-chapter06) することもできます。 + +この章では新しい言語機能は追加しません。目標は、`if` 文にピープホール最適化を追加することです。 + +## 型システムの改訂 + +整数の部分束に加えて、別の束構造を導入するよう型システムを拡張する必要があります。これまでと同じく、「トップ」を T、「ボトム」を ⊥ と表記します。 + +* 「Ctrl」は生きた制御を、「~Ctrl」は死んだ制御を表します。 +* 「INT」は整数値を表します。整数値は独自の部分束を持つことに注意してください。 + +| | ⊥ | Ctrl | ~Ctrl | T | INT | +|-------|---|------|-------|-------|-----| +| T | ⊥ | Ctrl | ~Ctrl | T | INT | +| Ctrl | ⊥ | Ctrl | Ctrl | Ctrl | ⊥ | +| ~Ctrl | ⊥ | Ctrl | ~Ctrl | ~Ctrl | ⊥ | +| ⊥ | ⊥ | ⊥ | ⊥ | ⊥ | ⊥ | + +* 「トップ」と任意の型の meet は、その型です。 +* 「ボトム」と任意の型の meet は、「ボトム」です。 +* 「トップ」「ボトム」「Ctrl」「~Ctrl」は単純型に分類します。 +* 「Ctrl」と「~Ctrl」の meet は「Ctrl」です。 +* 上記で扱っていない場合、単純型と非単純型の meet の結果は「ボトム」です。 + +改訂した束の全体を次に示します。 + + + +## `if` のピープホール最適化 + +* 基本方針は、一方の分岐が死んでいるとわかっていても、`if` 文全体の解析を続けることです。 +* この方法では死んだノードを作り、デッドコード除去で片付けます。解析時に特別なロジックを導入せず、通常のグラフ構築の仕組みで死んだノードを片付けられるという利点があります。 +* `If` ノードをピープホール最適化するとき、条件が定数かどうかを調べます。定数なら、それが `0` かどうかによって、どちらか一方の分岐が死にます。 +* この場合、`Proj` ノードを追加する時点で一方の射影が死んでいるとわかっています。その `Proj` ノードのピープホール最適化では、`Proj` を、死んだ制御を示す「~Ctrl」型の `Constant` に置き換えます。 +* この時点では `If` ノードに利用側がないため、デッドコード除去はそれを削除しようとします。しかし解析を続ける必要があるので、まだ削除できません。重要なのは、*両方の `Proj` ノードを作った後なら `If` ノードを削除できる* ということです。その後の制御フローから見えるのは `Proj` ノードだけだからです。 +* これを保証するため、最初の `Proj` を作る前に `If` にダミーの利用側を追加し、作成直後に取り除きます。こうして2つ目の `Proj` が `If` のデッドコード除去を引き起こせるようにします。 +* 生きた `Proj` は `If` の親に、死んだ `Proj` は「~Ctrl」に置き換えます。 +* その後、解析は通常どおり続きます。 +* ほかに変わるのは、死んだ制御を伴ってマージ地点に到達したときです。 +* 同じ方針に従い、`Phi` ノードの作成も含めて通常どおりマージします。 +* `Region` の入力の1つが「~Ctrl」になっている場合があります。`Phi` はこれを見て自身を最適化します。生きた入力が1つだけなら、`Phi` のピープホール最適化は自身を残りの入力に置き換えます。 +* 最後に `Region` ノードをピープホール最適化するとき、生きた入力が1つしかなく、Phi の利用側もないとわかれば、その1つの生きた入力に置き換えられます。 + +## `Region` と `Phi` + +維持する不変条件の1つは、`Region` の各制御入力が、すべての `Phi` のデータ入力と1対1に対応することです。したがって、`Region` が制御入力を失ったら、各 `Phi` の対応するデータ入力も削除しなければなりません。逆に、依存する `Phi` がなくなるまでは Region を簡約できません。 + +`If` を処理するときは `Region` の制御入力を取り除かず、死んだ制御入力に単に `Constant(~Ctrl)` を設定します。`Phi` のピープホールロジックはそれに気付き、自身を生きた入力に置き換えます。 + +## 考察 + +上記の変更により、`if` 文の条件がコンパイル時に `true` または `false` に評価される場合、ピープホール最適化でデッドコードを畳み込めるようになります。 + +次の単純な例を見てみましょう。 + +```java +if( true ) return 2; +return 1; +``` + +ピープホール最適化前: + + + +ピープホール最適化後: + + + +別の例: + +```java +int a=1; +if( true ) + a=2; +else + a=3; +return a; +``` + +ピープホール最適化前: + + + +ピープホール最適化後: + + + +## 定数式以外へのピープホール最適化の拡張 + +`CFG` のピープホール最適化は、式がコンパイル時の定数ならデッドコードを簡約します。しかし、次の例では可能な最適化を十分に行えていません。 + +```java +int a = 0; +int b = 1; +if( arg ) { + a = 2; + if( arg ) { b = 2; } + else b = 3; +} +return a+b; +``` + +この例では `arg` が外部で定義されるため、定数ではないと仮定しなければなりません。ただし、内側の `if` 文で `if( arg )` という条件を繰り返しています。外側の `if( arg )` が true なら、*内側の `if` も必ず true に評価される* とわかります。 + +現在のピープホール最適化には、これが見えません。 + + + +ピープホール最適化に、内側の `if` が外側の `if` によって完全に決定されると認識させ、コードを次のように簡単にしたいのです。 + +```java +int a = 0; +int b = 1; +if ( arg ) { + a = 2; + b = 2; +} +return a+b; +``` + +つまり、最終結果は、`arg` が true なら `4`、false なら `1` です。 + +## 支配ノード + +このピープホール最適化を実装する鍵は、内側の `if` が外側の `if` によって完全に決定されるという観察です。コンパイラの用語では、外側の `if` が内側の `if` を支配するといいます。 + +この種の支配関係を判定する一般的な方法は、支配木(dominator tree)を構築することです。その詳細は論文 `A Simple, Fast Dominance Algorithm` を参照してください。[^1] + +ただし、支配木の構築は高コストで、解析の途中では行いたくありません。幸い、まだループがないので、制御フローグラフにバックエッジもありません。制御フローグラフは本質的に階層的で、木のような形です。そのため、単純な方法で支配ノードを特定できます。 + +基本的な考えは、各制御ノードに、階層内での深さを表す `depth` を付けることです。必要に応じて、この深さを漸進的に計算できます。階層の深い位置にあるノードは、上にあるノードより大きな深さを持ちます。 + +次に、`Region` 以外の制御ノードには入ってくる制御エッジが1つしかないので、ほとんどの制御ノードには直接支配ノード(immediate dominator)が1つだけあるとわかります。`Region` ノードは制御エッジを1本以上持ちうるため異なります。ただし `if` 文の場合、`Region` ノードは `if` の各分岐から1つずつ、ちょうど2つの制御入力を持ちます。この `if` に限った状況で `Region` ノードの直接支配ノードを見つけるには、各分岐の直接支配ノードをたどり、両方の分岐を支配する共通の祖先ノードを見つけます。 + +この祖先ノードも `if` で、条件が現在の `if` と同じなら、現在の `if` の結果は完全に決定されるとわかります。 + +この確認を行うコードは、[`If` の `compute` メソッド](https://github.com/SeaOfNodes/Simple/blob/main/chapter06/src/main/java/com/seaofnodes/simple/node/IfNode.java#L38-L45) にあります。 + +`Node` の `idom` メソッドは、深さに基づく直接支配ノードの計算のデフォルト実装を提供します。`Region` は [これをオーバーライド](https://github.com/SeaOfNodes/Simple/blob/main/chapter06/src/main/java/com/seaofnodes/simple/node/RegionNode.java#L57-L74) し、上記のより複雑な探索を実装します。 + +上の例を簡約するには、[`Add` ノードにさらにピープホール最適化を追加](https://github.com/SeaOfNodes/Simple/blob/main/chapter06/src/main/java/com/seaofnodes/simple/node/AddNode.java#L64-L67) する必要もあるとわかります。 + +これらすべての変更によって、ピープホール最適化の結果は望んでいたとおりになります! + + + +[^1]: Cooper, K. D. Cooper, Harvey, T. J., and Kennedy, K. (2001). + A Simple, Fast Dominance Algorithm. diff --git a/chapter06/README.md b/chapter06/README.md index 14e21bbc..3a7c7e09 100644 --- a/chapter06/README.md +++ b/chapter06/README.md @@ -1,5 +1,7 @@ # Chapter 6: Peepholes for If +English | [日本語](README.ja.md) + [Previous: Chapter 5](../chapter05/README.md) | [Next: Chapter 7](../chapter07/README.md) diff --git a/chapter06/docs/06-grammar.ja.md b/chapter06/docs/06-grammar.ja.md new file mode 100644 index 00000000..fba9379a --- /dev/null +++ b/chapter06/docs/06-grammar.ja.md @@ -0,0 +1,86 @@ +# 第6章の文法 + +[English](06-grammar.md) | 日本語 + +この文書は [原文](06-grammar.md) の日本語訳です。 + + +[第6章に戻る](../README.ja.md) + +```antlrv4 +grammar SimpleLanguage; + +program + : statement+ EOF + ; + +statement + : returnStatement + | declStatement + | blockStatment + | expressionStatement + | ifStatement + ; + +ifStatement + : 'if' '(' expression ')' statement ('else' statement)? + ; + + +expressionStatement + : IDENTIFIER '=' expression ';' + ; + +blockStatement + : '{' statement+ '}' + ; + +declStatement + : 'int' IDENTIFIER '=' expression ';' + ; + +returnStatement + : 'return' expression ';' + ; + +expression + : comparisonExpression + ; + +comparisonExpression + : additiveExpression (('==' | '!='| '>'| '<'| '>='| '<=') additiveExpression)* + ; + +additiveExpression + : multiplicativeExpression (('+' | '-') multiplicativeExpression)* + ; + +multiplicativeExpression + : unaryExpression (('*' | '/') unaryExpression)* + ; + +unaryExpression + : ('-') unaryExpression + | primaryExpression + ; + +primaryExpression + : IDENTIFIER + | INTEGER_LITERAL + | 'true' + | 'false' + | '(' expression ')' + ; + +INTEGER_LITERAL + : [1-9][0-9]* + | [0] + ; + +IDENTIFIER + : NON_DIGIT (NON_DIGIT | DIGIT)* + ; + +NON_DIGIT: [a-zA-Z_]; +DEC_DIGIT: [0-9]; +``` diff --git a/chapter06/docs/06-grammar.md b/chapter06/docs/06-grammar.md index 9ed7b160..2dbde035 100644 --- a/chapter06/docs/06-grammar.md +++ b/chapter06/docs/06-grammar.md @@ -1,5 +1,7 @@ # Grammar for Chapter 6 +English | [日本語](06-grammar.ja.md) + ```antlrv4 grammar SimpleLanguage; diff --git a/chapter07/README.ja.md b/chapter07/README.ja.md new file mode 100644 index 00000000..9b0c7747 --- /dev/null +++ b/chapter07/README.ja.md @@ -0,0 +1,229 @@ +# 第7章: While 文 + +[English](README.md) | 日本語 + +この文書は [原文](README.md) の日本語訳です。 + +[前の章: 第6章](../chapter06/README.ja.md) | +[次の章: 第8章](../chapter08/README.ja.md) + +# 目次 + +1. [バックエッジの扱い](#バックエッジの扱い) +2. [新しいノード型](#新しいノード型) +3. [詳細な手順](#詳細な手順) +4. [可視化](#可視化) +5. [入れ子のループ](#入れ子のループ) +6. [If を内包するループ](#if-を内包するループ) + +[linear](https://github.com/SeaOfNodes/Simple/tree/linear) ブランチの直線的な Git のリビジョン履歴で [この章](https://github.com/SeaOfNodes/Simple/tree/linear-chapter07) を読み、前の章と [比較](https://github.com/SeaOfNodes/Simple/compare/linear-chapter06...linear-chapter07) することもできます。 + +この章では `while` 文を導入します。 + +この章の [完全な言語文法](docs/07-grammar.ja.md) はこちらです。 + +## バックエッジの扱い + +ループ構造によって生じる難しさは、反復時に変数がループ本体へ戻ってくることです。たとえば、次のコードです。 + +```java +while(arg < 10) { + arg = arg + 1; +} +return arg; +``` + +変数 `arg` にはループ本体内で新しい値が代入され、その値がループ本体へ戻ります。 + +一般に、ループ構造を次のように書き換えます。 + +```java +loop_head: +if( !(arg < 10) ) + goto loop_exit; +arg = arg + 1; +goto loop_head; + +loop_exit: +``` + +これは説明のためのもので、この言語にはラベルや `goto` 文はありません。 + +SSA[^1] の観点では、`arg` が戻ってくるため、先頭に `phi` ノードが必要です。概念的には、次のような結果にしたいのです。 + +```java +// arg1 represents incoming arg +// +loop_head: +arg2 = phi(arg1, arg3); +if( !(arg2 < 10) ) + goto loop_exit; +arg3 = arg2 + 1; +goto loop_head; + +loop_exit: +``` + +`arg2` の phi が `arg3` を参照することに注目してください。`while` ループの条件を解析する時点では、`arg3` はまだわかりません。常に SSA 形式である Sea of Nodes グラフを正しく構築するために解決すべき問題の核心は、ここにあります。 + +[第5章](../chapter05/README.ja.md) を思い出してください。`if` 文の解析では、`if` の条件を過ぎるときにシンボルテーブルを複製します。後で2組のシンボルテーブルを `Region` でマージし、`if` 文の2つの分岐内で定義が変わったすべての名前に phi を作ります。`if` 文の場合、マージする定義がすでにわかっているマージ地点で phi を作ります。 + +ループ構造に対する基本的な考えは、ループ本体内でどの名前が再定義されるかわからないため、ループの `if` 条件に入る *前に*、シンボルテーブル内のすべての名前に対して積極的に phi を作ることです。ループが終わると、さかのぼって不要な phi を削除します。これを「eager phi(先行生成する phi)」方式と呼びます。[^2] + +[第8章](../chapter08/README.ja.md) では、名前の再定義に遭遇したときだけ phi を作る「lazy phi(遅延生成する phi)」方式を実装します。 + +## 新しいノード型 + +ノードの一覧は [第5章](../chapter05/README.ja.md) と同じですが、必要な追加ロジックの一部をより適切にカプセル化するため、`Region` のサブタイプ `Loop` を作ります。Loop と Phi のピープホール最適化では、バックエッジが必要な書き換えを、ループ本体の解析が完了するまで延期します。ほかのノードは通常どおり最適化を続けます。 + +## 詳細な手順 + +1. まず、`Region` の新しいサブクラス `Loop` を作ります。`Loop` は2つの制御入力を取ります。最初は入口、すなわち `$ctrl` の現在の束縛です。2つ目(`null`)は、ループ解析後に設定するバックエッジのためのプレースホルダーです。バックエッジの欠如は、Loop と対応する Phi が未完成であることを示します。そのため、それらのピープホール最適化は、各書き換えに必要な入力がそろうまで待ちます。 + + ```java + ctrl(new LoopNode(ctrl(),null).peephole()); + ``` + + 新しく作った Region が現在の制御になります。 + +2. 現在の Scope ノードを複製します。スコープ内のすべての階層のシンボルを複製し、`$ctrl` の束縛を除くすべてのシンボルに phi を作ります。 + + ```java + // Make a new Scope for the body. + _scope = _scope.dup(true); + ``` + + `dup` の呼び出しに引数 `true` を渡すことに注目してください。これが phi の作成を引き起こします。`dup()` メソッドで phi を作るコードを次に示します。 + + ```java + // boolean loop=true if this is a loop region + + String[] names = reverseNames(); // Get the variable names + dup.add_def(ctrl()); // Control input is just copied + for( int i=1; i<nIns(); i++ ) { + if ( !loop ) { dup.add_def(in(i)); } + else { + // Loop region + // Create a phi node with second input as null - to be filled in + // by endLoop() below + dup.add_def(new PhiNode(names[i], ctrl(), in(i), null).peephole()); + // Ensure our node has the same phi in case we created one + set_def(i, dup.in(i)); + } + } + ``` + + `else` ブロック内では、複製したスコープと元のスコープの両方に同じ phi ノードを設定します。 + +3. 次に、通常の if とほぼ同じように `if` 条件を設定します。 + + ```java + // Parse predicate + var pred = require(parseExpression(), ")"); + // IfNode takes current control and predicate + IfNode ifNode = (IfNode)new IfNode(ctrl(), pred).<IfNode>keep().peephole(); + // Setup projection nodes + Node ifT = new ProjNode(ifNode, 0, "True" ).peephole(); + ifNode.unkeep(); + Node ifF = new ProjNode(ifNode, 1, "False").peephole(); + ``` + +4. 現在のスコープをもう一度複製します。これはループ終了後に生き続ける出口スコープなので、出口スコープの `$ctrl` を `False` 射影に設定します。出口スコープは、ループの条件式の副作用も取り込みます。 + + ```java + // The exit scope, accounting for any side effects in the predicate + var exit = _scope.dup(); + exit.ctrl(ifF); + ``` + +5. 制御を `True` 射影に設定し、ループ本体を解析します。 + + ```java + // Parse the true side, which corresponds to loop body + ctrl(ifT); // set ctrl token to ifTrue projection + parseStatement(); // Parse loop body + ``` + +6. ループ本体の解析後、先ほど作ったすべての phi に戻って処理します。 + + ```java + // The true branch loops back, so whatever is current control gets + // added to head loop as input + head.endLoop(_scope, exit); + ``` + + `endLoop` メソッドは、ループ Region の2つ目の制御をバックエッジの制御に設定します。続いてすべての phi をたどり、それぞれの2つ目のデータ入力をループ本体の対応する項目に設定します。使われなかった phi はピープホール最適化され、元の入力に置き換わります。 + + ```java + Node ctrl = ctrl(); + ctrl.set_def(2,back.ctrl()); + for( int i=1; i<nIns(); i++ ) { + PhiNode phi = (PhiNode)in(i); + assert phi.region()==ctrl && phi.in(2)==null; + phi.set_def(2,back.in(i)); + // Do an eager useless-phi removal + Node in = phi.peephole(); + if( in != phi ) + phi.subsume(in); + } + ``` + +7. 最後に、最初の元のスコープ(head)と、本体用に作った複製の両方を削除します。出口では false の制御が現在の制御(手順4)であり、出口スコープを現在のスコープに設定します。 + + ```java + // At exit the false control is the current control, and + // the scope is the exit scope after the exit test. + return (_scope = exit); + ``` + +### 可視化 + +上の例の途中の状態を次に示します。 + + + +* ループ先頭、出口、本体の3つの Scope を示しています。それぞれの `$ctrl` の束縛は Loop、False、True を指します。 + +最終的なグラフは次のようになります。 + + + +## より複雑な例 + +### 入れ子のループ + +```java +int sum = 0; +int i = 0; +while(i < arg) { + i = i + 1; + int j = 0; + while( j < arg ) { + sum = sum + j; + j = j + 1; + } +} +return sum; +``` + + + +### If を内包するループ + +```java +int a = 1; +int b = 2; +while(a < 10) { + if (a == 2) a = 3; + else b = 4; +} +return b; +``` + + + +[^1]: Cytron, R. et al (1991). + Efficiently computing static single assignment form and the control dependence graph, in ACM Transactions on Programming Languages and Systems, 13(4):451-490, 1991. + +[^2]: Click, C. (1995). + Combining Analyses, Combining Optimizations, 103. diff --git a/chapter07/README.md b/chapter07/README.md index 5a49d3ef..95c66cda 100644 --- a/chapter07/README.md +++ b/chapter07/README.md @@ -1,5 +1,7 @@ # Chapter 7: While Statement +English | [日本語](README.ja.md) + [Previous: Chapter 6](../chapter06/README.md) | [Next: Chapter 8](../chapter08/README.md) diff --git a/chapter07/docs/07-grammar.ja.md b/chapter07/docs/07-grammar.ja.md new file mode 100644 index 00000000..ccc08250 --- /dev/null +++ b/chapter07/docs/07-grammar.ja.md @@ -0,0 +1,93 @@ +# 第7章の文法 + +[English](07-grammar.md) | 日本語 + +この文書は [原文](07-grammar.md) の日本語訳です。 + +> 訳注: 原文の見出しは「Grammar for Chapter 6」ですが、このファイルは第7章に属するため、訳の見出しは第7章としています。文法は原文のまま保持しています。 + + +[第7章に戻る](../README.ja.md) + +```antlrv4 +grammar SimpleLanguage; + +program + : statement+ EOF + ; + +statement + : returnStatement + | declStatement + | blockStatment + | expressionStatement + | ifStatement + | whileStatement + ; + +whileStatement + : 'while' '(' expression ')' statement + ; + +ifStatement + : 'if' '(' expression ')' statement ('else' statement)? + ; + + +expressionStatement + : IDENTIFIER '=' expression ';' + ; + +blockStatement + : '{' statement+ '}' + ; + +declStatement + : 'int' IDENTIFIER '=' expression ';' + ; + +returnStatement + : 'return' expression ';' + ; + +expression + : comparisonExpression + ; + +comparisonExpression + : additiveExpression (('==' | '!='| '>'| '<'| '>='| '<=') additiveExpression)* + ; + +additiveExpression + : multiplicativeExpression (('+' | '-') multiplicativeExpression)* + ; + +multiplicativeExpression + : unaryExpression (('*' | '/') unaryExpression)* + ; + +unaryExpression + : ('-') unaryExpression + | primaryExpression + ; + +primaryExpression + : IDENTIFIER + | INTEGER_LITERAL + | 'true' + | 'false' + | '(' expression ')' + ; + +INTEGER_LITERAL + : [1-9][0-9]* + | [0] + ; + +IDENTIFIER + : NON_DIGIT (NON_DIGIT | DIGIT)* + ; + +NON_DIGIT: [a-zA-Z_]; +DEC_DIGIT: [0-9]; +``` diff --git a/chapter07/docs/07-grammar.md b/chapter07/docs/07-grammar.md index 21c5ad0a..51ef8ebe 100644 --- a/chapter07/docs/07-grammar.md +++ b/chapter07/docs/07-grammar.md @@ -1,5 +1,7 @@ # Grammar for Chapter 6 +English | [日本語](07-grammar.ja.md) + ```antlrv4 grammar SimpleLanguage; diff --git a/chapter08/README.ja.md b/chapter08/README.ja.md new file mode 100644 index 00000000..73a5ea1b --- /dev/null +++ b/chapter08/README.ja.md @@ -0,0 +1,264 @@ +# 第8章: 遅延生成する Phi、Break、Continue、評価器 + +[English](README.md) | 日本語 + +この文書は [原文](README.md) の日本語訳です。 + +[前の章: 第7章](../chapter07/README.ja.md) | +[次の章: 第9章](../chapter09/README.ja.md) + +# 目次 + +1. [Phi の遅延生成](#phi-の遅延生成) +2. [`continue` 文](#continue-文) +3. [`break` 文](#break-文) +4. [パーサーでの考慮事項](#パーサーでの考慮事項) +5. [例](#例) + - [例1](#例1) + - [例2](#例2) + - [例3](#例3) +6. [Sea of Nodes グラフ評価器](#sea-of-nodes-グラフ評価器) + +[linear](https://github.com/SeaOfNodes/Simple/tree/linear) ブランチの直線的な Git のリビジョン履歴で [この章](https://github.com/SeaOfNodes/Simple/tree/linear-chapter08) を読み、前の章と [比較](https://github.com/SeaOfNodes/Simple/compare/linear-chapter07...linear-chapter08) することもできます。 + +この章では次のことを行います。 + +* ループ内の `Phi` の作成を、先行生成から遅延生成へ移します。 +* `break` 文と `continue` 文のサポートを追加します。 +* Sea of Nodes グラフ評価器を実装します。 + +この章の [完全な言語文法](docs/08-grammar.ja.md) はこちらです。 + +## Phi の遅延生成 + +[第7章](../chapter07/README.ja.md) では、ループ先頭にすべての変数の `PhiNode` を先行して作成しました。この方法では不要な Phi も作りますが、冗長な Phi は後で削除されます。 + +この章では、`PhiNode` の遅延生成をサポートします。 + +`PhiNode` を遅延生成する際の主な難しさは、Phi を作るとき、先行生成していた場合に作られるはずだった最も内側のループ先頭のスコープで作らなければならないことです。`if` 文と `while` 文ではスコープを複製するため、単に現在のスコープに Phi を作るわけにはいきません。 + +`PhiNode` を遅延生成する最も簡単な方法は、名前の検索にその処理を組み込むことだとわかります。しかし、名前を参照するとき、Phi を作る必要があるとどう判断するのでしょうか。特定の名前の対応付けを、検索時に `PhiNode` に変換すべきだと示す仕組みが必要です。 + +次のように実現します。 + +* `ScopeNode.dup()` で、ループ先頭のために Scope を複製する場合、名前の定義を元のノードに設定する代わりに、Scope 自身に設定します。これが番兵(sentinel)として機能します。 + + ```java + // For lazy phis on loops we use a sentinel + // that will trigger phi creation on update + dup.addDef(loop ? this : in(i)); + ``` + +* その後、検索で名前にアクセスするとき、定義が Scope ノードを指していることを検出し、その時点で `PhiNode` を作ります。対応する実装は `ScopeNode.update()` にあります。 + + ```java + if( old instanceof ScopeNode loop ) { + // Lazy Phi! + old = loop.in(idx) instanceof PhiNode phi && loop.ctrl()==phi.region() + // Loop already has a real Phi, use it + ? loop.in(idx) + // Set real Phi in the loop head + // The phi takes its one input (no backedge yet) from a recursive + // lookup, which might have inserted a Phi in every loop nest. + : loop.setDef(idx,new PhiNode(name,loop.ctrl(),loop.update(name,null,nestingLevel),null).peephole()); + setDef(idx,old); + } + ``` + +* スコープをマージするときは、定義に直接アクセスする代わりに名前で検索し、正しい内側のスコープ階層で遅延生成する `PhiNode` が作られるようにします。 + + ```java + if( in(i) != that.in(i) ) // No need for redundant Phis + // If we are in lazy phi mode we need to do a lookup + // by name as it will trigger a phi creation + setDef(i, new PhiNode(ns[i], r, this.lookup(ns[i]), that.lookup(ns[i])).peephole()); + ``` + +* 最後に `ScopeNode.endLoop()` で残った番兵を片付け、ループ先頭からの入力に置き換えます。 + + ```java + for( int i=1; i<nIns(); i++ ) { + if( back.in(i) != this ) { + PhiNode phi = (PhiNode)in(i); + assert phi.region()==ctrl && phi.in(2)==null; + phi.setDef(2,back.in(i)); + // Do an eager useless-phi removal + Node in = phi.peephole(); + if( in != phi ) + phi.subsume(in); + } + if( exit.in(i) == this ) // Replace a lazy-phi on the exit path also + exit.setDef(i,in(i)); + } + ``` + +## `continue` 文 + +第7章では、ループ末尾から先頭に戻るバックエッジは1本だけでした。 + +`continue` 文の追加により、ループ先頭に戻るバックエッジが複数になることがあります。 + +このバックエッジの実装には、いくつかの方法があります。 + +1. 従来の方法では、各 `continue` のバックエッジをループ先頭にマージします。Phi はエッジの数だけ入力を必要とします。欠点は、Loop が単純な入れ子構造ではなくなることです。これにより、より多くのループ不変式を外に持ち上げるためにループを「部分的に巻き戻す」など、多くの種類のピープホールによるループ最適化が難しくなります。 +2. 別の方法は、すべての `continue` を1つのマージ地点(`Region`)に集め、continue の `Region` からループ先頭へ流れるバックエッジを1本作ることです。 +3. 3つ目の方法は、continue の `Region` を1つ作る代わりに、そのスタックを作ることです。この方法では、Phi を現在と同じ2入力の単純な形に保てる利点があります。ほかの方法では Phi に3つ以上の入力が必要です。一方、`continue` 文と同じ数の continue `Region` と、それに対応する積み重なった Phi ができることになります。 + +この章では、基本的な `while` ループのアーキテクチャに最も簡単に実装できる方法3を採用します。[^1] + +実装ではスコープを慎重に扱う必要があります。`continue` 用の `PhiNode/RegionNode` を必要な場合だけ生成したいからです。 + +[基本的なループのアーキテクチャ](../chapter07/README.ja.md) では、ループのバックエッジに到達した時点ですべての入れ子のブロックやスコープを抜けています。現在のスコープと制御を Loop のスコープと制御につなぎ、必要に応じて `PhiNode` を作れば十分です。 + +しかし、`continue` は入れ子のスコープ(入れ子の `if` 文など)の中にも現れます。continue の `Region` を遅延生成したいので、最初に `continue` に遭遇したら、先頭スコープを複製し、現在のスコープをそこへマージして、continue の Scope と `Region` を生成する必要があります。その後、別の `continue` に遭遇したら、新しい `Region` を作り、それまでの continue Scope と現在の Scope をマージします。 + +実装は、上記の方法の変形です。 + +* 最初の `continue` によって現在のスコープを複製します。先頭スコープより深い入れ子のレキシカルスコープを切り詰め、先頭スコープでは見えなかった名前の束縛を取り除きます。これは本質的に、入れ子のスコープを「抜ける」ことを保証しています。 + + continue スコープの初期値を null にします。 + + ```java + // No continues yet + _continueScope = null; + ``` + + continue を解析するとき、最初は `_continueScope` が null です。 + + ```java + private Node parseContinue() { return (_continueScope = require(jumpTo( _continueScope ),";")); } + + private ScopeNode jumpTo(ScopeNode toScope) { + ScopeNode cur = _scope.dup(); + ctrl(new ConstantNode(Type.XCONTROL).peephole()); // Kill current scope + // Prune nested lexical scopes that have depth > than the loop head + // We use _breakScope as a proxy for the loop head scope to obtain the depth + while( cur._scopes.size() > _breakScope._scopes.size() ) + cur.pop(); + // If this is a continue then first time the target is null + // So we just use the pruned current scope as the base for the + // continue + if (toScope == null) + return cur; + // toScope is either the break scope, or a scope that was created here + assert toScope._scopes.size() <= _breakScope._scopes.size(); + toScope.mergeScopes(cur); + return toScope; + } + ``` + +* continue スコープは、後続の `continue` 文の基準となるスコープになり、こうして continue スコープと Region のスタックを形成します。 +* ループが終わった後、ループ内で continue スコープを作っていたなら、現在のスコープを continue スコープへマージし、その continue スコープを有効なスコープにする必要があります。 + + ```java + // Merge the loop bottom into other continue statements + if (_continueScope != null) { + _continueScope = jumpTo(_continueScope); + _scope.kill(); + _scope = _continueScope; + } + ``` + +## `break` 文 + +`break` の実装は `continue` より簡単です。ループ本体の解析前に出口スコープを作っており、これが初期の `break` スコープになります。`break` に遭遇したら、入れ子のレキシカルスコープを切り詰めた後、現在のスコープを現在の break スコープにマージします。その結果の新しいスコープが、有効な break スコープになります。 + +## パーサーでの考慮事項 + +`continue` や `break` の対象は、それを直接囲む `while` ループです。そのため、`continue` と `break` のスコープのスタックを管理します。`while` ループの解析前にそれまでの `continue` や `break` のスコープを保存し、解析後に復元することで実現します。 + +## 例 + +### 例1 + +```java +while(arg < 10) { + arg = arg + 1; + if (arg == 5) + continue; + if (arg == 6) + break; +} +return arg; +``` + + + +### 例2 + +```java +while(arg < 10) { + arg = arg + 1; + if (arg == 5) + continue; + if (arg == 6) + continue; +} +return arg; +``` + + + +### 例3 + +```java +while(arg < 10) { + arg = arg + 1; + if (arg == 5) + break; + if (arg == 6) + break; +} +return arg; +``` + + + +## Sea of Nodes グラフ評価器 + +評価器は、プログラムの開始点から制御フローを前向きにたどり、Return ノードに到達するか、最大ループ回数を超えるまで、与えられたグラフを直接評価します。 + +評価器は呼び出されると、最初にグラフを走査して Start ノードを見つけ、準備を行います。プログラムが特別なパラメーター `arg` を使っている場合、渡されたパラメーター値を、そのパラメーターを表すノードに束縛します。 + +この簡単な準備が済むと、Start ノードからの制御の射影をたどり、プログラムの「実行」を始められます。 + +この時点で制御フローはプログラムの最初の Region にありますが、ほかの Region とまったく同じように扱います。各 Region で行うべき処理は2つあります。まずすべての phi を具体的な値に解決し、次に制御フローの次の行き先を決めます。 + +もっと処理が必要に思えるかもしれません。しかし phi ノードを解決すれば、その Region の制御フロー経路に固有のロジックはすべて解決されます。それ以外の処理は、後で必要に応じて解決されます。プログラムの実行には、これで十分です! + +Region の後の制御フローの行き先は簡単に見つけられます。Region を使う制御ノードは1つだけだからです。そのため、Region の利用側の中からそのノードを探します。 + +そのノードを見つけたら、到達した制御フローノードの種類によって動作を変えます。可能性は `RegionNode`、`IfNode`、`ReturnNode` の3つだけです。別の `RegionNode` なら、同じ処理を繰り返します。`ReturnNode` なら、戻り値の式を解決してプログラムの値を返せます。最後に `IfNode` の場合、まず判定式を解決し、true(射影0)または false(射影1)の分岐を選択します。`IfNode` のどちらの射影を選んでも、その射影を通って、それを使う制御ノード(`RegionNode`)に進みます。 + +上ではノードを「解決する」と述べましたが、それは何を意味するのでしょうか。ノードは、値を持つ(たとえば定数)か、すでに解決されて値がキャッシュされているか、あるいは自身が使うノードに対して演算を行います。最後の場合、その入力ノードを解決する必要があります。入力が解決されたら、それらに対してノードの演算を行えます。たとえば `AddNode` の値は、2つの入力の和です。値を解決したら、その値を返します。 + +これで、ループのない単純なプログラムを評価できます。 + +ループもほぼこのままで動作し、少しだけ追加の処理が必要です。制御が `LoopNode` を通るたびに、評価器はループの反復回数を使い切っていないか確認します。使い切っていたら、タイムアウトを示す RuntimeException を投げます。 + +最後に、ループ Region の `PhiNode` の値を計算するときは、どのキャッシュ値も更新する前に、すべての新しい値を計算するよう注意が必要です。ループの反復では、すべての `PhiNode` の値が同じ時点の値になるようにしなければならないからです。新しい値をすべて計算し終えたら、それらのキャッシュを一度に更新できます。これを説明するため、次のコードを考えます。 + +```java +int t = 0; +while(arg < 10) { + t = arg; + arg = arg + 1; +} +return t; +``` + +このコードでは、グラフはループ内の2つの文をどちらの順序でも評価できるため、`t` に以前の `arg` の値を設定する前に、`arg` の新しい値をキャッシュしないようにする必要があります。 + +この評価方法は、命令選択、レジスタ割り当て、コード生成を気にせずプログラムを実行できるため、テストに便利です。通常のピープホール最適化で生成したグラフを実行します。後の章では、異なるワークリストの順序で最適化したグラフを評価器で比較し、結果の不一致を検出できます。この章のファザーは、生成したプログラムについて、コンパイラと評価器の失敗をチェックします。 + +``` java +arg=arg+arg; +arg=arg+arg; +... +arg=arg+arg; +return arg; +``` + +一方、このエンジンは特に高速になるようには設計していません! すべての式をキャッシュするわけではないため、直前の例のように実行時間が指数的になるプログラムを作ることができます。 + +[^1]: 方法2も試しました。この別の解法を見たい読者のため、その実装を持つ別のブランチを用意しています。 diff --git a/chapter08/README.md b/chapter08/README.md index f412fd36..54e64cde 100644 --- a/chapter08/README.md +++ b/chapter08/README.md @@ -1,5 +1,7 @@ # Chapter 8: Lazy Phis, Break, Continue, and Evaluator +English | [日本語](README.ja.md) + [Previous: Chapter 7](../chapter07/README.md) | [Next: Chapter 9](../chapter09/README.md) diff --git a/chapter08/docs/08-grammar.ja.md b/chapter08/docs/08-grammar.ja.md new file mode 100644 index 00000000..ef5a68b7 --- /dev/null +++ b/chapter08/docs/08-grammar.ja.md @@ -0,0 +1,101 @@ +# 第8章の文法 + +[English](08-grammar.md) | 日本語 + +この文書は [原文](08-grammar.md) の日本語訳です。 + + +[第8章に戻る](../README.ja.md) + +```antlrv4 +grammar SimpleLanguage; + +program + : statement+ EOF + ; + +statement + : returnStatement + | declStatement + | blockStatment + | expressionStatement + | ifStatement + | whileStatement + | breakStatement + | continueStatment + ; + +whileStatement + : 'while' '(' expression ')' statement + ; + +breakStatement + : 'break' ';' + ; + +continueStatement + : 'continue' ';' + ; + +ifStatement + : 'if' '(' expression ')' statement ('else' statement)? + ; + + +expressionStatement + : IDENTIFIER '=' expression ';' + ; + +blockStatement + : '{' statement+ '}' + ; + +declStatement + : 'int' IDENTIFIER '=' expression ';' + ; + +returnStatement + : 'return' expression ';' + ; + +expression + : comparisonExpression + ; + +comparisonExpression + : additiveExpression (('==' | '!='| '>'| '<'| '>='| '<=') additiveExpression)* + ; + +additiveExpression + : multiplicativeExpression (('+' | '-') multiplicativeExpression)* + ; + +multiplicativeExpression + : unaryExpression (('*' | '/') unaryExpression)* + ; + +unaryExpression + : ('-') unaryExpression + | primaryExpression + ; + +primaryExpression + : IDENTIFIER + | INTEGER_LITERAL + | 'true' + | 'false' + | '(' expression ')' + ; + +INTEGER_LITERAL + : [1-9][0-9]* + | [0] + ; + +IDENTIFIER + : NON_DIGIT (NON_DIGIT | DIGIT)* + ; + +NON_DIGIT: [a-zA-Z_]; +DEC_DIGIT: [0-9]; +``` diff --git a/chapter08/docs/08-grammar.md b/chapter08/docs/08-grammar.md index 00f93e04..0c1d05da 100644 --- a/chapter08/docs/08-grammar.md +++ b/chapter08/docs/08-grammar.md @@ -1,5 +1,7 @@ # Grammar for Chapter 8 +English | [日本語](08-grammar.ja.md) + ```antlrv4 grammar SimpleLanguage; diff --git a/chapter09/README.ja.md b/chapter09/README.ja.md new file mode 100644 index 00000000..a0fe23c3 --- /dev/null +++ b/chapter09/README.ja.md @@ -0,0 +1,190 @@ +# 第9章: 大域的値番号付けと反復的なピープホール最適化 + +[English](README.md) | 日本語 + +この文書は [原文](README.md) の日本語訳です。 + +[前の章: 第8章](../chapter08/README.ja.md) | +[次の章: 第10章](../chapter10/README.ja.md) + +# 目次 + +1. [ピープホール最適化の基盤づくり](#ピープホール最適化の基盤づくり) +2. [大域的値番号付け](#大域的値番号付け) +3. [解析後の反復的なピープホール最適化](#解析後の反復的なピープホール最適化) +4. [遠くの隣接ノード](#遠くの隣接ノード) +5. [今後の課題](#今後の課題) +6. [GVN による共通部分式](#gvn-による共通部分式) + - [例1](#例1) + - [例2](#例2) +7. [解析後の反復最適化](#解析後の反復最適化) + +> 訳注: 原文の目次の「Other Concerns」は、本文では「Future Work」という見出しです。日本語の目次は本文の見出しに合わせています。 + +[linear](https://github.com/SeaOfNodes/Simple/tree/linear) ブランチの直線的な Git のリビジョン履歴で [この章](https://github.com/SeaOfNodes/Simple/tree/linear-chapter09) を読み、前の章と [比較](https://github.com/SeaOfNodes/Simple/compare/linear-chapter08...linear-chapter09) することもできます。 + +この章では次のことを行います。 + +* 大域的値番号付け(共通部分式除去)を追加します。 +* 解析後にピープホール最適化を不動点まで反復する最適化を追加します。 + +この章に言語の変更はなく、最適化だけを追加します。 + +## ピープホール最適化の基盤づくり + +第9章の目標は、ピープホール最適化を「仕上げる」ことです。これ以上ピープホール最適化が登場しないという意味ではありません。むしろ、今後もたくさん登場します。ここで作るのは、それらを整理し、安全で扱いやすく実装し、不動点に到達するまで適用するための基盤です。 + +本当の目標は、新しいピープホール最適化を恐れずに追加できるようにすることです。間違っていれば、すぐにアサーションで失敗します。正しければ、新しい最適化はほかのすべての最適化と一緒に動き、お互いに効果を高め合います。 + +## 大域的値番号付け + +大域的値番号付け(GVN)は、従来のハッシュテーブルを使い、ノードのオペコードと入力からハッシュを計算します。2つのノードが同じ入力に同じ処理を行うなら、結果も同じなので、共有できます。 + +* Node にグローバルな静的ハッシュテーブル `GVN` を追加します。 +* `Node.equals` を追加します。2つのノードは、同じ入力に対し同じオペコード、すなわち同じ Java クラスを持つ場合に等価です。 +* ノードのオペコード(代用としてノード名の文字列)と入力に基づく `hashCode` を追加します。 +* `ConstantNode` などいくつかのノードでは、追加の固有情報が必要です。定数17と99は同じではありませんが、どちらも同じ `ConstantNode` クラスとラベルを持ちます。`eq` と `hash` は、この追加情報を確認(またはハッシュ化)します。 + +ここでは意図的に *値の等価性* を使っていることに注意してください。Simple の多くの箇所では *参照の等価性* を使います。たとえば、ノードとエッジの管理は値の等価性ではなく参照の等価性で行い、`Utils.find()` も参照で検索します。Simple プロジェクト全体で、ノードを検索するときは、それが *値の等価性* か *参照の等価性* かに注意してください。常に明示されているわけではありませんが、文脈から明らかであるはずです。 + +グラフは頻繁に編集するため、ノードの入力が変わることがあります。それによってハッシュも変わるので、GVN テーブル内で見つからなくなります。たとえば、次の `Add` を `Con` に置き換える場合です。 + + (Mul (Add 4 8) x) // Fold the constant + (Mul 12 x) // The `Mul` changes hash and inputs + +対策として、エッジを変更するときにはノードが `GVN` テーブルに存在していないことを要求します。何らかの「エッジのロック」が必要です。「エッジがロックされている」なら、エッジの変更前にまずノードを `GVN` から取り除く必要があります。 + +* `_hash` フィールドを、キャッシュされたハッシュコードとエッジのロックの両方に使います。0ならノードはロックされておらず、`GVN` にもありません。0以外ならノードの「エッジがロックされており」、`GVN` に存在します。 +* `hashCode` 関数はキャッシュされたハッシュがあれば使い、偶然ハッシュが0になることは許しません。 +* エッジを変更する前にノードを `GVN` から取り除く `unlock()` を追加します。ノードのエッジを操作するすべての呼び出しの前に挿入します。エッジを変更するすべてのコード経路は、すでにノードを再度ピープホール最適化することを保証しているため、その再訪時にノードを `GVN` テーブルへ再挿入します。 + +この新しい `GVN` テーブルは、`peepholeOpt()`(および `peephole()`)で、ピープホール最適化するすべてのノードについて共通部分式を探すために使います。ノードのハッシュが0なら、以前の同一ノードとの一致を探します。見つかれば、その以前のノードを使い、現在のノードはデッドコード除去で削除します。見つからなければ、現在のノードを「代表インスタンス」として `GVN` に挿入します。ノードのハッシュがすでに0以外なら、それが「代表インスタンス」だとわかるので、`GVN` を検索する必要はありません。 + +この最適化全体は、`iter()` の十数行と、共有の `equals`、`hashCode`(コメントを含めてさらに約50行)で実現できます。 + +不変条件の1つは、ピープホール最適化の実行中に型が単調に上がることです。GVN はこれを一時的に破る場合があります。2つの等価なノードのうち、一方の `_type` はすでに引き上げられ、もう一方はまだかもしれません。どちらを残すかによって、「より低い」`_type` のノードを選ぶ可能性があります。この「低い」は、型の束における意味です。両方の型が正しい必要があり、両ノードの `compute()` は同じ型を返さなければなりません。その型は JOIN に等しい必要があります。対策は、2つのノードの型について(MEET ではなく)JOIN を計算することです。 + +## 解析後の反復的なピープホール最適化 + +解析後の `IterPeeps` は、ピープホール最適化を不動点、すなわち適用できる最適化がなくなるまで反復します。ピープホール最適化がコードサイズを増やすことはまれ(まったくない?)なので、これは線形時間になるはずです。グラフは何らかの尺度、通常はサイズについて単調に小さくなるはずです。MulNode やロード/ストアノードの数などが減り、より「高価な」ノードを安価なノードに置き換える場合もあります。 + +理論上、ワークリストの処理は単純です。次の項目を取って処理します。グラフが変わったら、隣接ノードをワークリストに入れます。この手順を、ワークリストが空になるまで繰り返します。 + +扱う必要のある主な問題は次のとおりです。 + +* ノードには利用側があります。あるノード群を別のものに置き換えるには、さらにグラフを組み替える必要があります。高度に難しいわけではありませんが、細かな扱いが面倒です。少数のグラフ操作ユーティリティと、ピープホール最適化の間ではグラフが安定し正しいという強い不変条件が役に立ちます。ここでは、従来の安定したノードのユーティリティを基に、`Node.subsume` が操作の大部分を行います。 +* ノードの変更は、グラフの「近傍」も変えます。隣接ノードにもピープホール最適化を適用できるか確認し、その先にも同様に続ける必要があります。ピープホール最適化やグラフ更新の後では、ノードの利用側と定義側をワークリストに入れます。 +* 強い不変条件は、すべてのノードについて、ワークリスト上にあるか、適用できるピープホール最適化がないか、のどちらかであることです。確認は簡単ですが、高コストです。通常の「不動点までのピープホール最適化の反復」は基本的に線形時間ですが、この確認は各最適化ステップで線形時間なので、全体では二次時間になります。便利なアサーションですが、アルゴリズム全体が安定したら無効にし、新しく追加したピープホール最適化が誤動作するときに再び有効にできます。このコードは `IterPeeps.iterate` の `assert progressOnList(stop);` として有効になっています。 + +## 遠くの隣接ノード + +一部のピープホール最適化では、「近傍」は直接の利用側や定義側より遠くまで及びます。そのため、より長距離の関係を扱う方法が必要です。たとえば、ある最適化が「this -> A -> B -> C」を調べ、自身を「D」に置き換えようとしても、「B」の何らかの判定で失敗する場合があります。「B」が更新されたら、「this」を再訪し、ピープホール最適化を再確認したいはずです。そこで、「B」に「this」の依存関係を記録します。「B」の更新時に、依存リスト(「this」)を「処理待ち」リストへ追加します。 + +遠くの隣接ノードの確認の例が `AddNode` にあります。`(Add (Add x 2) 1)` という重なった定数を探し、`(Add x 3)` にしたいとします。調べた結果、代わりに `(Add (Phi (Add x 2) self) 1)` が見つかったとします。`Phi` は `Add` ではないため、当然この確認はそこで打ち切られます。しかし、`(Phi y self)` は一意な入力を1つしか持たず、いずれそれ自体が `y` にピープホール最適化されます。そうなれば、重なった `Add` の最適化を適用できます。そこで、失敗した Add の最適化は `phi.addDep(Add)` を呼び、`Phi` への依存を登録します。実際に後で `Phi` が最適化されたら、その依存ノード(たとえば `Add`)を `IterPeeps` のワークリストへ戻し、重なった Add の最適化を再試行します。 + +一般的な規則は次のとおりです。 + +* **ピープホール最適化のパターンが遠くのノードを調べるなら、依存リストに登録しなければなりません!** +* `node.in(1).in(2)` を `node.in(1).in(2).addDep(node)` にします。 + +この章では、次のパターンでこれを使います。 + +* 一部の最適化は `Phi` の入力と関係します。直接の場合も間接の場合もあり、たとえば `(Add Add( (Phi x) 2 ) 1)` です。`Phi` の入力がすべて `Constant` なら最適化を進められるため、そのノードを `Phi` の入力の依存側にします。また、`Phi` の Region の依存側にもします。`Node.allCons()` と `PhiNode.allCons()` を参照してください。 +* `(Add (Add x 1) 2)` のパターンでは、`x` が定数になれば処理が進むため、外側の `Add` を `x` の依存側として追加します。`AddNode.idealize()` を参照してください。 +* `Phi` の型を計算するとき、対応する Region の該当する制御が生きていれば、`Phi` を Region の制御入力の依存側として追加します。その制御が死ねば、`Phi` にピープホール最適化を適用できるからです。`PhiNode.compute()` と `PhiNode.singleUniqueInput()` を参照してください。 + +変更点: + +* Node に `ArrayList<Node> _deps` フィールドを追加します。初期値は null です。 +* 一部のピープホール最適化(上記参照)は、遠くのノードの確認に失敗したら `distant.addDep(this)` を呼び、依存を追加します。`addDep` は `_deps` を作り、各種の重複した追加を除外します。 +* `IterPeeps` のループは、変更を見つけたら、依存側もすべてワークリストへ移します。 + +# 例 + +## GVN による共通部分式 + +これらの図は値の依存関係に焦点を当てています。通常の制御フローは省略しています。最初の例では、小さな True/False の射影で2つの Return を区別します。空の制御スロットは、コンパイラの入力の欠如ではなく、省略した文脈を表す場合があります。 + +### 例1 + +GVN がどのように共通部分式を見つけるかを示す小さな例です。 + +```java +int x = arg + arg; +if(arg < 10) { + return arg + arg; +} +else { + x = x + 1; +} +return x; +``` + +GVN を使う前には、次のグラフになります。`arg+arg` を `arg*2` に変換していることに注目してください。 + + + +GVN を追加すると、次のようになります。`arg*2` の式が2回現れる代わりに、再利用されるようになったことに注目してください。 + + + +### 例2 + +共通の式を再利用できると、さらに最適化が可能になります。単純な例を示します。 + +```java +return arg*arg-arg*arg; +``` + +GVN がなければ、ピープホール最適化は減算の両辺が同じ値だと認識できません。 + + + +* 各乗算(*)ノードは、`arg` に向かうエッジを2本持ちます。 +* 入力スロットのラベルが、同じ定義を使うこの2つの使用を区別します。 + +GVN があれば、ピープホール最適化はよりよい結果を得られます。 + + + +## 解析後の反復最適化 + +ワークリストに基づく解析後の最適化は、解析中にはできなかった最適化を可能にします。たとえば、次のコードです。 + +```java +int step = 1; +while (arg < 10) { + arg = arg + step + 1; +} +return arg; +``` + +`while` ループの解析中には、`step` が定数で、変化しないことはまだわかりません。そのため、解析後の最適化がなければ次のようになります。 + +両方の図で、ループの判定と制御のバックエッジは省略しています。Phi のデータのバックエッジは残しているため、繰り返される加算とその置き換えを比較しやすくなっています。その入力1は入口の値、入力2は次の反復の値です。 + + + +解析後のワークリストに基づく最適化を有効にすると、次のようになります。グラフのループ本体内に `arg+2` が現れたことに注目してください。 + + + +## 今後の課題 + +後の章で扱いたい問題がさらにあります。 + +* 順序を考えずにピープホール最適化を実行すると、いくつかの欠点があります。 + - 死んでいるものや、もうすぐ死ぬものを最適化してから削除する場合があります。無駄な処理です。 + - 死んだ無限ループは、ピープホール最適化の無限の循環を引き起こすことがよくあります。死んだループの「根元」を処理すれば、全体を畳み込めるのに、そこに到達できません。 + - 一部の最適化はグラフを直接小さくします。ほかの最適化はサイズを保ったまま別のものを減らします(たとえば2倍の Mul を Shift に置き換える場合)。さらに、簡約する前に一時的にグラフを大きくしようとする最適化も出てくるかもしれません。 +* したがって、まずグラフを直接小さくする最適化(デッドコード除去など)を行い、次に別のものを減らす最適化を行い、最後にそれ以外を行うことには利点があります。これはソートされたワークリストを意味しますが、順序の種類はごく限られるため、基数ソートだけで十分です。ピープホール最適化を、たとえば次のような分類に分ける必要があります。 + - 「厳密に小さくする」もの。 + - 「ノード数は同じだが、たとえば `Mul` を `Shift` に置き換える」もの。 + - 自由度を増やすもの(エッジの迂回)。 + - 「後で小さくなるため、今は大きくする」もの(インライン化はこの分類です)。 +* リストのどちらかの端から常に取り出すのを避ける利点もあります。一方または他方の端から処理すると、多くの最適化パターンが二次時間になるかもしれません。何かを変更してリストに戻すと、それがすぐに再び取り出されるためです。つまり、同じ最適化を繰り返すループに陥り、たとえば左側に連なる加算木を、少しずつ右側に連なる加算木に移すことになります。擬似ランダムな取り出しは、ランダム化によって悪い最適化パターンを避けます。 +* なぜ `IterPeeps` は、`addDeps` の仕組みを備えたワークリストを使う代わりに、全ノードを1回か2回走査するだけではないのでしょうか。定義側を利用側より先に訪問する順序(たとえば逆後順)で巡回することさえできます。 + + ループがなければ、そのような1回の走査で局所的なピープホール最適化をすべて見つけられます。実際、パーサーはすでにこれを行っています。しかし、ループやさらに遠くのケースでの機会は見つけられません。ループ周辺の最適化を得るには、再訪が必要です。各回 O(N) のコストで O(N) 回の走査を必要とするケースは簡単に作れ、アルゴリズムはすぐに二次時間になります。 + + `addDeps` の解法は、ピープホール最適化を書く際のコストをいくらか増やす代わりに、この二次時間のコストを避けます。 diff --git a/chapter09/README.md b/chapter09/README.md index 1139060b..5cb70860 100644 --- a/chapter09/README.md +++ b/chapter09/README.md @@ -1,5 +1,7 @@ # Chapter 9: Global Value Numbering and Iterative Peepholes +English | [日本語](README.ja.md) + [Previous: Chapter 8](../chapter08/README.md) | [Next: Chapter 10](../chapter10/README.md) diff --git a/chapter09/docs/09-grammar.ja.md b/chapter09/docs/09-grammar.ja.md new file mode 100644 index 00000000..f89b449b --- /dev/null +++ b/chapter09/docs/09-grammar.ja.md @@ -0,0 +1,103 @@ +# 第9章の文法 + +[English](09-grammar.md) | 日本語 + +この文書は [原文](09-grammar.md) の日本語訳です。 + +> 訳注: 原文の見出しは「Grammar for Chapter 8」ですが、このファイルは第9章に属するため、訳の見出しは第9章としています。文法は原文のまま保持しています。 + + +[第9章に戻る](../README.ja.md) + +```antlrv4 +grammar SimpleLanguage; + +program + : statement+ EOF + ; + +statement + : returnStatement + | declStatement + | blockStatment + | expressionStatement + | ifStatement + | whileStatement + | breakStatement + | continueStatment + ; + +whileStatement + : 'while' '(' expression ')' statement + ; + +breakStatement + : 'break' ';' + ; + +continueStatement + : 'continue' ';' + ; + +ifStatement + : 'if' '(' expression ')' statement ('else' statement)? + ; + + +expressionStatement + : IDENTIFIER '=' expression ';' + ; + +blockStatement + : '{' statement+ '}' + ; + +declStatement + : 'int' IDENTIFIER '=' expression ';' + ; + +returnStatement + : 'return' expression ';' + ; + +expression + : comparisonExpression + ; + +comparisonExpression + : additiveExpression (('==' | '!='| '>'| '<'| '>='| '<=') additiveExpression)* + ; + +additiveExpression + : multiplicativeExpression (('+' | '-') multiplicativeExpression)* + ; + +multiplicativeExpression + : unaryExpression (('*' | '/') unaryExpression)* + ; + +unaryExpression + : ('-') unaryExpression + | primaryExpression + ; + +primaryExpression + : IDENTIFIER + | INTEGER_LITERAL + | 'true' + | 'false' + | '(' expression ')' + ; + +INTEGER_LITERAL + : [1-9][0-9]* + | [0] + ; + +IDENTIFIER + : NON_DIGIT (NON_DIGIT | DIGIT)* + ; + +NON_DIGIT: [a-zA-Z_]; +DEC_DIGIT: [0-9]; +``` diff --git a/chapter09/docs/09-grammar.md b/chapter09/docs/09-grammar.md index 00f93e04..5f6d5209 100644 --- a/chapter09/docs/09-grammar.md +++ b/chapter09/docs/09-grammar.md @@ -1,5 +1,7 @@ # Grammar for Chapter 8 +English | [日本語](09-grammar.ja.md) + ```antlrv4 grammar SimpleLanguage; diff --git a/chapter10/README.ja.md b/chapter10/README.ja.md new file mode 100644 index 00000000..efc4f0ea --- /dev/null +++ b/chapter10/README.ja.md @@ -0,0 +1,270 @@ +# 第10章: 構造体とメモリ + +[English](README.md) | 日本語 + +[前: 第9章](../chapter09/README.ja.md) | +[次: 第11章](../chapter11/README.ja.md) + +この文書は [原文](README.md) の日本語訳です。 + +この章では、ユーザー定義の構造体、ポインタ、割り当て、フィールドのロードとストア、null チェックを追加します。また、メモリへの作用も含めてグラフを実行する評価器を導入します。[文法](docs/10-grammar.ja.md)は第10章と第11章で共通です。次の章ではメモリの表現を改善します。 + +## 参照 + +構造体は、名前と型を持つフィールドの集まりです。C や Java とよく似た、ごく伝統的なものです。新しい構造体は `new` キーワードで作ります。これはメモリを割り当て、参照を生成します。メモリの解放は後の章で扱います。フィールドへのアクセスには `.` 記法を使います。フィールドには、ほかの構造体への参照も含め、任意のスカラー値を格納でき、相互再帰も可能です。参照フィールドは通常の変数と同じように、null を許容することも禁止することもできます。 + +```java +struct Person { + String last; // Last name, must exist + String? middle; // Optional middle name + int age; +}; +``` + +ここには、null にならない final な String の `last`、null を許容する final な String の `middle`、そして `int age` があります。`int` として宣言されたすべてのフィールドと同じく、`age` は変更可能です。後の章では、変更可能な参照フィールドと不変の整数フィールドの両方を扱えるようにします。当面はこの選択で、よくある用途の大部分をカバーします。 + +フィールドにアクセスするには参照が必要です。静的フィールドは後の章で追加します。 + +フィールドへの参照には、必ず null でないポインタを使わなければなりません。これは型システムが直接チェックします。つまり Simple は、設計によって null ポインタ例外を禁止します。使用前に null をチェックする例を示します。 + +```java +print(ptr.age); // ERROR! ptr might be null + +if( ptr ) print(ptr.age); // Test before use + +// Field use can be far removed, as long as the check dominates +if( ptr ) { + S1; // Other statements + while( other ) { ...ptr.age... } +}; +``` + +割り当てとストアが加わるため、メモリへの作用を扱う必要が生じます。 + +## 1つのメモリ値 + +算術ノードは使用する値に依存します。同じように、ロードはメモリを観測する時点にも依存します。この章では、各ストアは直前のメモリ状態を消費し、次の状態を生成します。ロードはメモリを消費しますが、変更しません。これによって、ソースプログラムのメモリ順序がグラフに明示されます。 + +```java +struct Point { int x; int y; } +Point p = new Point; +p.x = 3; +p.y = arg; +return p.x; +``` + +`NewNode` はポインタを生成します。パーサーはまず両フィールドにゼロを書き込むストアを出力し、次に `x` に 3、`y` に `arg` を格納します。最後のロードは、`y` へのストア後のメモリを観測します。異なるフィールドは独立ですが、この章では意図的にこの依存関係を残します。第11章で取り除きます。 + + + +グラフは局所的な畳み込み前の依存関係を示しています。メモリに注目するため、制御エッジの大部分を省略し、合流が重要な箇所に最小限の True/False ノードだけを残しています。また、この配置は実行スケジュールではなく、依存関係を示します。 + +| ノード | 入力 | 結果 | +|---|---|---| +| `New` | 制御と構造体の型 | 新しい、null でないオブジェクトポインタ | +| `Store` | メモリ、ポインタ、フィールド名、値 | 更新されたメモリ全体の状態 | +| `Load` | メモリ、ポインタ、フィールド名 | フィールドの値 | +| `Start` | 外部からの入口 | 制御、初期メモリ、`arg` | +| `Return` | 制御、メモリ、結果 | それまでの作用をすべて保持した完了 | + +制御とメモリをともに消費または生成するノードでは、制御がスロット 0、メモリがスロット 1 を占めます。Start の出力は `{ctrl, $mem, arg}`、Return の入力は `{ctrl, $mem, result}` です。 + +メモリエッジは依存関係であり、ヒープのコピーではありません。グラフは合法な実行順序を決めます。ハードウェアでは Store が物理メモリを直接更新します。評価器では、Store がオブジェクトに付随するメモリ配列を更新します。 + +## メモリは SSA 変数 + +パーサーは `$mem` という名前の隠れた束縛を1つ保持します。ソースの識別子に `$` は含められないため、ユーザーコードからこの名前は使えません。メモリの読み出しと更新には、局所変数と同じ `ScopeNode` の検索・更新操作を使います。 + +`if` の2つの分岐は、異なるメモリ値で終了する可能性があります。通常の Phi が、実行された分岐のメモリを選択します。 + +```java +struct Point { int x; int y; } +Point p = new Point; +if( arg ) p.x = 3; else p.x = 4; +return p.x; +``` + + + +ループには第8章の遅延 Phi 構築を再利用します。ループ内で `$mem` を読み出すか更新すると、最初は未完成のメモリ Phi を作ります。ループを閉じると、その後退エッジが与えられます。`break`、`continue`、入れ子のループ、早期 return でも、スカラー変数と同じスコープの仕組みを使います。 + +```java +struct Point { int x; int y; } +Point p = new Point; +while( arg ) { + p.x = arg; + arg = arg-1; +}; +return p.x; +``` + + + +すべての Return は現在のメモリ全体の値を消費します。返されるスカラーがストアに依存しない場合や、プログラムが変更したオブジェクト自体を返す場合でも、これによってストアが生き残ります。 + +## 拡張された型の束 + +この章では、Simple の型の束を大きく改訂します。 + + + +図では **トップ** に `⊤`、**ボトム** に `⊥` を使います。接尾辞は領域を表します。`⊤:struct` は*構造体トップ*で、単独の `⊤` は全体のトップです。本文ではこれらの言葉を使い、コード例では実際の Java 定数を使います。実装の `$TOP` と `$BOT` という名前を図に表示する必要はありません。 + +| 図 | 本文 | Java 定数 | 内部の構造体名 | +|---|---|---|---| +| `⊤:struct` | 構造体トップ | `TypeStruct.TOP` | `$TOP` | +| `⊥:struct` | 構造体ボトム | `TypeStruct.BOT` | `$BOT` | + +図のエッジは束の順序を示し、トップはボトムの上にあります。省略記号を含む箱は、さらに並列の選択肢があることを省略して示すもので、追加の共用体型ではありません。フィールドとタプル要素の型の束は省略しています。フィールドの型には任意のスカラー型(Integer または Pointer)を使え、Tuple の型には Control や Memory を含む*任意の*束の型を使えます。 + +型の束には、以下の領域があります。 + +*  は制御フローを表します。制御トップは到達不能な制御、制御ボトムは到達可能な制御です。 +*  には整数トップ、整数ボトムと、その間に並列に配置された定数があります。省略記号はほかの整数定数を表します。 +* (新規)は構造体の型と null 許容性を組み合わせます。 + * 接頭辞 `*` は、その型へのポインタを意味します。`*Person` は Person への null でないポインタです。接尾辞 `?` は null を含むため、`*Person?` は Person ポインタまたは null を許容します。 + * ポインタ領域には構造体の束全体が2つ含まれます。1つは null を含まず、もう1つは null を含みます。図は両方について名前付きの選択肢を展開しています。単に2つのポインタ型があるわけではありません。 + * `null` は構造体トップと null 許容フラグを組み合わせたもので、null でないオブジェクトの選択肢を持ちません。図では `null` とともに `*⊤:struct?` と表記しています。 + * 構造体ボトムへのポインタは任意の構造体を指せます。図では null でない形式を `*⊥:struct`、null を許容する形式を `*⊥:struct?` と示します。後者がポインタボトムで、ポインタトップは `*⊤:struct` です。 + * null は false、null でないポインタは true と評価されます。 + + > C との類推: 構造体ボトムへの null でないポインタは `void *` に似ています。ただし C の `void *` は null にもなれます。Simple はこの定数を `TypeMemPtr.VOIDPTR` と名付け、`*void` と表示します。構造体ボトム自体は、構造体の型に共通する下界であって、C の `void` 型ではありません。 + +* (新規)は、ユーザー定義の名前付き構造体を表します。この章のフィールドは整数型です。 + * 構造体トップはすべての名前付き構造体の上に、構造体ボトムはすべての名前付き構造体の下にあります。トップは高位の空の構造体の選択肢で、ボトムは保守的に任意の構造体名を含みます。 + * `Person`、`s1`、`s2`、さらにほかの名前は、並列の選択肢です。継承はないため、異なる名前の構造体は、同じフィールドを持っていても互いの下に入りません。 + * プログラムが宣言できる名前に固定の上限はありません。図の省略記号は、特別な無名構造体ではなく、さらに名前付きの選択肢があることを示します。各名前付きの箱は、それぞれのフィールド型の束を省略して表しています。 +* (新規)はメモリ全体のトークンを表します。メモリトップとメモリボトムは `TypeMem.TOP` と `TypeMem.BOT` です。どちらも個別のフィールドに格納された値を記録しません。 +*  は、Start の制御、メモリ、引数のように、1つのノードから出る結果の集まりを表します。ピンク色の見出しは長さの異なるタプルを区別します。縦に積まれた黒い `type` の各セルは、別のタプルを含む可能性も含め、型の束全体を再び表しています。 + +束には以下の操作を使います。 + +* **Meet(交わり)** は最大下界を計算します。これらの例では、入力が表す選択肢を組み合わせるものと考えてください。 + * `Person` と `s1` の meet は構造体ボトムです。この2つの名前だけを表す別の型はありません。 + * `*Person` と `*s1` の meet は、構造体ボトムへの null でないポインタです。 + * `*Person` と `null` の meet は `*Person?` です。 + * 構造体ボトムへの null でないポインタと `null` の meet はポインタボトムです。 + * `1` と `2` の meet は整数ボトムです。 +* **Dual(双対)** は束を中心線に対して反転します。トップとボトムが入れ替わり、dual を2回適用すると元の型に戻ります。 + * 構造体トップと構造体ボトムは双対です。 + * `null` と、構造体ボトムへの null でないポインタは双対です。 + * ポインタの dual は、その構造体のフィールドにも dual を適用し、null 許容性を反転します。したがって `*Person` の dual は、一般には元の `*Person?` ではありません。フィールドの型も双対にする必要があります。図ではフィールドの詳細を省略しています。 +* **Join(結び)** は最小上界を計算します。meet と dual を通して `JOIN(x,y) = MEET(x.dual(),y.dual()).dual()` と定義されます。 + * `Person` と `s1` の join は構造体トップです。 + * `*Person` と `*s1` の join はポインタトップです。 + * 構造体ボトムへの null でないポインタと `null` の join はポインタトップです。 + * `1` と `2` の join は整数トップです。 + +Sea of Nodes グラフの構築中には、値が生成された領域の中に留まることを保証します。つまり、ポインタフィールドに整数が入ることは(当然ながら)なく、汎用の `TOP` や `BOT` さえ入りません。強調しておきたい細かい点がいくつかあります。 + +* Phi を作るとき、その初期型は最初の入力の宣言型に基づきます。ほかにどんな型との meet を取るかはまだわからないため、これは悲観的な型の割り当てです。 +* Phi の入力がすべてわかったら、宣言型の局所的なトップから始め、すべての入力型の meet を計算します。この計算により、Phi の型をより精密にします。 + +## パーサーの拡張 + +これまでの章で使える型は Integer だけでした。今では変数を Integer 型にも、Struct 型へのポインタにもできます。このため、パーサーは変数の宣言型を追跡するようになりました(以前は Integer だけでした)。変数の宣言型は、その変数に合法的に代入できる値の集合を定義します。Sea of Nodes グラフは、変数に代入された値の実際の型も追跡します。これらの型の遷移は前節で説明した型の束によって定義されます。 + +## null ポインタ解析 + +Simple 言語の構文では、ポインタ変数が null を許容すると指定できます。そのうえで、null ポインタ例外が発生することを防ぎます。コンパイラは null チェックが必要かどうかを判定し、必要なのに存在しなければプログラムを拒否します(ユーザーに null チェックの挿入を求めます)。 + +パーサーは、ポインタ変数の真偽を調べる条件に遭遇すると、その知識を使って以降の分岐で型情報を絞り込みます。 + +動機となる例を2つ示します。 + +```java +struct Bar { int a; } +Bar? bar = new Bar; +if (arg) bar = null; +if( bar ) bar.a = 1; +``` + +上で `bar` が `null` でなければ、`bar.a` への代入を許可したいところです。最後の行に null チェックがなく、たとえば単に `bar.a = 1;` だった場合、`Java` はこのプログラムをコンパイルしますが NPE を投げます。Simple は null チェックが必要だと指摘して、プログラムを拒否します。 + +```java +struct Bar { int a; } +Bar? bar = new Bar; +if (arg) bar = null; +int rez = 3; +if( !bar ) rez=4; +else bar.a = 1; +``` + +`bar` が `null` なら、`!bar` を `true` と評価したいところです。else 分岐では `bar` が null でないとわかるため、`bar.a` への代入は許可されるべきです。 + +この動作を可能にするため、以下の拡張を行います。 + +* 型システムでポインタが null かどうかを追跡します。したがって、ptr 型が流れるすべての Node でも追跡します。 +* `If` ノードの述語が ptr 値なら、その述語をアップキャストで「包みます」。アップキャストは true 分岐で null の可能性を取り除き、ポインタを使えるようにします。 +* アップキャストには、構造体ボトムへの null でないポインタ(`TypeMemPtr.VOIDPTR`)への `Cast` を使います。元のポインタ型とこの型の join を取ります。これは図の `*⊥:struct` に対応します。この JOIN 操作は、ptr についてすでにわかっていることをすべて保持し、ptr が null でないという新しい知識を追加します。 +* アップキャストは `CastNode` 演算で表し、適用可能であれば現在のスコープで元の述語のすべての出現をアップキャストに置き換えます。変更は `If` の各分岐に局所的で、true 分岐と false 分岐で別々に行います(false 分岐では述語を反転します)。 +* アップキャストにも通常のピープホール最適化を適用します。入力の型がアップキャストのスーパータイプなら `CastNode` を除去できます。`ScopeNode.upcast()` メソッドと `CastNode` を参照してください。 +* Not の型を計算するとき、入力が ptr なら Integer 型を生成します。入力が `null` なら `1`、null でない ptr 値なら `0` に変換します。`NotNode.compute()` を参照してください。 + +## 局所的なメモリ最適化 + +**同じポインタとフィールド**へのストアの直後にあるロードは、格納した値を使えます。`p.x` に `arg` を書き込んだばかりなので、その読み戻しにメモリアクセスは不要です。 + +```java +struct Box { int x; } +Box p = new Box; +p.x = arg; +return p.x; // Can return arg directly. +``` + +| 変換前: 格納した値を読み戻す | 変換後: 値を直接使う | +|---|---| +|  |  | + +Return の結果入力は Load から `arg` に変わり、使われなくなった Load は消えます。Return は引き続き Store のメモリを消費するので、書き込みは保持されます。 + +同じアドレスへの別のストアの直後にあるストアは、先行するストアを必要とするほかのユーザーがなければ、その先行ストアを破棄できます。この別の書き換えにより、上の図に残っているゼロ初期化のストアも除去できます。ここでも、ポインタとフィールドの両方のチェックが重要です。 + +ロードはメモリ Phi を通って上に移動することもできます。これは[第5章の Phi を通って上に押し上げる最適化](../chapter05/README.ja.md#phi-を通して加算を押し上げる)を拡張したものです。そこでは流入経路上で算術を畳み込みましたが、ここでは対応するストアに対してロードを畳み込めます。「Phi の Load は、Load の Phi になる」。グラフの記法では次のようになります。 + +```text +Load(Phi(r, m0, m1), p.x) + -> Phi(r, Load(m0, p.x), Load(m1, p.x)) +``` + +メモリの Phi が、ロードされた値の Phi になります。このピープホール変換は Node の総数を増やすので、利益のヒューリスティックは、少なくとも1つの Load が畳み込まれることを要求します。ループの場合、この Load は**後退エッジ**でなければなりません。入口だけで畳み込むと、サイクルに沿って無限に押し上げ続けられるロードが残るためです。算術の書き換えと同じく、畳み込みを露出させることは、変換後の後片付けに期待するものではなく、グラフを変換する判断の一部です。[`LoadNode.idealize()`](src/main/java/com/seaofnodes/simple/node/LoadNode.java) を参照してください。 + +逆方向は[第5章の共通演算の引き下げ](../chapter05/README.ja.md#例3)を拡張します。「Load の Phi は、Phi の Load になる」。対応する Load や Store の Phi は、合流後の1つの演算にできます。たとえば、次の2つの書き込みは、 + +```java +if (arg) s.x = arg+1; +else s.x = arg+2; +``` + +`s.x = Phi(arg+1,arg+2)` になり、最終的には `s.x = arg+Phi(1,2)` にできます。一般的な書き換えでは、メモリやポインタも含め、異なる各オペランドについて正しく型付けされた別々の Phi を作ります。メモリ演算にはさらに注意が必要です。型の精度が失われる可能性があるので、それも利益判定の一部としてチェックします。 + +また、Store を移動したときに元の作用が消えるように、Store にほかのユーザーがあってはいけません。Load については、そのメモリのユーザーに、競合する書き込みやメモリの合流が生じる可能性があってはいけません。そうでなければ、読み出しを遅らせると値が変わり得ます。ずっと後に反依存を追加しますが、今は保守的にチェックします。 + +### 簡単な最適化を見逃す + +1本のメモリ鎖は正しいものの保守的です。オブジェクトが同一でも、`y` へのストアが、先行する `x` へのストアからの値の転送を妨げることがあります。これは最適化の取りこぼしであり、次の章の動機となります。 + +## グラフの実行 + +評価器には、小さな遅延スケジューラがあります。ロードは、同じメモリ定義を消費する後続のストアより先に実行しなければなりません。ロードからストアへの通常のデータエッジがなくてもそうです。これらのロード/ストアの反依存が、明示的なグラフエッジを補います。 + +たとえば `p` の割り当て後の `int old = p.x; p.x = arg; return old;` は、最後のストアの前の値を読みます。以下のグラフでは Load と Store が同じメモリ状態を消費します。スケジューラは Load を先に実行しなければなりません。両者の間に通常のデータエッジはありません。 + + + +このスケジューラは本章の実行には十分です。第13章では、大域的コード移動とスケジューリングを独立した話題として扱います。 + +## ビルドとテスト + +このディレクトリで、JDK 21 以降、Make、シェルを使って実行します。 + +```sh +make lib +make tests +make release +make view +``` + +Make は Maven を使わずにこのスナップショットを直接ビルドします。`make release` は `build/release/simple.jar` を生成し、`make view` は共通の対話的グラフビューアを起動します。Maven と IDEA のモジュール記述も提供されています。テストには、これまでのすべての章、メモリ順序、返されたオブジェクトの内容、入れ子のループが含まれます。 + +次の[第11章](../chapter11/README.ja.md)では、パーサーのこの1つの束縛を維持しつつ、グラフを独立したメモリスライスに分割します。 diff --git a/chapter10/README.md b/chapter10/README.md index 219d178f..e55d1057 100644 --- a/chapter10/README.md +++ b/chapter10/README.md @@ -1,5 +1,7 @@ # Chapter 10: Structs and Memory +English | [日本語](README.ja.md) + [Previous: Chapter 9](../chapter09/README.md) | [Next: Chapter 11](../chapter11/README.md) diff --git a/chapter10/docs/10-grammar.ja.md b/chapter10/docs/10-grammar.ja.md new file mode 100644 index 00000000..aac4be58 --- /dev/null +++ b/chapter10/docs/10-grammar.ja.md @@ -0,0 +1,131 @@ +# 第10章の文法 + +[English](10-grammar.md) | 日本語 + +この文書は [原文](10-grammar.md) の日本語訳です。 + +[第10章に戻る](../README.ja.md) + +```antlrv4 +grammar SimpleLanguage; + +program + : statement+ EOF + ; + +statement + : returnStatement + | structDeclaration + | declStatement + | blockStatment + | expressionStatement + | ifStatement + | whileStatement + | breakStatement + | continueStatment + ; + +field + : 'int' IDENTIFIER ';' + ; + +fields + : field+ + ; + +structDeclaration + : 'struct' IDENTIFIER '{' fields '}' + ; + +whileStatement + : 'while' '(' expression ')' statement + ; + +breakStatement + : 'break' ';' + ; + +continueStatement + : 'continue' ';' + ; + +ifStatement + : 'if' '(' expression ')' statement ('else' statement)? + ; + + +expressionStatement + : IDENTIFIER '=' expression ';' + | fieldExpression '=' expression ';' + ; + +blockStatement + : '{' statement+ '}' + ; + +structName + : IDENTIFIER + ; + +declStatement + : 'int' IDENTIFIER '=' expression ';' + | structName ('?')? IDENTIFIER '=' expression ';' + ; + +returnStatement + : 'return' expression ';' + ; + +expression + : comparisonExpression + ; + +comparisonExpression + : additiveExpression (('==' | '!='| '>'| '<'| '>='| '<=') additiveExpression)* + ; + +additiveExpression + : multiplicativeExpression (('+' | '-') multiplicativeExpression)* + ; + +multiplicativeExpression + : unaryExpression (('*' | '/') unaryExpression)* + ; + +unaryExpression + : ('-') unaryExpression + | '!' unaryExpression + | primaryExpression + ; + +newExpression + : 'new' IDENTIFIER + ; + +fieldExpression + : primaryExpresson '.' IDENTIFIER + ; + +primaryExpression + : IDENTIFIER + | INTEGER_LITERAL + | 'true' + | 'false' + | 'null' + | newExpression + | '(' expression ')' + | fieldExpression + ; + +INTEGER_LITERAL + : [1-9][0-9]* + | [0] + ; + +IDENTIFIER + : NON_DIGIT (NON_DIGIT | DIGIT)* + ; + +NON_DIGIT: [a-zA-Z_]; +DEC_DIGIT: [0-9]; +``` diff --git a/chapter10/docs/10-grammar.md b/chapter10/docs/10-grammar.md index b76531d5..0c6fe7e0 100644 --- a/chapter10/docs/10-grammar.md +++ b/chapter10/docs/10-grammar.md @@ -1,5 +1,7 @@ # Grammar for Chapter 10 +English | [日本語](10-grammar.ja.md) + ```antlrv4 grammar SimpleLanguage; diff --git a/chapter11/README.ja.md b/chapter11/README.ja.md new file mode 100644 index 00000000..137ace01 --- /dev/null +++ b/chapter11/README.ja.md @@ -0,0 +1,168 @@ +# 第11章: メモリの分割 + +[English](README.md) | 日本語 + +[前: 第10章](../chapter10/README.ja.md) | +[次: 第12章](../chapter12/README.ja.md) + +この文書は [原文](README.md) の日本語訳です。 + +第10章では、1つの SSA メモリ値でメモリへの作用を明示しました。この章ではパーサーのモデルをそのまま維持し、グラフの書き換えによって独立したメモリ鎖を復元します。新しい言語構文はありません。[文法](docs/10-grammar.ja.md)、ポインタ型、null チェックは変わりません。 + +## なぜメモリを分割するのか + +```java +struct Point { int x; int y; } +Point p = new Point; +p.x = 42; +while (arg > 0) { + p.y = p.y + arg; + arg = arg - 1; +} +return p.x; +``` + +ループが変更するのは `y` なので、`x` に影響できません。メモリ鎖が1本だと、`x` のロードはループのストアの後に続きます。フィールドを分割すれば、通常のストア後のロードへの値の転送によって、戻り値を 42 にできます。 + + + +グラフは、最後の Load を畳み込む前の分割された鎖を示します。そのメモリ入力は、ループの `y` の鎖を迂回して 42 の Store に直接つながります。第10章と同様、小さく切り離された True/False 射影は、Load と Store を制御するソースの分岐を示します。Loop とその Phi 束縛を含む、ほかの制御フローの文脈は省略しています。グラフが示すのは実行順序ではなく依存関係です。 + +## 同値類によるエイリアシング + +各構造体フィールドは整数のエイリアス識別子を持ちます。別のフィールドには、異なる構造体で同じ綴りを持っていても、別の識別子を与えます。*同じ*構造体の2つのインスタンスは、フィールドのエイリアスを共有します。したがって異なるクラス間では決してエイリアスしませんが、同じクラス内のアクセスは**エイリアスする可能性があります**(同じクラスであることは同じアドレスを意味しません)。ポインタの同一性は、依然として確認する必要があります。 + +パーサーは構造体の宣言時に、alias#2 から始めてフィールドの識別子を割り当てます。 + +宣言されたフィールドやエイリアスのそれぞれについて、Start 射影、スコープの束縛、ループ Phi を割り当てることはしません。Start が1つのメモリ値を供給し、Scope が1つの `$mem` 束縛を保持し、Return がメモリ全体を消費します: `{ctrl, $mem, result}`。Start の出力と同様、制御はスロット 0、メモリはスロット 1 にあります。最適化器は、グラフの演算が必要とする箇所にだけスライスを導入します。 + +## 3つのメモリノード + +| ノード | 意味 | +|---|---| +| `MemMerge` | デフォルトと精密なエイリアスごとの上書きからなる完全なメモリ状態 | +| `MemPhi` | 流入する制御経路に従って選択される1つのエイリアス | +| `BulkMemPhi` | 明示した除外集合を除くすべてのエイリアスを、制御によって選択する | + +`MemMerge` は、あるプログラム位置でメモリの独立した部分を組み合わせます。制御フローの合流ではありません。たとえばエイリアス `x` へのストア後は、次のようになります。 + +```text +prior = current memory +store = Store_x(prior, pointer, value) +current memory = MemMerge(default=prior, x=store) +``` + +MemMerge はスロット 1 にデフォルトのメモリを持ち、それと異なる場合にはほかのスロットに精密なエイリアスを持ちます。Load、Store、MemPhi は、入力の MemMerge から精密な入力を選択し、可能なら精密なエイリアス入力を使います。 + +明示的なエイリアスはデフォルトを上書きします。エイリアスのスライスは互いに交わらず、その和集合はすべてのメモリを覆います。積み重なった MemMerge を平坦化するときは、外側の MemMerge が置き換えていないエントリを保持します。 + +以下のグラフは、冗長な初期化ストアを除去した後の `p.x = arg; p.y = 42; return p;` を示します。MemMerge はスロット 1 にデフォルトのメモリを、スロット 2 と 3 に `x` と `y` の上書きを受け取ります。 + + + +## メモリ Phi の分割 + +パーサーは最初に、通常の SSA アルゴリズムでメモリの合流が必要な箇所に BulkMemPhi を作ります。合流点の確定中は、除外集合を空としてすべてのエイリアスを覆うところから始めます。その後、ピープホール最適化によって、必要に応じて遅延的に、ひとまとめのメモリから精密なメモリスライスを分離します。 + +この変換により、ひとまとめのメモリから精密な単一エイリアスを分離できます。 + +```text +BulkPhi(R, left, right) + becomes +MemMerge( + default = BulkPhi_except{x}(R, left, right), + x = MemPhi_x(R, slice(left,x), slice(right,x))) +``` + +「BulkPhi は、(そのエイリアスを除いた)BulkPhi と(そのエイリアスの)MemPhi をまとめる Merge になる」。 + +新しい精密な Phi と残りのひとまとめの Phi は、Region(図では省略)と一致する制御経路の添字を共有します。ループでも同じ変換を使い、入力の中に後退エッジが含まれます。さらに分割しても、すでに抽出したスライスをすべて保持します(積み重なった分割はピープホール最適化で整理します)。 + +ここでは2つの分岐が `p.x` に 3 または 4 を格納してから `p` を返します。グラフは、ひとまとめの Phi とその入力の集約をさらに簡略化する前、`x` を抽出した直後の分割を示します。 + +```java +struct Point { int x; int y; } +Point p = new Point; +if (arg) p.x = 3; +else p.x = 4; +return p; +``` + + + +このグラフから先に進めると、たとえば次の順でピープホール最適化が発動します。 + +**入力の MemMerge を平坦化してから、`y` を分割します。** MemMerge #17 と #18 は #9 から `y` のスライスを継承しますが、それぞれ自身の `x` の上書きが優先します。両者のデフォルトは Start のメモリとなり、#9 は使われなくなります。BulkMemPhi #21 は `x` を除外しますが、まだ `y` を覆うので、これらの MemMerge を単に迂回することはできません。`y` の MemPhi #24 を抽出し、両フィールドを除外した BulkMemPhi #25 を作ります。追加の MemMerge #26 がこれらのスライスを組み合わせます。 + + + +**変更のないメモリを畳み込みます。** MemPhi #24 の両入力は同じ初期化 Store #8 なので、その Store になります。BulkMemPhi #25 は、今度は #17 と #18 を透かして見ることができます。それらの明示的なエイリアスはすべて、その領域から除外されているためです。残る両入力は Start のメモリになり、このひとまとめの Phi も畳み込まれます。出力側の集約を平坦化すると、MemMerge #22 には Start からのデフォルトメモリ、MemPhi #20 からの `x`、Store #8 からの `y` が残ります。 + + + +**Store をくくり出してから、上書きされた初期化を取り除きます。** ひとまとめの Phi と入力の集約がなくなると、Store #14 と #16 を使うのは MemPhi #20 だけになります。これらは同じポインタとエイリアスに格納し、同じ入力メモリを共有します。`PhiNode` は Store を合流の下に引き下げられます。 + +```text +MemPhi_x(Store_x(mem, p, 3), Store_x(mem, p, 4)) + becomes +Store_x(mem, p, Phi(3, 4)) +``` + +一般に Phi の組み替えでは、第10章と同じく、Load や Store を押し上げたり引き下げたりして組み合わせられます。Phi の新しいメモリオペランドには、そのエイリアスの精密な MemPhi を使えます。このピープホール変換は精密なエイリアスを必要とし、BulkMem があると妨げられます。そのため、BulkMem を分割するきっかけにもなります。 + +例の続きを見ると、新しい値の Phi #28 は true 側で 3、false 側で 4 を選びます。Store #27 が両分岐の Store とそのメモリ Phi を置き換えます。初期の `p.x = 0` の Store #7 は、この1つの Store だけをユーザーに持つようになり、連続するストアの除去で迂回されます。`p.y = 0` の Store #8 は残ります。オブジェクトを返すので、その `y` フィールドは引き続き初期化が必要です。 + + + +したがって最終グラフには、2つの Store、通常の値の Phi が1つ、MemMerge が1つあり、MemPhi も BulkMemPhi も残りません。ワークリストの順序が違えば、たとえば元の入力 MemMerge を平坦化前に迂回し、一時的な `y` の分割を避けられる場合もあります。 + +このメモリ分割変換は大きくグラフに介入するため、再帰的に呼び出すことを禁止します。ループの周囲を分割することはよいのですが、その途中にあるループ先頭を分割しようとすると問題になります。分割で生まれた新しいノードをワークリストに入れ、現在の分割が完了してから訪問します。これによって、ループを巡るメモリを安全に完全分割できます。 + +ひとまとめの Phi は複数のエイリアスを合流するため、MemMerge に対してピープホール最適化をするときに注意が必要です。MemMerge の精密な合流と、ひとまとめの Phi が重複してはいけません。 + +これは第9章のワークリストの教訓も拡張します。ひとまとめの Phi は自分のユーザーを調べるので、ユーザーの分割が変わったら生成側を起こす必要があります。それが `addDep()` の仕事です。MemMerge からスライスを選ぶと、新しいひとまとめの Phi のユーザーが露出することがあります。その選択された定義もキューに入れなければなりません。つまり MemMerge、MemPhi、BulkMemPhi を扱うときには、非局所的な依存関係を想定する必要があります。 + +メモリ分割でノード数が増えることもありますが、Phi(Mem、Bulk、その他)はコードを生成しません。前進は、ひとまとめの Phi からエイリアスを抽出し、一部の Load と Store を除去することで得られます。すべての書き換えにグラフの縮小を求めるのではありません。言い換えれば、MemPhi 数が増えても、Load/Store の総数は減っています。 + +## 型 + +型の束は、第10章の6つの領域、色、記法を維持します。`⊤` はトップ、`⊥` はボトムを意味し、接尾辞は領域を示します。ほかの5領域は変わりませんが、メモリには、メモリトップ(`TypeMem.TOP`)とメモリボトム(`TypeMem.BOT`)の間に、精密なエイリアスごとの平坦な要素 `MEM#N` が加わります。 + + + +並列の `MEM#2`、`MEM#3`、さらにほかのエイリアス要素は、独立したフィールドスライスを表します。上の最初の `Point` 宣言では、この2つのエイリアスは `Point.x` と `Point.y` を識別します。異なるエイリアスの meet はメモリボトム、join はメモリトップです。精密なエイリアスはそれぞれ自分自身の双対です。(省略記号は並列なエイリアスがさらにあることを示し、エイリアスの集合を表す型ではありません。) + +## 評価と順序付け + +MemMerge は依存関係のノードであり、メモリを上書きしません。コードも出力しません。評価器での結果はメモリトークンですが、Store は実際にオブジェクトのフィールドを変更します。スケジューラは、メモリをまとめることと上書きすることを区別しなければなりません。異なるエイリアス類の Store はロード/ストアの反依存を課しませんが、同じ類の Load には反依存が必要です。 + +`int old = p.x; p.x = arg; p.y = arg; return old;` では、Load は後続の `x` の Store より前でなければならず、`y` の Store は独立したメモリ鎖を持ちます。グラフでは、局所的な畳み込み前のこのスケジューリング要件を示すため、Load を残しています。 + + + +この章には多くのテストがあります。独立したフィールドの畳み込み、並列のメモリ Phi、複数のフィールド更新が Return で保持されること、早期 return、break/continue を伴う入れ子のループ、エイリアスする可能性のあるポインタ経由の後続ストアより前の読み出しを扱います。 + +## 後の章に残ること + +第11章では、パース中にすべてのアクセスのエイリアスがわかっています。ここで遅延するのは、メモリ分割の構築です。第25章では(同じ表現を使いながら)既存のアクセスのエイリアスが*後になって*わかることも許し、さらに不完全な型、モジュールのエイリアス、分割コンパイルなど、多くの利点を可能にします。 + +第12〜24章はこの表現を使います。第13章では大域的コード移動とロード/ストアの反依存を追加します。第15章では部分メモリと型付きエイリアス内容を伴う割り当てを追加します。第16章では初期化をコンストラクタの値に拡張し、第18章では関数呼び出しをまたいでメモリ全体を運びます。 + +## ビルドとテスト + +JDK 21 以降を使い、このディレクトリで実行します。 + +```sh +make lib +make tests +make release +make view +``` + +Make は Maven を使わず、この完全なスナップショットをビルドします。`make release` は `build/release/simple.jar` を書き出し、`make view` は共通のグラフビューアを起動します。ルートの Maven reactor と提供される IDEA のモジュール記述の両方に本章が含まれます。リポジトリのルートから、前後両方をテストするには次を実行します。 + +```sh +make tests CHAPTERS="chapter10 chapter11" +``` + +次の[第12章](../chapter12/README.ja.md)では、このメモリ表現を維持したまま参照フィールドと再帰構造体を追加します。 diff --git a/chapter11/README.md b/chapter11/README.md index d3e8c7e1..9bf2e1e7 100644 --- a/chapter11/README.md +++ b/chapter11/README.md @@ -1,5 +1,7 @@ # Chapter 11: Splitting Memory +English | [日本語](README.ja.md) + [Previous: Chapter 10](../chapter10/README.md) | [Next: Chapter 12](../chapter12/README.md) diff --git a/chapter11/docs/10-grammar.ja.md b/chapter11/docs/10-grammar.ja.md new file mode 100644 index 00000000..1dbfba6a --- /dev/null +++ b/chapter11/docs/10-grammar.ja.md @@ -0,0 +1,131 @@ +# 第11章の文法 + +[English](10-grammar.md) | 日本語 + +この文書は [原文](10-grammar.md) の日本語訳です。 + +[第11章に戻る](../README.ja.md) + +```antlrv4 +grammar SimpleLanguage; + +program + : statement+ EOF + ; + +statement + : returnStatement + | structDeclaration + | declStatement + | blockStatment + | expressionStatement + | ifStatement + | whileStatement + | breakStatement + | continueStatment + ; + +field + : 'int' IDENTIFIER ';' + ; + +fields + : field+ + ; + +structDeclaration + : 'struct' IDENTIFIER '{' fields '}' + ; + +whileStatement + : 'while' '(' expression ')' statement + ; + +breakStatement + : 'break' ';' + ; + +continueStatement + : 'continue' ';' + ; + +ifStatement + : 'if' '(' expression ')' statement ('else' statement)? + ; + + +expressionStatement + : IDENTIFIER '=' expression ';' + | fieldExpression '=' expression ';' + ; + +blockStatement + : '{' statement+ '}' + ; + +structName + : IDENTIFIER + ; + +declStatement + : 'int' IDENTIFIER '=' expression ';' + | structName ('?')? IDENTIFIER '=' expression ';' + ; + +returnStatement + : 'return' expression ';' + ; + +expression + : comparisonExpression + ; + +comparisonExpression + : additiveExpression (('==' | '!='| '>'| '<'| '>='| '<=') additiveExpression)* + ; + +additiveExpression + : multiplicativeExpression (('+' | '-') multiplicativeExpression)* + ; + +multiplicativeExpression + : unaryExpression (('*' | '/') unaryExpression)* + ; + +unaryExpression + : ('-') unaryExpression + | '!' unaryExpression + | primaryExpression + ; + +newExpression + : 'new' IDENTIFIER + ; + +fieldExpression + : primaryExpresson '.' IDENTIFIER + ; + +primaryExpression + : IDENTIFIER + | INTEGER_LITERAL + | 'true' + | 'false' + | 'null' + | newExpression + | '(' expression ')' + | fieldExpression + ; + +INTEGER_LITERAL + : [1-9][0-9]* + | [0] + ; + +IDENTIFIER + : NON_DIGIT (NON_DIGIT | DIGIT)* + ; + +NON_DIGIT: [a-zA-Z_]; +DEC_DIGIT: [0-9]; +``` diff --git a/chapter11/docs/10-grammar.md b/chapter11/docs/10-grammar.md index 5a3f3778..6d9aec1c 100644 --- a/chapter11/docs/10-grammar.md +++ b/chapter11/docs/10-grammar.md @@ -1,5 +1,7 @@ # Grammar for Chapter 11 +English | [日本語](10-grammar.ja.md) + ```antlrv4 grammar SimpleLanguage; diff --git a/chapter12/README.ja.md b/chapter12/README.ja.md new file mode 100644 index 00000000..3aa4c63f --- /dev/null +++ b/chapter12/README.ja.md @@ -0,0 +1,101 @@ +# 第12章: 参照 + +[English](README.md) | 日本語 + +[前: 第11章](../chapter11/README.ja.md) | +[次: 第13章](../chapter13/README.ja.md) + +この文書は [原文](README.md) の日本語訳です。 + +第10章と第11章では、構造体に整数フィールドを追加し、そのメモリへの作用を SSA で表しました。この章では、まだ定義が現れていない構造体も含め、フィールドにほかの構造体への参照を格納できるようにします。メモリグラフを変更せずに、連結リストや互いを参照するオブジェクトを扱えます。 + +線形の Git 履歴で[この章](https://github.com/SeaOfNodes/Simple/tree/linear-chapter12)を読むことや、[第11章との差分](https://github.com/SeaOfNodes/Simple/compare/linear-chapter11...linear-chapter12)を見ることもできます。 + +## 型付きフィールドと前方参照 + +```java +struct Person { + int age; + FamilyTree? tree; +} +struct FamilyTree { + Person? father; + Person? mother; +} +``` + +パーサーの `TYPES` テーブルは名前を型に対応付けます。初期状態では `int`(およびほかのプリミティブ型。後の章でさらに登場します)が入っています。`type()` が未知の名前を使う宣言を認識すると、`_fields == null` の、名前だけの `TypeStruct` を登録します。これは構造体名だけがわかっていて、ほかの情報はない構造体です。その構造体へのポインタがあれば、参照フィールドを記述するには十分で、`?` によってポインタが null であることを許容します。 + +`parseStruct()` は、すべてのフィールドを収集してから完全な構造体を `TYPES` に登録します。テーブルのエントリを置き換えるのであって、古い型を変更したり、フィールドリストの途中までを順次公開したりはしません。フィールドのエイリアスは、これまでと同じく宣言が完成した時点で割り当てます。 + +[パーサー](src/main/java/com/seaofnodes/simple/Parser.java)、特に `type()`、`parseField()`、`parseStruct()` と、[文法](docs/12-grammar.ja.md)を参照してください。 + +## 再帰構造体: `L0` と `L1` + +```java +struct LLI { LLI? next; int i; } +``` + +コンパイラが実際に何を構築するかを覚えておくには、次の2つの記述が役立ちます。 + +```text +L0 = TypeStruct("LLI", fields = null) +L1 = TypeStruct("LLI", fields = { next: pointer-to-L0?, i: int }) +TYPES["LLI"] = L1 +``` + +`L0` は `LLI? next` フィールドをパースするときに作られます。閉じ波括弧の後で、名前テーブル内の `L0` を `L1` が置き換えます。`L1` 内の `next` フィールドは引き続き `L0` を指します。`L1` に戻るように書き換えません。 + +この設計は、**不変で浅い参照を、必要に応じて名前で解決する**ものです。実行時のオブジェクトはサイクルを形成しても、コンパイラの型記述は有限で非循環のままです。これは、**再帰的な型の展開を止める**意図的な決定です。すべての参照に完成した定義を再帰的に代入すると、無限に展開するか、循環する型のインターン化、等価性、ハッシュ、束の操作が必要になります(実際に、束を扱う章で登場します)。 + +[`TypeStruct`](src/main/java/com/seaofnodes/simple/type/TypeStruct.java) は、代わりに名前だけの記述で止めます。`dual()` と `glb()` はその記述をそのままにします。同じ構造体の完全な記述と名前だけの記述の meet は、名前だけの記述になります。これによってフィールドの詳細は失われますが、構造体の識別情報は失われません。`L0` をますます深い `L1`、`L2` などの列へと展開しようとするパスはありません。 + +## 浅い参照の解決 + +`head.next` のパース時、`parsePostfix()` はポインタの構造体名を取り出し、`TYPES` で現在の宣言を検索します。ポインタ自身の型にまだ `L0` が含まれていても、その宣言からフィールドの配置とエイリアスを得ます。`head.next.next.i` のような連鎖には、次のフィールドアクセスでも検索を繰り返せば十分です。 + +局所宣言や代入では、`parseExpressionStatement()` も、代入の互換性を確認する前に名前だけのポインタを解決します。この「深める」処理はチェックに使う一時的な型を変更するだけで、式ノードの型を書き換えたり、グラフ内の古い参照をすべて置き換えたりはしません。 + +```java +struct LLI { LLI? next; int i; } +LLI? head = null; +while (arg) { + LLI x = new LLI; + x.next = head; + x.i = arg; + head = x; + arg = arg - 1; +} +if (!head) return 0; +LLI? next = head.next; +if (next == null) return 1; +return next.i; +``` + +null チェックは、生き残る制御経路でポインタの型を絞り込みます。特に、明示的な `else` がなくても、`if` の false 経路はその絞り込みを記録しなければなりません。名前の解決はフィールドの定義を与えますが、ポインタが null でないことを証明するわけではありません。 + +## 初期化と不完全な定義 + +`newStruct()` は、各フィールドをその型の `makeInit()` で初期化します。整数はゼロ、参照は null です。Load と Store は第11章と同じエイリアスとメモリ最適化を使います。したがって、初期化 Store からの Load は null に畳み込めます。 + +使われない前方参照は、最後まで定義する必要がありません。 + +```java +struct Holder { Missing? ref; } +Holder h = new Holder; +return h.ref; // null; no definition of Missing is needed +``` + +ここでは、完全な定義なしに null でない `Missing` を構築する方法はありません。`new Missing` は拒否されます。そのフィールドにアクセスするにも、null でないポインタに加えて定義が必要です。null を許容する参照型に言及するだけでは、プログラムの終わりまでに定義する義務は生じません。 + +この章には、初期化に関する一時的な許容事項が1つあります。 + +```java +struct N { N next; int i; } +N n = new N; +return n.next; // null, despite the non-null field declaration +``` + +初期化 Store は、null を許容しない参照フィールドにも null を書き込めます。通常の `n.next = null` は拒否されます。第16章ではコンストラクタを追加して初期化をチェックします。この章では、この区別を [`StoreNode`](src/main/java/com/seaofnodes/simple/node/StoreNode.java) に明示的に保持します。 + +[参照のテスト](src/test/java/com/seaofnodes/simple/Chapter12Test.java)は、連結リスト、相互参照、null チェック、不完全な型を検証します。`make tests` で実行でき、`make view` は同じ未スケジュールのグラフを表示します。これで第13章には、ロード/ストアの反依存も含めた大域的コード移動の導入に必要な、メモリと参照の意味論がそろいました。 diff --git a/chapter12/README.md b/chapter12/README.md index 3110d735..33b6bfd1 100644 --- a/chapter12/README.md +++ b/chapter12/README.md @@ -1,5 +1,7 @@ # Chapter 12: References +English | [日本語](README.ja.md) + [Previous: Chapter 11](../chapter11/README.md) | [Next: Chapter 13](../chapter13/README.md) diff --git a/chapter12/docs/12-grammar.ja.md b/chapter12/docs/12-grammar.ja.md new file mode 100644 index 00000000..afa19a08 --- /dev/null +++ b/chapter12/docs/12-grammar.ja.md @@ -0,0 +1,136 @@ +# 第12章の文法 + +[English](12-grammar.md) | 日本語 + +この文書は [原文](12-grammar.md) の日本語訳です。 + +[第12章に戻る](../README.ja.md) + +```antlrv4 +grammar SimpleLanguage; + +program + : statement+ EOF + ; + +statement + : returnStatement + | structDeclaration + | declStatement + | blockStatement + | expressionStatement + | ifStatement + | whileStatement + | breakStatement + | continueStatement + ; + +PRIMTYPE + : 'int' + ; + +field + : PRIMTYPE IDENTIFIER ';' + | structName ('?')? IDENTIFIER ';' + ; + +fields + : field+ + ; + +structDeclaration + : 'struct' IDENTIFIER '{' fields '}' + ; + +whileStatement + : 'while' '(' expression ')' statement + ; + +breakStatement + : 'break' ';' + ; + +continueStatement + : 'continue' ';' + ; + +ifStatement + : 'if' '(' expression ')' statement ('else' statement)? + ; + + +expressionStatement + : IDENTIFIER '=' expression ';' + | fieldExpression '=' expression ';' + ; + +blockStatement + : '{' statement+ '}' + ; + +structName + : IDENTIFIER + ; + +declStatement + : PRIMTYPE IDENTIFIER '=' expression ';' + | structName ('?')? IDENTIFIER '=' expression ';' + ; + +returnStatement + : 'return' expression ';' + ; + +expression + : comparisonExpression + ; + +comparisonExpression + : additiveExpression (('==' | '!='| '>'| '<'| '>='| '<=') additiveExpression)* + ; + +additiveExpression + : multiplicativeExpression (('+' | '-') multiplicativeExpression)* + ; + +multiplicativeExpression + : unaryExpression (('*' | '/') unaryExpression)* + ; + +unaryExpression + : ('-') unaryExpression + | '!' unaryExpression + | primaryExpression + ; + +newExpression + : 'new' IDENTIFIER + ; + +fieldExpression + : primaryExpression '.' IDENTIFIER + ; + +primaryExpression + : IDENTIFIER + | INTEGER_LITERAL + | 'true' + | 'false' + | 'null' + | newExpression + | '(' expression ')' + | fieldExpression + ; + +INTEGER_LITERAL + : [1-9][0-9]* + | [0] + ; + +IDENTIFIER + : NON_DIGIT (NON_DIGIT | DEC_DIGIT)* + ; + +NON_DIGIT: [a-zA-Z_]; +DEC_DIGIT: [0-9]; +``` diff --git a/chapter12/docs/12-grammar.md b/chapter12/docs/12-grammar.md index 24199ede..41a7e24e 100644 --- a/chapter12/docs/12-grammar.md +++ b/chapter12/docs/12-grammar.md @@ -1,5 +1,7 @@ # Grammar for Chapter 12 +English | [日本語](12-grammar.ja.md) + ```antlrv4 grammar SimpleLanguage; diff --git a/chapter13/README.ja.md b/chapter13/README.ja.md new file mode 100644 index 00000000..d16b96ed --- /dev/null +++ b/chapter13/README.ja.md @@ -0,0 +1,256 @@ +# 第13章: 大域的コード移動 + +[English](README.md) | 日本語 + +[前: 第12章](../chapter12/README.ja.md) | +[次: 第14章](../chapter14/README.ja.md) + +この文書は [原文](README.md) の日本語訳です。 + +この章では、第10〜12章で構築した、参照フィールドと遅延的なメモリ分割を含むグラフをスケジュールします。移動可能な演算のスロット 0 の制御入力を選び、エイリアスする可能性がある Load と Store の順序を維持します。 + +また、この章ではかなり詳しいグラフアルゴリズムをいくつか紹介します。グラフ理論を復習しておくとよいでしょう。 + +# 目次 + +1. [概要](#概要) +2. [スケジューリングの例を追う](#スケジューリングの例を追う) +3. [ループのスケジューリング](#ループのスケジューリング) +4. [大域的コード移動アルゴリズムの構成要素](#大域的コード移動アルゴリズムの構成要素) +5. [SoN グラフ内の基本ブロックの識別](#son-グラフ内の基本ブロックの識別) +6. [無限ループの処理](#無限ループの処理) +7. [支配ノード](#支配ノード) +8. [ループ深度](#ループ深度) +9. [早期スケジュール](#早期スケジュール) +10. [遅延スケジュール](#遅延スケジュール) +11. [反依存の挿入](#反依存の挿入) +12. [動画での解説](#動画での解説) + +[linear](https://github.com/SeaOfNodes/Simple/tree/linear) ブランチの線形な Git リビジョン履歴で[この章](https://github.com/SeaOfNodes/Simple/tree/linear-chapter13)を読み、前章との[差分](https://github.com/SeaOfNodes/Simple/compare/linear-chapter12...linear-chapter13)を見ることもできます。 + +元の入力ソースプログラムは、物事が起こる順序を定義します。プログラムを Sea of Nodes 表現にパースし、さまざまな最適化を行うと、その順序は完全には維持されません。最適化された Sea of Nodes グラフを動かすのは、元のソースプログラムの命令列ではなく、ノード間の依存関係です。 + +この章の目標は、最適化された Sea of Nodes グラフから、命令を実行するスケジュールをどう復元するかを見ることです。このスケジュールは、元のソースプログラムの意味論を保持する必要がありますが、それ以外は命令の実行を並べ替えてかまいません。 + +スケジューリングアルゴリズムは SoN グラフ内の暗黙の基本ブロックをまたいで働くため、大域的コード移動アルゴリズムと呼びます。スケジューラは基本ブロックの境界を越えて命令を移動できます。 + +このアルゴリズムの主な参考文献は GCM GVN 論文 [^1] です。ただし Simple の実装には、論文で説明された版と異なる点があります。 + +複雑な話題なので、まず概要を示してから詳しく見ていきます。 + +## 概要 + +Sea of Nodes グラフには、2つの仮想的なグラフが含まれています。 + +* Start、If、Region など、特定のノード型が表す制御フローグラフがあります。 +* 主として、値やメモリを生成・消費するノード間の定義と使用のエッジ(Def-Use)によって成り立つデータグラフがあります。 + +スケジューリングの観点では、制御フローグラフは固定され、動かせません。一方、データ値やメモリを消費・生成するノードには、実行する時点にある程度の自由があります。スケジューリングアルゴリズムの目標は、これらのノードについて、最適かつ正しい配置を見つけることです。 + +中心となるスケジューリングアルゴリズムは2段階で働きます。 + +* 早期スケジュール: この段階では末端(Stop)から始めて、各 Node の「入力」を上向きに DFS でたどります。各データノードを、その入力に支配される最初の制御ブロックに配置します。 +* 遅延スケジュール: この段階では Stop から逆向きに、定義より先に使用をスケジュールします。そしてデータノードを、上で計算した最初のブロックと、すべての使用を支配する最後の制御ブロックの間に移動します。配置は、可能な限り浅いループの入れ子に置き、かつできるだけ制御に依存させるという条件に従います。さらに Load 命令の配置では、同じメモリ位置に対する Load と Store の順序を正しくする必要があります。 + +## スケジューリングの例を追う + +各手順を詳しく説明する前に、例を追ってスケジューリング中に SoN グラフがどう変わるかを見ると理解の助けになります。 + +説明のために、次のプログラムを使います。 + +```java +struct S { int f; } +S v=new S; +v.f = 2; +int i=new S.f; +i=v.f; +if (arg) v.f=1; +return i; +``` + +まずスケジューリング前のグラフを見ましょう。 + +この説明用の図は、精密な `S.f` の鎖と、Return が消費する最後の MemMerge を示します。変更されないデフォルトのメモリは Start から来ており、冗長なひとまとめの Phi は畳み込まれて消えています。第10章・第11章と同じく、Region とその Phi 束縛を含む制御フローの文脈は省略しています。Load や Store を制御する箇所には、小さく切り離された True/False 射影が残っています。第10章の模式的なソース上の制御とは異なり、これらの図は各段階の実際のスケジューリング入力を示します。入口ブロックと Loop の束縛は省略しています。 + + + +次の点に注目してください。 + +* 制御ノードは動かせません。ここでは大部分を省略しています。 +* New ノードにはすでに制御入力があり、Phi は Region に束縛されています。これらの束縛は図示していなくても固定のままです。 +* ロード `.f` とストア `.f=` のノードはまだ「浮いて」います。この段階では制御入力がないため、スケジューリングでそのブロックを選ぶ必要があります。 + +次に、早期スケジュール実行後のグラフを見ましょう。 + + + +早期スケジュールは、入力がそこで利用できるため、ロード `.f` とストア `.f=` を最初の基本ブロックに置きます。この入口ブロックへの制御束縛は省略しているので、メモリの図は変わっていないように見えます。 + +以下のグラフは、遅延スケジューリング後のメモリとデータの依存関係に注目しています。Region を含む制御フローの骨組みは省略しています。Store #22 の制御入力は、独立した True 射影 #8 への赤いエッジとして見えます。Store から Load への反依存とメモリ Phi は残しています。 + + + +以下の抜粋は、グラフの主な変更を示します。 + + + +* ストア `.f=` は、ロード `.f` に対する反依存を持つようになりました。これにより、プログラムの意味論が要求する通り、ストアがロードより後にスケジュールされます。反依存は後で詳しく扱います。 +* ストア `.f=` が If ノードの True 分岐に束縛されたことにも注目してください。 + +## ループのスケジューリング + +今度はループを含む別の例を示します。 + +```java +struct S { int f; } +S v = new S; +int i = arg; +while (arg > 0) { + int j = i/3; + if (arg == 5) + v.f = j; + arg = arg - 1; +} +return v; +``` + +スケジューリング前の SoN グラフは次のようになります。 + + + +早期スケジュールを生成すると、次を得ます。 + + + +早期スケジュールは Store #31 をループ先頭に配置します(制御束縛は省略)。遅延スケジューリングは、それを `if (arg == 5)` の True 分岐 #15 に移します。赤い制御エッジがそれを示します。 + + + +最終的な実行スケジュールでは、式 `i/3` はループの外で実行できます。`arg` の元の値だけに依存するため、ループ不変だからです。 + +## 大域的コード移動アルゴリズムの構成要素 + +上では GCM アルゴリズムの構成要素のいくつかに触れました。ここで、まだ触れていないものも含めて列挙します。 + +* SoN グラフの制御フローノードには、暗黙の基本ブロックがあります。GCM アルゴリズムでは、これらのノードをもっと明示的に認識する必要があります。 +* プログラムに無限ループがあると、Stop ノードから到達できないノードが生じ、アルゴリズムに問題をもたらします。対処するため、無限ループを発見し、ループから Stop ノードへのダミーエッジを作る必要があります。 +* Simple の SoN グラフではループをすでに識別しているため、ループ発見の手順は不要です。ただし、各 CFG ノードに関連するループ深度を計算する必要があります。 +* [第6章](../chapter06/README.ja.md)で支配ノードの概念を説明しました。支配ノードは GCM アルゴリズムの要なので、段階的な支配ノード発見アルゴリズムを、その要件を満たすように拡張します。 +* アルゴリズムの2段階、早期スケジューリングと遅延スケジューリングはすでに述べました。 +* さらに、特定の場面では、正しい実行順序を強制するため Load と Store の間に反依存エッジを追加する必要があります。 +* GCM アルゴリズムの実装を容易にするため、Node 階層にいくつか変更を加えます。これらは、前の章から引き継いだ Node 階層を概念的に変えるものではありません。 + +## SoN グラフ内の基本ブロックの識別 + +Sea of Nodes グラフは、プログラムの制御フローグラフをすでに捉えています。この情報は、制御ノードと、制御ノードからほかの型のノードへのエッジに暗黙に含まれます。 + +復習すると、制御は Start ノードで、`$ctrl` という名前に束縛された射影を介して始まります。プログラム中を制御が流れるにつれて、この名前の束縛は更新され、制御が受け渡され、最終的に Stop ノードに到達します。 + +ある制御ノードは基本ブロックを開始し、別のものは基本ブロック(BB)を終了すると認識すれば、CFG の基本ブロック構造を簡単に構築できます。 + +すべての制御ノードを列挙します。 + +| ノード | BB を開始する | BB を終了する | +|---|---|---| +| Start | はい。ただし `$ctrl` 射影を介してのみ | いいえ | +| CProj | 入力制御が If ノードなら、はい | いいえ | +| Region | はい | いいえ | +| If | いいえ | はい | +| Return | いいえ | はい | +| Stop | はい | はい | + +この章における Node 階層の変更は次の通りです。 + +* 上のすべての制御ノードは、基底クラス CFG Node を継承するようになります。これによって、制御フローノードの共通機能を基底クラスに置けます。 +* CProj ノードは通常の Proj ノードを継承し、次の場合に使います。 + * Start からの `$ctrl` 射影 + * If からの True と False の射影 +* Never ノードは If の特別なサブクラスで、後述するように無限ループの処理に使います。 +* XCtrl ノードは死んだ制御を表します。 + +## 無限ループの処理 + +無限ループを含むコードの例です。 + +```java +while (1) {} +return 0; +``` + +まず、その結果のグラフを見ましょう。 + + + +次に、無限ループから Stop ノードへのエッジを挿入した後の、変更されたグラフを見てください。 + + + +実装は [`LoopNode.forceExit()`](src/main/java/com/seaofnodes/simple/node/LoopNode.java#L57) にあります。後退エッジの支配ノードの鎖をたどり、出口を探します。出口がなければ Never ノードと、決して通らない return 経路を挿入し、スケジューラの逆向きの探索でループに到達できるようにします。 + +## 支配ノード + +Simple では支配ノードを段階的に計算します。ピープホール最適化でグラフの一部を削除することはあっても、新しい制御構造を導入することはないという性質を利用します。これにより、以下の単純な方法を使えます。 + +CFG ノードはすべての制御ノードの基底クラスです。`_idepth` を通して、支配深度の保守的な近似を保持します。このフィールドは直近の支配ノードの深度を表すキャッシュ値です。初期値 `0` は未計算を意味し、要求時に深度を計算します。 + +デフォルトの計算とキャッシュは [`CFGNode.idepth()`](src/main/java/com/seaofnodes/simple/node/CFGNode.java#L51) を参照してください。[`RegionNode`](src/main/java/com/seaofnodes/simple/node/RegionNode.java#L94)、[`LoopNode`](src/main/java/com/seaofnodes/simple/node/LoopNode.java#L31)、[`StartNode`](src/main/java/com/seaofnodes/simple/node/StartNode.java#L36)、[`StopNode`](src/main/java/com/seaofnodes/simple/node/StopNode.java#L58) のオーバーライドは、合流、ループ入口、グラフの端点を扱います。 + +制御フローグラフの一部を削除すると、`_idepth` に飛びが生じますが、支配木を下るにつれて `_idepth` の値が増えるという不変条件は依然として正しく反映されます。 + +最初の計算時にキャッシュする支配深度とともに、直近の支配ノードを得るメソッドを提供します。この値は、ある時点でのみ有効であり、グラフが変わると無効になるためキャッシュしません。 + +[`CFGNode.idom()` と `domLCA()`](src/main/java/com/seaofnodes/simple/node/CFGNode.java#L60) は、デフォルトの直近の支配ノードと、2つの支配ノードの最小共通祖先を見つける探索を提供します。[`RegionNode.idom()`](src/main/java/com/seaofnodes/simple/node/RegionNode.java#L103) は流入する経路を組み合わせ、[`LoopNode.idom()`](src/main/java/com/seaofnodes/simple/node/LoopNode.java#L33) はループ入口を使います。Start と Stop に直近の支配ノードはありません。 + +## ループ深度 + +Simple の Sea of Nodes グラフは、Loop ノードによってループを明示的に識別します。言語が提供するループの作り方は `while` 文だけなので、汎用的なループ発見処理を実装する必要はありません。 + +ただし、ループ深度を計算する必要はあります。支配深度の計算と似た方法で行います。 + +[`CFGNode.loopDepth()`](src/main/java/com/seaofnodes/simple/node/CFGNode.java#L78) は制御入力の深度を継承し、[`RegionNode.loopDepth()`](src/main/java/com/seaofnodes/simple/node/RegionNode.java#L116) は流入する経路を使います。[`LoopNode.loopDepth()`](src/main/java/com/seaofnodes/simple/node/LoopNode.java#L35) は入れ子を1段増やし、後退エッジの支配ノードの鎖をたどってループ出口に印を付けます。[`StartNode`](src/main/java/com/seaofnodes/simple/node/StartNode.java#L41) と [`StopNode`](src/main/java/com/seaofnodes/simple/node/StopNode.java#L68) は最も外側の深度を 1 にします。 + +## 早期スケジュール + +GCM アルゴリズム本体は、早期スケジュールの計算から始まります。末端(Stop)から各 Node の「入力」を上向きに DFS でたどり、各データノードを、その入力に支配される最初の制御ブロックに配置します。 + +前提として、前述した方法で無限ループを修正しておく必要があります。 + +実装は [`GlobalCodeMotion.schedEarly()`](src/main/java/com/seaofnodes/simple/GlobalCodeMotion.java#L62) から始まります。[`_rpo_cfg()`](src/main/java/com/seaofnodes/simple/GlobalCodeMotion.java#L85) は CFG の走査順序を構築し、[`_schedEarly()`](src/main/java/com/seaofnodes/simple/GlobalCodeMotion.java#L94) はデータ入力を再帰的にスケジュールします。制御をスケジュールする前にデータのサイクルへ入ることを避けるため、Phi 入力を通る再帰は飛ばします。 + +既存の制御および Phi/Proj の束縛は維持します。浮いているノードには、最も深い入力ブロックを最も早い配置として与えます。MemMerge も、疎なエイリアステーブル内の存在しないエントリを無視して、同じ規則に従います。 + +## 遅延スケジュール + +遅延スケジューリングは Stop から始め、ワークリストを使って定義より先に使用を配置します。CFG ノード、Phi、射影、割り当ては固定の配置を与えます。ほかのノードは、自分の使用がスケジュールされるまで待ちます。Load は、自分のエイリアスを上書きしたり合流したりできるメモリユーザーも待ちます。選ぶブロックは、早期配置とすべての使用の共通支配ノードとの間にあり、浅いループを優先し、その次に深い制御フローを優先します。 + +実装は次の手順に分かれます。 + +* [`schedLate()`](src/main/java/com/seaofnodes/simple/GlobalCodeMotion.java#L114) は配置テーブルを管理し、最終的な制御入力を設定します。[`breadth()`](src/main/java/com/seaofnodes/simple/GlobalCodeMotion.java#L129) はワークリストを駆動し、使用が完了すると定義と待機中のロードを起こします。 +* [`_doSchedLate()`](src/main/java/com/seaofnodes/simple/GlobalCodeMotion.java#L175) は使用の共通支配ノードを見つけ、ロードの反依存を考慮して、早期配置に向かってたどります。[`better()`](src/main/java/com/seaofnodes/simple/GlobalCodeMotion.java#L215) を呼び出し、候補ブロックを比較します。 +* [`use_block()`](src/main/java/com/seaofnodes/simple/GlobalCodeMotion.java#L202) は Phi の使用を、合流そのものではなく、対応する流入制御経路で扱います。 + +## 反依存の挿入 + +同じメモリ位置への Load と Store を正しく順序付けるため、以下のように Store から Load へのエッジを挿入します。Store は、先行する Load を待たなければなりません。これらのエッジは、通常 SoN で捉える Def-Use の依存関係を表さず、純粋にスケジューリングの制約として存在するため、反依存と呼びます。 + +反依存は、遅延スケジュールの実行**中**に計算します。早期スケジュールと、Load をスケジュールする前に得られる Load の使用の遅延スケジュールに依存するためです。 + +Load から後ろを見て、そのメモリ入力を Start のメモリ射影、メモリ Phi、または Store までたどります。最適化によって、MemMerge を通る Load の精密なスライスはすでに選択されています。そのメモリ定義のユーザーを調べ、Load のエイリアスを覆う Store とメモリ Phi だけを残します。Start の1つのメモリ射影は多数のエイリアスを供給する可能性があるので、メモリ入力を共有するだけで反依存があるとは限りません。 + +MemMerge はスライスを包むだけで、どれも上書きしません。これを書き込みとして扱うと、Load 自身の結果に依存する集約を Load が待つことになり得ます。Load をスケジュールする準備ができたかどうかの判断と、反依存の計算には、同じエイリアスのフィルタを使います。完了したメモリユーザーは、共有するメモリ定義がすでにスケジュールされていても、待機中のロードを起こします。 + +`schedLate` の途中なので、Load のすべてのユーザーの遅延スケジュールはすでに計算済みで、Load の使用の*最小共通祖先*、すなわち LCA または遅い位置を持っています。Load に影響し得るメモリ定義の集合を調べ、Store から Load への反依存を追加するか、Load の実効的な LCA を上げます。Phi のメモリ定義では Phi の入力を調べ、そのブロックに実効的な使用を置きます。これを Load の LCA を上げるために使います。ストアでも同じことを行いますが、Load の LCA と**同じ**ブロックのストアが見つかったら、同一ブロック内の順序を強制する反依存エッジを追加します。 + +エイリアスのフィルタは [`antiUse()`](src/main/java/com/seaofnodes/simple/GlobalCodeMotion.java#L224)、Load の候補ブロックへの印付けと競合するメモリユーザーの調査は [`find_anti_dep()`](src/main/java/com/seaofnodes/simple/GlobalCodeMotion.java#L233) を参照してください。[`anti_dep()`](src/main/java/com/seaofnodes/simple/GlobalCodeMotion.java#L260) は、競合する演算の支配区間をたどり、Load の LCA を上げ、必要なら Store から Load へのエッジを追加します。評価器のスケジューラは GCM と独立してその Store を配置する可能性があるため、Store の区間全体をチェックします。 + +## 動画での解説 + +実装を詳しくたどるには、以下の動画をご覧ください。 + +* [Coffee Compiler Club - 2024年6月21日](https://youtu.be/5Po0gxfE7LA?feature=shared) +* [Coffee Compiler Club - 2024年6月26日](https://www.youtube.com/watch?v=HnN2YzmFb-8&t=0s) + +[^1]: Cliff Click. (1995). + Global Code Motion Global Value Numbering. + +次の[第14章](../chapter14/README.ja.md)では、このスケジュール済みコンパイラに浮動小数点演算と狭い数値型を追加します。 diff --git a/chapter13/README.md b/chapter13/README.md index 84660eba..f86abe57 100644 --- a/chapter13/README.md +++ b/chapter13/README.md @@ -1,5 +1,7 @@ # Chapter 13: Global Code Motion +English | [日本語](README.ja.md) + [Previous: Chapter 12](../chapter12/README.md) | [Next: Chapter 14](../chapter14/README.md) diff --git a/chapter13/docs/13-grammar.ja.md b/chapter13/docs/13-grammar.ja.md new file mode 100644 index 00000000..9f18bc74 --- /dev/null +++ b/chapter13/docs/13-grammar.ja.md @@ -0,0 +1,136 @@ +# 第13章の文法 + +[English](13-grammar.md) | 日本語 + +この文書は [原文](13-grammar.md) の日本語訳です。 + +[第13章に戻る](../README.ja.md) + +```antlrv4 +grammar SimpleLanguage; + +program + : statement+ EOF + ; + +statement + : returnStatement + | structDeclaration + | declStatement + | blockStatement + | expressionStatement + | ifStatement + | whileStatement + | breakStatement + | continueStatement + ; + +PRIMTYPE + : 'int' + ; + +field + : PRIMTYPE IDENTIFIER ';' + | structName ('?')? IDENTIFIER ';' + ; + +fields + : field+ + ; + +structDeclaration + : 'struct' IDENTIFIER '{' fields '}' + ; + +whileStatement + : 'while' '(' expression ')' statement + ; + +breakStatement + : 'break' ';' + ; + +continueStatement + : 'continue' ';' + ; + +ifStatement + : 'if' '(' expression ')' statement ('else' statement)? + ; + + +expressionStatement + : IDENTIFIER '=' expression ';' + | fieldExpression '=' expression ';' + ; + +blockStatement + : '{' statement+ '}' + ; + +structName + : IDENTIFIER + ; + +declStatement + : PRIMTYPE IDENTIFIER '=' expression ';' + | structName ('?')? IDENTIFIER '=' expression ';' + ; + +returnStatement + : 'return' expression ';' + ; + +expression + : comparisonExpression + ; + +comparisonExpression + : additiveExpression (('==' | '!='| '>'| '<'| '>='| '<=') additiveExpression)* + ; + +additiveExpression + : multiplicativeExpression (('+' | '-') multiplicativeExpression)* + ; + +multiplicativeExpression + : unaryExpression (('*' | '/') unaryExpression)* + ; + +unaryExpression + : ('-') unaryExpression + | '!' unaryExpression + | primaryExpression + ; + +newExpression + : 'new' IDENTIFIER + ; + +fieldExpression + : primaryExpression '.' IDENTIFIER + ; + +primaryExpression + : IDENTIFIER + | INTEGER_LITERAL + | 'true' + | 'false' + | 'null' + | newExpression + | '(' expression ')' + | fieldExpression + ; + +INTEGER_LITERAL + : [1-9][0-9]* + | [0] + ; + +IDENTIFIER + : NON_DIGIT (NON_DIGIT | DEC_DIGIT)* + ; + +NON_DIGIT: [a-zA-Z_]; +DEC_DIGIT: [0-9]; +``` diff --git a/chapter13/docs/13-grammar.md b/chapter13/docs/13-grammar.md index 4e7ae5b4..40cfc432 100644 --- a/chapter13/docs/13-grammar.md +++ b/chapter13/docs/13-grammar.md @@ -1,5 +1,7 @@ # Grammar for Chapter 13 +English | [日本語](13-grammar.ja.md) + ```antlrv4 grammar SimpleLanguage; diff --git a/chapter14/README.ja.md b/chapter14/README.ja.md new file mode 100644 index 00000000..76d8bf4f --- /dev/null +++ b/chapter14/README.ja.md @@ -0,0 +1,409 @@ +# 第14章: 数値型 + +[English](README.md) | 日本語 + +[前: 第13章](../chapter13/README.ja.md) | +[次: 第15章](../chapter15/README.ja.md) + +この文書は [原文](README.md) の日本語訳です。 + +# 目次 + +1. [浮動小数点演算](#浮動小数点演算) +2. [狭いワード型](#狭い型とサブワード型) +3. [オーバーフローの処理](#オーバーフローの処理) +4. [整数の範囲](#整数の範囲) +5. [ビット演算](#ビット演算) +6. [優先順位](#優先順位) +7. [型の束](#型の実装) + +> 訳注: 原文の目次にある「Nodes」に対応する節は、原文本文にはありません。 + +構造体、参照フィールド、スケジューリングがそろったので、この章では、それらが運ぶ数値を拡張します。浮動小数点演算、狭い整数型と浮動小数点型、整数の範囲を追加します。 + +[linear](https://github.com/SeaOfNodes/Simple/tree/linear) ブランチの線形の Git リビジョン履歴で[この章](https://github.com/SeaOfNodes/Simple/tree/linear-chapter14)を読むことや、前章との[差分](https://github.com/SeaOfNodes/Simple/compare/linear-chapter13...linear-chapter14)を見ることもできます。 + +## 浮動小数点演算 + +浮動小数点値は `flt` 型から始まり、IEEE754 の64ビット演算を行います。整数式は、浮動小数点式に含まれると自動的に浮動小数点へ拡張されます。 + +```java +flt f = 0.5; +int x = 2; +f = f+x; // Auto-widen x to 2.0, result is 2.5 +f = f+true; // Since bools are just synonyms for integers 0,1 they auto-widen also +``` + +今のところ、逆に `flt` を丸めて `int` に戻す方法はありません。 + +```java +int x = 3.14; // error +``` + +平方根を計算するニュートン法を示します。 + +```java +flt guess = arg; // Initial guess is just argument +while( true ) { + // Next guess from Newton's method + flt next = (arg/guess + guess)/2; + // Break if hit fixed point + if( next == guess ) break; + guess = next; +} +return guess; +``` + +引数 2 で実行すると 1.414213562373095 を生成します。 + +浮動小数点演算には専用の Node があります。 + +| ノード | 演算 | +|---|---| +| AddF, SubF, MulF, DivF | 基本的な二項浮動小数点演算 | +| MinusF | 単項の符号反転 | +| EQF, LTF, LEF | 浮動小数点の比較演算子 | + +パーサーは [`Node.widen()`](src/main/java/com/seaofnodes/simple/node/Node.java) を使って浮動小数点演算を選び、`ToFloatNode` による変換を挿入します。浮動小数点の束は、範囲導入前の整数の束のように、`TOP` と `BOT` の間に定数を持ちます。[`TypeFloat`](src/main/java/com/seaofnodes/simple/type/TypeFloat.java) は、値が `f32` に収まるかどうかも記録します。`f32` への代入では、その精度に丸めます。浮動小数点から整数への変換は、別の未実装の言語機能として残ります。 + +## 狭い型とサブワード型 + +狭い整数型、またはサブワード整数型とは、システムやプログラミング言語が提供する標準的な「全幅」(通常32または64ビット)の整数型よりも、使用するメモリ(ビット数)が少ない整数データ型のことです。 + +配列、特にあらゆるネットワークコードでよく使うバイト配列の前段階として、Simple にはサブワード整数型を操作する方法が必要です。整数と浮動小数点の演算は、常に64ビットで計算しますが、変数やフィールドへの代入時には切り詰め(丸め)、ロード時には(符号)拡張します。 + +```java +i8 x = 123456789; +if( x != 21 ) + return false; +return true; +``` + +変数 `x` は符号付き8ビットに制限されます。非常に大きい値 `123456789`(16進数では `0x75BCD15`)を代入すると、単に8ビットに切り詰めて `21 (0x15)` になります。ロード時には符号拡張されます。 + +```java +i16 x = 123456789; +if( x != -13035 ) + return false; +return true; +``` + +変数 `x` は符号付き16ビットに制限されます。非常に大きい値 `0x75BCD15` を代入すると、単に16ビットに切り詰めて `0xCD15` になります。ロード時には符号拡張され、`-13035 (0xFFFF FFFF FFFF CD15)` になります。 + +```java +u16 x = 123456789; +if( x != 52501 ) + return false; +return true; +``` + +変数 `x` は符号なし16ビットに制限されます。非常に大きい値 `0x75BCD15` を代入すると、単に16ビットに切り詰めて `0xCD15` になります。ロード時にはゼロ拡張され、`52501 (0x0000 0000 0000 CD15)` になります。 + +同様に、浮動小数点数を IEEE754 の標準32ビット形式に制限できます。代入時には丸め、ロード時には64ビットに拡張します。 + +これでプリミティブ型の完全な一覧は次のようになります。 + +| データ型 | サイズ(ビット) | 説明 | +|---|---|---| +| `int`, `i64` | 64 | 現代のハードウェアに対応する64ビット符号付き整数。 | +| `u64` | 64 | **未実装**。64ビットを超える振る舞いを示唆します。 | +| `i32` | 32 | 32ビット符号付き整数。ロード時に符号拡張。 | +| `u32` | 32 | 32ビット符号なし整数。ロード時にゼロ拡張。 | +| `i16` | 16 | 16ビット符号付き整数。ロード時に符号拡張。 | +| `u16` | 16 | 16ビット符号なし整数。ロード時にゼロ拡張。 | +| `i8` | 8 | 8ビット符号付き整数。ロード時に符号拡張。 | +| `u8` | 8 | 8ビット符号なし整数。ロード時にゼロ拡張。 | +| `i1` | 1 | 1ビット符号付き整数。ロード時に符号拡張。 | +| `u1` | 1 | 1ビット符号なし整数。ロード時にゼロ拡張。 | +| `bool` | 1 | `u1` の別名。 | +| `flt`, `f64` | 64 | IEEE 754 の64ビット浮動小数点数。一般に double と呼ばれます。 | +| `f32` | 32 | IEEE 754 の32ビット浮動小数点数。 | + +## オーバーフローの処理 + +整数変数が保持できる最大値より大きな値を、その変数に格納しようとすると整数オーバーフローが起きます。オーバーフローにはいくつかの場合があります。 + +#### 符号なし(固定幅)整数のオーバーフロー + +```java +u8 a = 255; +u8 b = a + 1; // match u8 type against expr.type +// a = 256 +// u8 = [0...255] +return b; +``` + +式の型は、指定した型に一致しません。expr: ConstantNode(256) t: u8(0, 255) + +```java +private Node parseExpressionStatement() { + Type t = type(); // u8 = [0...255] + ... + // expr.type = TypeInteger(256); + // Auto-narrow wide ints to narrow ints + expr = zsMask(expr,t); +} +``` + +符号なしなので、指定した型からあふれる新しい値にビットマスクをかけるだけです。「ビットごとの AND」のオペランドは、値(256)とマスク(255、上限)です。これを切り詰めと呼びます。 + +`ZsMask:` + +```java + // zero/sign extend. "i" is limited to either classic unsigned (min==0) or + // classic signed (min=minus-power-of-2); max=power-of-2-minus-1. +private Node zsMask(Node val, Type t ) { + if( !(val._type instanceof TypeInteger tval && t instanceof TypeInteger t0 && !tval.isa(t0)) ) { + if( !(val._type instanceof TypeFloat tval && t instanceof TypeFloat t0 && !tval.isa(t0)) ) + return val; + // Float rounding + return new RoundF32Node(val).peephole(); + } + if( t0._min==0 ) // Unsigned + return new AndNode(val,new ConstantNode(TypeInteger.constant(t0._max)).peephole()).peephole(); + // Signed extension + int shift = Long.numberOfLeadingZeros(t0._max)-1; + Node shf = new ConstantNode(TypeInteger.constant(shift)).peephole(); + if( shf._type==TypeInteger.ZERO ) + return val; + return new SarNode(new ShlNode(val,shf.keep()).peephole(),shf.unkeep()).peephole(); +} +``` + +切り詰めは `AndNode` が行います。 + +```java +... +if( t0._min==0 ) // Unsigned + return new AndNode(val,con(t0._max)).peephole(); +``` + +これらは定数なので、peephole を呼ぶと `256 & 255` = 0 になります。出力は次の通りです。 + +``` +return 0; +``` + +#### 最大整数のオーバーフロー + +```java +i64 a = 9223372036854775807; +i64 b = a + 1; +return b; +``` + +加算の計算時には、オーバーフローしないことを確認します。 + +`AddNode.overflow:` + +```java +private static boolean overflow( long x, long y ) { + if( (x ^ y ) < 0 ) return false; // unequal signs, never overflow + return (x ^ (x + y)) < 0; // sum has unequal signs, so overflow +} +``` + +`AddNode.compute` + +```java +... +// Fold ranges like {0-1} + {2-3} into {2-4}. +if( !overflow(i1._min,i2._min) && + !overflow(i1._max,i2._max) ) + return TypeInteger.make(i1._min+i2._min,i1._max+i2._max); +``` + +ここで **x + y** はオーバーフローして**負**になり、x ^ 負の値 = 負の値になります。負の数はすべて 0 より小さいので、true を返します。**オーバーフロー**する場合、TypeInteger のボトム型を返すだけです。 + +``` + return TypeInteger.BOT; // [] +``` + +ただし、この例では定数を扱っているので、ここではそのまま自然にオーバーフローさせます。 + +```java +// i1: 9223372036854775807 +// i2: 1 +// i1 + i2 will overflow. +return TypeInteger.constant(i1.value()+i2.value()); // -9223372036854775808; +``` + +#### 定数の範囲の加算 + +```java +u8 a = 12; +u8 b = 13; +u8 c = 124; +u8 d = a + b + c; +return d; +``` + +`is_con` が true なら、一般に `min = max` と言えます。したがって次のようになります。 + +```java +public static TypeInteger make(boolean is_con, long con) { + return make(is_con ? con : (con==0 ? Long.MAX_VALUE : Long.MIN_VALUE), + is_con ? con : (con==0 ? Long.MIN_VALUE : Long.MAX_VALUE)); +``` + +- この結果は次の通りです。 + +```java +return make(con, con); +``` + +`value()` は定数に対してだけ呼ばれます。つまり `_min == _max;` という不変条件が成り立つため、`min_` と `max_` は等しく、どちらを返してもかまいません。 + +これは通常の加算と同じです。 + +```java +public long value() { assert isConstant(); return _min; } +``` + +#### さらに切り詰める + +```java +u8 a = arg; // arg & 255 +u8 b = arg; // arg & 255 (same as a) +u8 c = a + b; // (arg&255)*2, type = TypeInteger.BOT +return c; // (((arg&255)*2)&255), type = TypeInteger.U8 +``` + +前と同じく、**arg** は **(arg&255)** になります。このノードを **GVN** テーブルに挿入し、**b** はその値を取得します。AddNode のピープホール最適化はこれらを `((arg&255)*2)` に変え、式に追加のマスクをかけて有効な境界内に保ちます。 + +## 整数の範囲 + +一般的な範囲の計算については、Hacker's Delight を参照してください。[^1] + +## ビット演算 + +次のビット演算子をサポートします。 +AND `&`、OR `|`、XOR `^`、左シフト `<<`、右シフト `>>`、ゼロ埋め右シフト `>>>` + +| 演算子 | 記号 | 説明 | +|---|---|---| +| **AND** | `&` | ビットごとの AND 演算。 | +| **OR** | `\|` | ビットごとの OR(包含的論理和)演算。 | +| **XOR** | `^` | ビットごとの XOR(排他的論理和)演算。 | +| **左シフト** | `<<` | ビットを左にずらし、ゼロで埋めます。 | +| **右シフト** | `>>` | ビットを右にずらし、空いた位置に符号ビットの値を複製して埋めます。 | +| **符号なし右シフト** | `>>>` | ビットを右にずらし、空いた位置を常にゼロで埋めます。 | + +`>>>` は算術シフトではなく論理シフトです。 + +これらの論理演算のピープホール最適化の例を示します。 + +#### AndNode: + +##### lhs & -1 = lhs; + +```java +// And of -1. We do not check for (-1&x) because this will already +// canonicalize to (x&-1) +if( t2.isConstant() && t2 instanceof TypeInteger i && i.value()==-1 ) +return lhs; + +``` + +#### OrNode: + +##### lhs | 0 = lhs; + +```java +// Or of 0. We do not check for (0|x) because this will already +// canonicalize to (x|0) +if( t2.isConstant() && t2 instanceof TypeInteger i && i.value()==0 ) +return lhs; + +``` + +#### 右シフト + +lhs >> 0 = lhs; + +```java +// Sar of 0. +if( t2.isConstant() && t2 instanceof TypeInteger i && (i.value()&63)==0 ) + return lhs; +``` + +#### 左シフト + +lhs << 0 = lhs; + +```java +// Shl of 0. +if( t2.isConstant() && t2 instanceof TypeInteger i && (i.value()&63)==0 ) + return lhs; + +``` + +#### 符号なし右シフト + +lhs >>> 0 = lhs; + +```java +// Shr of 0. +if( t2.isConstant() && t2 instanceof TypeInteger i && (i.value()&63)==0 ) +return lhs; +``` + +#### XorNode: + +##### lhs ^ 0 = lhs; + +```java +// Xor of 0. We do not check for (0^x) because this will already +// canonicalize to (x^0) +if( t2.isConstant() && t2 instanceof TypeInteger i && i.value()==0 ) + return lhs; + +``` + +### 優先順位 + +ビット演算子の優先順位は最も低くなります。たとえば次のようになります。 + +``` +arg + 123 & 3 = (arg + 123) & 3 +``` + +## 型の実装 + +の `TypeInteger` クラスを作り直し、min/max 値のあらゆる範囲をサポートします。今のところ、プログラマーに公開するのはサイズが2のべき乗になる一部の範囲だけですが、最適化器の内部ではすべての範囲をサポートします。MEET 操作は最小値の最小と最大値の最大を取ります。DUAL 操作は最小値と最大値を入れ替えます。 + +例: 定数 `0` は `[0...0]` です。 + +例: `bool` 型は範囲 `[0...1]` を持ちます。 + +例: `u8` 型は範囲 `[0...255]` を持ちます。 + +例: `i8` 型は範囲 `[-128...127]` を持ちます。 + +例: `bool` の `dual` は `[1...0]` です(最小値と最大値を交換するだけです)。 + +ここで導入するの `TypeFloat` クラスは、32ビットと64ビットの両方のサイズをサポートします。 + +整数範囲の図は模式図で、取り得る範囲の例としてブール値の範囲とその双対を示しています。ほかの領域は、前の章の名前付き構造体、null 許容ポインタ、精密なメモリエイリアスを維持します。 + + + +### meet: + +2つの範囲の Meet は次の通りです。 + +```java +@Override +public Type xmeet(Type other) { +// Invariant from caller: 'this' != 'other' and same class (TypeInteger) +TypeInteger i = (TypeInteger)other; // Contract +return make(Math.min(_min,i._min), Math.max(_max,i._max)); +} +``` + +これによって、どちらかの範囲内に起こり得るすべての値を組み合わせます。また、実装もごく単純です。 + +[^1]: Hacker's delight. + 4-2 Propagating Bounds through Adds and Subtracts + +次の章では、これらの数値型を配列の長さ、添字、バイト配列を含む要素の格納に使います。 diff --git a/chapter14/README.md b/chapter14/README.md index 84207f52..dbfaa549 100644 --- a/chapter14/README.md +++ b/chapter14/README.md @@ -1,5 +1,7 @@ # Chapter 14: Numeric Types +English | [日本語](README.ja.md) + [Previous: Chapter 13](../chapter13/README.md) | [Next: Chapter 15](../chapter15/README.md) diff --git a/chapter14/docs/14-grammar.ja.md b/chapter14/docs/14-grammar.ja.md new file mode 100644 index 00000000..0b52ecdd --- /dev/null +++ b/chapter14/docs/14-grammar.ja.md @@ -0,0 +1,128 @@ +# 第14章の文法 + +[English](14-grammar.md) | 日本語 + +この文書は [原文](14-grammar.md) の日本語訳です。 + +[第14章に戻る](../README.ja.md) + +```antlrv4 +grammar SimpleLanguage; + +program : block EOF ; + +block : statement+ ; + + +statement + : '{' block '}' + | returnStatement + | ifStatement + | whileStatement + | breakStatement + | continueStatement + | structDeclaration + | expressionStatement + ; + + +returnStatement : 'return' expression ';' ; + +ifStatement : 'if' '(' expression ')' statement ('else' statement)? ; + +whileStatement : 'while' '(' expression ')' statement ; + +breakStatement : 'break' ';' ; + +continueStatement : 'continue' ';' ; + + +structDeclaration : 'struct' IDENTIFIER '{' fields '}' ; + +fields : field+ ; + +field : type IDENTIFIER ';' ; + + +expressionStatement + : type IDENTIFIER ';' + | type IDENTIFIER '=' expression ';' + | IDENTIFIER '=' expression ';' + | expression + ; + +PRIMTYPE + : 'int' + | 'i8' + | 'i16' + | 'i32' + | 'i64' + | 'u8' + | 'u16' + | 'u32' + | 'flt' + | 'f32' + | 'f64' + | 'bool' + ; + +type + : PRIMTYPE + | typeName ('?')? + ; + +typeName : IDENTIFIER ; + + +expression : bitWiseExpression ; + +bitWiseExpression + : comparisonExpression ( '&' | '|' | '^' ) comparisonExpression)* + ; + +comparisonExpression + : shiftExpression (('==' | '!='| '>'| '<'| '>='| '<=') shiftExpression)* + ; + +shiftExpression + : additiveExpression ( '<<' | '>>>' | '>>' ) additiveExpression)* + ; + +additiveExpression + : multiplicativeExpression (('+' | '-') multiplicativeExpression)* + ; + +multiplicativeExpression + : unaryExpression (('*' | '/') unaryExpression)* + ; + +unaryExpression + : ('-') unaryExpression + | '!' unaryExpression + | primaryExpression postFix* + ; + +primaryExpression + : INTEGER_LITERAL + | '(' expression ')' + | 'true' + | 'false' + | 'null' + | newExpression + | IDENTIFIER + ; + +newExpression : 'new' IDENTIFIER ; + +postFix : '.' IDENTIFIER [ '=' expression ] ; + +INTEGER_LITERAL + : [1-9]DIGIT* + | [0] + ; + +IDENTIFIER : NON_DIGIT (NON_DIGIT | DIGIT)* ; + +NON_DIGIT: [a-zA-Z_]; +DIGIT: [0-9]; +``` diff --git a/chapter14/docs/14-grammar.md b/chapter14/docs/14-grammar.md index f23f81cd..a5258925 100644 --- a/chapter14/docs/14-grammar.md +++ b/chapter14/docs/14-grammar.md @@ -1,5 +1,7 @@ # Grammar for Chapter 14 +English | [日本語](14-grammar.ja.md) + ```antlrv4 grammar SimpleLanguage; diff --git a/chapter15/README.ja.md b/chapter15/README.ja.md new file mode 100644 index 00000000..b1ff339b --- /dev/null +++ b/chapter15/README.ja.md @@ -0,0 +1,188 @@ +# 第15章: 配列 + +[English](README.md) | 日本語 + +[前: 第14章](../chapter14/README.ja.md) | +[次: 第16章](../chapter16/README.ja.md) + +この文書は [原文](README.md) の日本語訳です。 + +# 目次 + +1. [パーサーの変更](#パーサー) +2. [配列型は TypeStruct](#配列型は-typestruct) +3. [割り当てと部分メモリ](#割り当てと部分メモリ) +4. [新しいアドレス計算](#新しいアドレス計算) +5. [構造体の配置](#構造体の配置) +6. [単純なアドレス計算のピープホール最適化](#単純なアドレス計算のピープホール最適化) +7. [考察](#考察) + +ついに配列を導入します。配列にはもっと複雑なアドレス計算が必要なので、Load と Store のアドレス指定を、フィールドの*名前*から実際の*オフセット*へ変更しなければなりません。そのため、この章では構造体フィールドの*配置*も行います。 + +配列は標準的な `new int[len]` 構文で作り、任意の基本型と、長さを表す任意の整数式を使えます。読み書きも通常の形式で、`Person p = persons[idx];` や `persons[idx] = new Person;` とします。長さには後置の `#`、たとえば `persons#` でアクセスします。 + +Java と同じく、配列には常に安全性のチェックがあり、実行時チェックに失敗すると評価器はパニックになります。 + +- 範囲外の添字: +- `ary[-1]` +- `ary[ary#]` +- 不正な長さの配列の作成: +- `new int[-1]; // Negative length`(負の長さ) +- `new int[1<<63]; // Too large`(大きすぎる) +- `new int[3.14]; // Not an integer`(整数ではない) + +[linear](https://github.com/SeaOfNodes/Simple/tree/linear) ブランチの線形の Git リビジョン履歴で[この章](https://github.com/SeaOfNodes/Simple/tree/linear-chapter15)を読むことや、前章との[差分](https://github.com/SeaOfNodes/Simple/compare/linear-chapter14...linear-chapter15)を見ることもできます。 + +## パーサー + +配列をサポートするパーサーの変更は、ごく小さなものです。主な変更は、`type()` のパース時に配列構文を探すことです。`int[]` が有効な型になります。配列は任意の次元を許し、基本型も任意なので、たとえば `Person[][]?` は null である可能性がある `Person` の2次元配列です。 + +配列は常にゼロ/null の本体で作られるので、暗黙に常に `null` 値を許容します。 + +新しい配列は、新しいオブジェクトとほぼ同じ構文、`new int[99]` で作ります。長さは必須で、任意の整数式を使えます。たとえば `new int[arg]` は長さ `arg` の整数配列です。多次元配列では最も外側の次元だけを作ります。要素自体はすべて null で、次に低い次元の配列として型付けされます。 + +配列は暗黙に2つのフィールド、長さと本体を持ちます。長さは割り当て時に設定される読み取り専用フィールドで、後置 `#` 演算子、たとえば `ary#` で参照します。配列の合計を求める例です。 + +```java +int[] ary = new int[arg]; +int sum = 0; +int i = 0; +while( i < ary# ) { + sum = sum + ary[i]; + i = i + 1; +} +``` + +配列には2つの暗黙のフィールドがあるので、基本配列型ごとに2つの一意なエイリアス番号を得ます。したがって `int[]` と `flt[]` の型は決してエイリアスしませんが、すべての `int[]` 型はエイリアスします。また、配列本体へのすべての参照も互いにエイリアスします。このため、ループ内で配列へのロードとストアを混在させると、保守的なコード(最適化なし)になることがあります。一部は後の章で対処できます。単純な1次元ステンシル計算は大幅に最適化できますが、Simple コンパイラは複雑なベクトル化を目標にしていません。 + +## 配列型は TypeStruct + +配列は固有の型を持ちますが、実際には `#` と `[]` という名前のフィールドを持つ `TypeStruct` にすぎません。これらはパーサーでフィールド名として使えないので、ユーザーが配列と混同される独自の構造体を書くことはできません。 + +これらの `TypeStruct` 配列は、ほかの点では `TypeStruct` とまったく同じように振る舞います。 + +## 割り当てと部分メモリ + +この章でも、第11章の1つの隠れた `$mem` 変数と、遅延的な `BulkMemPhi`/`MemPhi` 分割を維持します。Start は `{ctrl, $mem, arg}` を生成し、Return は `{ctrl, $mem, value}` を消費します。配列の初期化によって New が変わります。別々の初期化 Store の代わりに、New がオブジェクトを割り当ててゼロ化します。 + +New は `{ctrl, $mem, size}` を消費し、`{ptr, $mem}` だけを生成します。New に制御の結果がないため、ポインタは結果 0 です。メモリ入力と結果は、New 内部の `TypeStruct` のフィールドのエイリアスだけを覆います。これらは `TypeMemPtr` からも得られます。エイリアス番号は識別子であり、射影の添字ではありません。 + +エイリアス `A` に作用する割り当てについて、パーサーは次を構築します。 + +```text +input = MemMerge(default: absent, each a in A: before[a]) +new = New(ctrl, input, size) +ptr = Proj(new, 0) +after = MemMerge(default: before, each a in A: Proj(new, 1)) +``` + +影響を受けるすべてのエントリは、同じメモリ射影を共有します。入力の MemMerge は部分的です。null のデフォルトは、列挙されていないエイリアスを**覆わない**ことを意味します。出力の集約は、関係のないエイリアスについて前のメモリを保持します。空の構造体は空の部分入力を持ち、メモリ全体を変更しません。 + +メモリ型は、エイリアスと、そのスライスに格納される値の型を対にするようになります。New を通るエイリアス問い合わせは、流入する内容とフィールドの初期値(整数ゼロ、浮動小数点ゼロ、null)を組み合わせます。問い合わせは集約と割り当てをたどり、キャッシュされた Phi の型で止まり、最適化器の依存関係を記録します。MemPhi は、自分の1つのエイリアスについてこれらの値の型を合流します。エイリアスごとの New 射影も、新しい束の領域も不要です。 + +New 自身のポインタを使う Load は、初期化された値に畳み込めます。別の New から来たポインタは、割り当てを迂回してそのメモリ入力へ進めます。単にノードが異なるだけでは不十分です。Phi やキャストが、依然として割り当てられたオブジェクトを表す可能性があるためです。配列の長さは後続の Store で設定し、本体は New がゼロ化します。配列アクセスは境界チェックの代理として、既存の制御入力を保持します。 + +両スケジューラは、MemMerge を通してエイリアスをたどり、New のメモリへの作用を見つけます。MemMerge 自体はメモリを上書きしませんが、割り当て前の読み出しと、既存オブジェクトへの後続の書き込みの順序を隠してはいけません。 + +## 新しいアドレス計算 + +配列では添字でアクセスするためのアドレス計算が必要なので、Load と Store にオフセットを計算する方法が必要です。以前の Load と Store は構造体だけを対象にし、内部にフィールド名を持っていました。つまり暗黙のアドレス指定はフィールド名によるもの(文字列検索のようなもの)でした。この章で新たに構造体の*配置*を作り、すべての構造体のフィールドのオフセットを計算します。 + +Load と Store はフィールド名の代わりに `off` を取るようになり、フィールド名はデバッグ用にだけ含めます。 + +```java +public MemOpNode(String name, int alias, Type glb, Node mem, Node ptr, Node off) { + super(null, mem, ptr, off); + _name = name; + _alias = alias; + _declaredType = glb; +} +``` + +配列の `off` は、通常の配列計算の手順で計算します。 + +`off = base + (idx << scale)` + +配列については要素型からベースと添字を計算し、パーサーがオフセットを構築するのに使います。構造体については配置を使います。 + +```java +// Get field type and layout offset from base type and field index fidx +Field f = base._fields[fidx]; // Field from field index +Node off; +if( name.equals("[]") ) { // If field is an array body + // Array index math + Node idx = require(parseExpression(),"]"); + off = new AddNode(con(base.aryBase()),new ShlNode(idx,con(base.aryScale())).peephole()).peephole(); +} else { // Else normal struct field + // Hardwired field offset + off = con(base.offset(fidx)); +} +``` + +## 構造体の配置 + +この章で新しく導入する構造体の*配置*は、特定のフィールドを格納するメモリブロック内の固定オフセットです。`TypeStruct` でオフセットを計算し、キャッシュします。すべてのフィールド型は `log_size` を持つようになります。フィールドはサイズ順に詰め込み、アラインメント用のパディングを入れ、構造体全体も8の倍数までパディングします。したがって実際のフィールドオフセットとフィールドの宣言順序は*無関係*です。大きなフィールドを低い位置に詰め、小さなフィールドをその次に詰めます。 + +```java +struct s { + i8 b; // log_size=0 + u16 char; // log_size=1 + Person p; // log_size=2 + flt pi; // log_size=3 +} +// Layout will be: +struct s { + flt pi; // offset= 0, log_size=3 + Person p; // offset= 8, log_size=2 + u16 char; // offset=12, log_size=1 + i8 b; // offset=14, log_size=0 + void pad; // offset=15, size = 1 +} // size =16 +``` + +フィールドのサイズは、おそらく `TypeInteger` を除けばほぼ明らかです。浮動小数点は32ビットか64ビットです。ポインタは `TypeMemPtr` で4バイトに設定しており、後により広いポインタ範囲をサポートするため変更できます。`TypeInteger` は、ユーザーに見える固定された整数範囲の集合についてだけ、有効な `log_size` フィールドを持ちます。Simple では `compute` の呼び出しでほかの範囲も可能ですが、フィールドの基本型には決して現れません。 + +`struct S { i16 x; }` + +ここでは `x` のオフセットは 0、サイズは2バイトで、`S` はサイズ 8 までパディングされます。 + +`S s = new S; s.x = 3;` + +同じ `x` フィールドが今度は 0 または 3 を保持するため、`x` の `TypeInteger` は範囲 `[0-3]` になります。これはたった2ビットで表せますが、フィールドサイズの計算には使いません。 + +## 単純なアドレス計算のピープホール最適化 + +0 だけの左シフトは恒等操作です。`(x<<0)` は `x` になります。 + +定数加算に対する左シフトは分配して、さらに定数畳み込みを可能にします。`(x + c) << i` は `(x << i) + (c << i)` になります。 + +つまり IR は次のようになります。 + +`x[i]` は `16/*base*/ + (i << 3/*scale*/)` となり、`Load(mem,x,Add(Shl(i,3),16))` になります。 + +`x[i+1]` は `16/*base*/ + ((i+1) << 3/*scale*/)` となり、`Load(mem,x,Add(Shl(i,3),24))` になります。 + +`idx+1` の `+1` がベースの計算に畳み込まれる点に注目してください。 + +また、この形は X86 のアドレッシングモードにぴったりです。 + +## 考察 + +テストケースには、単純な移動和とエラトステネスの篩が含まれます。 + +```java +int i=0; +while( i < ary# ) { + ary[i] = i; + i = i+1; +} +``` + +ここでは `i++` 演算子と `for` ループがないことが、はっきり見えます。別の構文を想像できます。 + +```java +for( int i=0; i < ary#; i++ ) + ary[i] = i; +``` + +これは既存のパーサーの上の糖衣構文だけで実現でき、配列をループする構文をずっと明快にできます。ここには多くの選択肢があり、別の章で集中的に扱うべき話題です。 diff --git a/chapter15/README.md b/chapter15/README.md index db0f2f1c..1ce5badf 100644 --- a/chapter15/README.md +++ b/chapter15/README.md @@ -1,5 +1,7 @@ # Chapter 15: Arrays +English | [日本語](README.ja.md) + [Previous: Chapter 14](../chapter14/README.md) | [Next: Chapter 16](../chapter16/README.md) diff --git a/chapter15/docs/15-grammar.ja.md b/chapter15/docs/15-grammar.ja.md new file mode 100644 index 00000000..22cbab5c --- /dev/null +++ b/chapter15/docs/15-grammar.ja.md @@ -0,0 +1,135 @@ +# 第15章の文法 + +[English](15-grammar.md) | 日本語 + +この文書は [原文](15-grammar.md) の日本語訳です。 + +[第15章に戻る](../README.ja.md) + +```antlrv4 +grammar SimpleLanguage; + +program : block EOF ; + +block : statement+ ; + + +statement + : '{' block '}' + | returnStatement + | ifStatement + | whileStatement + | breakStatement + | continueStatement + | structDeclaration + | expressionStatement + ; + + +returnStatement : 'return' expression ';' ; + +ifStatement : 'if' '(' expression ')' statement ('else' statement)? ; + +whileStatement : 'while' '(' expression ')' statement ; + +breakStatement : 'break' ';' ; + +continueStatement : 'continue' ';' ; + + +structDeclaration : 'struct' IDENTIFIER '{' fields '}' ; + +fields : field+ ; + +field : type IDENTIFIER ';' ; + + +expressionStatement + : type IDENTIFIER ';' + | type IDENTIFIER '=' expression ';' + | IDENTIFIER '=' expression ';' + | expression + ; + +PRIMTYPE + : 'int' + | 'i8' + | 'i16' + | 'i32' + | 'i64' + | 'u8' + | 'u16' + | 'u32' + | 'flt' + | 'f32' + | 'f64' + | 'bool' + ; + +type + : PRIMTYPE + | typeName '?'? + | typeName nestedArray+ + ; + +nestedArray : '?'? '[]'* ; + +typeName : IDENTIFIER ; + + +expression : bitWiseExpression ; + +bitWiseExpression + : comparisonExpression ( '&' | '|' | '^' ) comparisonExpression)* + ; + +comparisonExpression + : shiftExpression (('==' | '!='| '>'| '<'| '>='| '<=') shiftExpression)* + ; + +shiftExpression + : additiveExpression ( '<<' | '>>>' | '>>' ) additiveExpression)* + ; + +additiveExpression + : multiplicativeExpression (('+' | '-') multiplicativeExpression)* + ; + +multiplicativeExpression + : unaryExpression (('*' | '/') unaryExpression)* + ; + +unaryExpression + : ('-') unaryExpression + | '!' unaryExpression + | primaryExpression postFix* + ; + +primaryExpression + : INTEGER_LITERAL + | '(' expression ')' + | 'true' + | 'false' + | 'null' + | newExpression + | IDENTIFIER + ; + +newExpression : 'new' IDENTIFIER [ '[' expression ']' ] ; + +postFix + : '.' IDENTIFIER [ '=' expression ] + | '[' expression ']' [ '=' expression ] + | '#' + ; + +INTEGER_LITERAL + : [1-9]DIGIT* + | [0] + ; + +IDENTIFIER : NON_DIGIT (NON_DIGIT | DIGIT)* ; + +NON_DIGIT: [a-zA-Z_]; +DIGIT: [0-9]; +``` diff --git a/chapter15/docs/15-grammar.md b/chapter15/docs/15-grammar.md index 76ea0cab..d524c560 100644 --- a/chapter15/docs/15-grammar.md +++ b/chapter15/docs/15-grammar.md @@ -1,5 +1,7 @@ # Grammar for Chapter 15 +English | [日本語](15-grammar.ja.md) + ```antlrv4 grammar SimpleLanguage; diff --git a/chapter16/README.ja.md b/chapter16/README.ja.md new file mode 100644 index 00000000..e6b1f2d3 --- /dev/null +++ b/chapter16/README.ja.md @@ -0,0 +1,107 @@ +# 第16章: コンストラクタ + +[English](README.md) | 日本語 + +[前: 第15章](../chapter15/README.ja.md) | +[次: 第17a章](../chapter17a/README.ja.md) + +この文書は [原文](README.md) の日本語訳です。 + +# 目次 + +1. [コンストラクタ](#コンストラクタ) +2. [初期化コード](#初期化コード) +3. [複数の宣言](#同じ型の複数の宣言) +4. [コンストラクタを通るメモリ](#コンストラクタを通るメモリ) + +この章では、フィールドのデフォルト値とコンストラクタブロックを追加します。第17a章では、これらのコンストラクタで初期化する、固定の束縛と読み取り専用のアクセスを追加します。 + +[linear](https://github.com/SeaOfNodes/Simple/tree/linear) ブランチの線形の Git リビジョン履歴で[この章](https://github.com/SeaOfNodes/Simple/tree/linear-chapter16)を読むことや、前章との[差分](https://github.com/SeaOfNodes/Simple/compare/linear-chapter15...linear-chapter16)を見ることもできます。 + +## コンストラクタ + +[第12章](../chapter12/README.ja.md)の参照には、null を許容しないフィールドが常に `null` から始まるという大きな問題があります。この章ではそれを修正し、null を許容しないフィールドの初期化に*コンストラクタ*構文を要求します。 + +フィールドの初期化には3つの方法があります。 + +1. 許容するフィールドには、デフォルトのゼロ/null 初期化を使います。特別な構文は不要です。 + +``` +int x; // x is initialized to 0 +struct Ref { Ref? ptr; }; // Instances of Ref will have ptr initialized to null +return new Ref.ptr; // Returns a null +``` + +2. 型の宣言で初期値を指定できます。それ以降の割り当てはこの値から始まります。 + +``` +int x = 5; // x is initialized to 5 +struct Point { int x=1; int y=1; }; // Point x and y will start as 1, not 0 +return new Point.x; // Returns a 1 +``` + +3. 割り当てで初期値を指定できます。 + +``` +struct Point { int x=1; int y=1; }; // Point x and y will start as 1, not 0 +return new Point { x=3; }.x; // Returns a 3 +``` + +null を許容しないフィールドは、最初に使用する前、かつ割り当ての終了前に、*必ず*初期化しなければなりません。宣言でも割り当てでも初期化できます。初期化でデフォルト値を与えることはできますが、デフォルト値から始まるわけではありません。 + +``` +struct Person { u8[] name; }; +return new Person; // ERROR: 'Person' is not fully initialized, field 'name' needs to be set in a constructor +``` + +## 初期化コード + +パーサーは、型の宣言と割り当てのどちらにも、完全な Block 文をパースすることを許します。`while` ループや `return` を含め、任意の量のコードが合法です。 + +``` +struct Square { + flt side = arg; + flt diag = arg*arg/2; + // Newton's approximation to the square root, computed in a constructor. + // The actual allocation will copy in this result as the initial + // value for 'diag'. + while( 1 ) { + // The next-guess variable "next" is not a field in Square, + // because it does not appear at the top level + flt next = (side/diag + diag)/2; + if( next == diag ) break; + diag = next; + } +}; +return new Square; +``` + +別の例です。 + +``` +struct Buffer { + if( arg < 0 || arg > 1000000 ) + return null; // Size out of bounds + u8[] buffer = new u8[arg]; +}; +return new Buffer; +``` + +### 同じ型の複数の宣言 + +この章では新たに、ほかの言語でよく見られる、同じ型の複数の宣言も許します。 + +``` +int x,y; // Two int variables declared +struct Point { int x,y,z; }; // Three fields declared +``` + +## コンストラクタを通るメモリ + +[第11章](../chapter11/README.ja.md)の遅延的なメモリ分割は、コンストラクタを通して続きます。パーサーは、スカラー変数と並んで1つの `$mem` 変数を追跡します。分岐とループは BulkMemPhi でその束縛を合流し、フィールドアクセスは必要に応じて精密な MemPhi を分離します。宣言型の変数レコードは ScopeNode にあります。パーサーに別個のメモリエイリアステーブルはありません。 + +コンストラクタは割り当て前にフィールド値を計算します。本体の読み出し、書き込み、ループは、通常のコードと同じ `$mem` 束縛を更新します。その後 New は `{ctrl, $mem, size, field values...}` を取り、`{ptr, $mem}` を生成します。入力メモリは構造体のエイリアスを含む部分 MemMerge で、出力メモリも同じエイリアスを覆います。メモリ全体の MemMerge は無関係なスライスを保持します。配列も同じ配置を使い、長さとデフォルトの要素値を初期化入力として供給します。 + +自身の New からの Load は、対応する初期化入力を使えます。エイリアスの内容は、初期値の型と古いオブジェクトからの流入内容を組み合わせるため、新しいオブジェクトが既存オブジェクトの事実や作用を消すことはありません。ロードが迂回するのは、別のものと証明された割り当てだけです。スケジューリングは、読み出しを後続の書き込みより前に順序付ける際、部分的な集約をたどります。 + +1つの割り当てメモリ結果を共有することで、Store を New に畳み込む処理はより保守的になります。既存の単独使用のチェックが、覆うすべてのエイリアスのユーザーを見るようになるためです。以前の別々のメモリ射影なら定数畳み込みできたところに、フィールドの Load が残ることがあります。Phi のくくり出しも、単純な1段階の Load 安全性チェックを維持します。 diff --git a/chapter16/README.md b/chapter16/README.md index c55de47f..a9d9422d 100644 --- a/chapter16/README.md +++ b/chapter16/README.md @@ -1,5 +1,7 @@ # Chapter 16: Constructors +English | [日本語](README.ja.md) + [Previous: Chapter 15](../chapter15/README.md) | [Next: Chapter 17a](../chapter17a/README.md) diff --git a/chapter16/docs/16-grammar.ja.md b/chapter16/docs/16-grammar.ja.md new file mode 100644 index 00000000..6660651d --- /dev/null +++ b/chapter16/docs/16-grammar.ja.md @@ -0,0 +1,136 @@ +# 第16章の文法 + +[English](16-grammar.md) | 日本語 + +この文書は [原文](16-grammar.md) の日本語訳です。 + +[第16章に戻る](../README.ja.md) + +```antlrv4 +grammar SimpleLanguage; + +program : block EOF ; + +block : statement+ ; + + +statement + : '{' block '}' + | returnStatement + | ifStatement + | whileStatement + | breakStatement + | continueStatement + | structDeclaration + | expressionStatement + ; + + +returnStatement : 'return' expression ';' ; + +ifStatement : 'if' '(' expression ')' statement ('else' statement)? ; + +whileStatement : 'while' '(' expression ')' statement ; + +breakStatement : 'break' ';' ; + +continueStatement : 'continue' ';' ; + + +structDeclaration : 'struct' IDENTIFIER '{' block '}' ; + +expressionStatement + : type IDENTIFIER ';' + | type IDENTIFIER '=' expression ';' + | IDENTIFIER '=' expression ';' + | expression + ; + +PRIMTYPE + : 'int' + | 'i8' + | 'i16' + | 'i32' + | 'i64' + | 'u8' + | 'u16' + | 'u32' + | 'flt' + | 'f32' + | 'f64' + | 'bool' + ; + +type + : PRIMTYPE + | typeName '?'? + | typeName nestedArray+ + ; + +nestedArray : '?'? '[]'* ; + +typeName : IDENTIFIER ; + + +expression : bitWiseExpression ; + +bitWiseExpression + : comparisonExpression ( '&' | '|' | '^' ) comparisonExpression)* + ; + +comparisonExpression + : shiftExpression (('==' | '!='| '>'| '<'| '>='| '<=') shiftExpression)* + ; + +shiftExpression + : additiveExpression ( '<<' | '>>>' | '>>' ) additiveExpression)* + ; + +additiveExpression + : multiplicativeExpression (('+' | '-') multiplicativeExpression)* + ; + +multiplicativeExpression + : unaryExpression (('*' | '/') unaryExpression)* + ; + +unaryExpression + : ('-') unaryExpression + | '!' unaryExpression + | primaryExpression postAssign + ; + +primaryExpression + : INTEGER_LITERAL + | FLOAT_LITERAL + | '(' expression ')' + | 'true' + | 'false' + | 'null' + | newExpression + | IDENTIFIER + ; + +newExpression + : 'new' IDENTIFIER [ '{' block '}' ] + | 'new' IDENTIFIER '[' expression ']' + ; + +postAssign : postFix [ '=' expression ] | [ '#' ]; + +postFix + : '.' IDENTIFIER postFix + | '[' expression ']' postFix + ; + + +IDENTIFIER : NON_DIGIT (NON_DIGIT | DIGIT)* ; + +INTEGER_LITERAL + : [1-9]DIGIT* + | [0] + ; + +NON_DIGIT: [a-zA-Z_]; +DIGIT: [0-9]; +``` diff --git a/chapter16/docs/16-grammar.md b/chapter16/docs/16-grammar.md index 7da725e9..f86e7269 100644 --- a/chapter16/docs/16-grammar.md +++ b/chapter16/docs/16-grammar.md @@ -1,5 +1,7 @@ # Grammar for Chapter 16 +English | [日本語](16-grammar.ja.md) + ```antlrv4 grammar SimpleLanguage; diff --git a/chapter17a/README.ja.md b/chapter17a/README.ja.md new file mode 100644 index 00000000..2bbb632c --- /dev/null +++ b/chapter17a/README.ja.md @@ -0,0 +1,100 @@ +# 第17a章: 可変性と読み取り専用ビュー + +[English](README.md) | 日本語 + +[前: 第16章](../chapter16/README.ja.md) | +[次: 第17b章](../chapter17b/README.ja.md) + +この文書は [原文](README.md) の日本語訳です。 + +第16章はオブジェクトにフィールドの初期値を与えます。今度は、どの束縛を再代入でき、どの参照が構築後の書き込みを許すかを区別します。フィールドや配列要素も含め、これらは独立した問いです。第17b章では、同じ規則を使って型推論と代入の短縮形を追加します。 + +## 2つの権限 + +構造体型の前の修飾子はアクセス権限を表します。`!Point` は書き込みを許し、`~Point` は深い読み取り専用ビューを与えます。変数名の直前の修飾子は、その束縛を制御します。`!p` は再代入を許し、`~p` は固定します。空白によって意味は変わりません。 + +| 宣言 | `p` の再代入 | `p` 経由の書き込み | +|---|---|---| +| `!Point !p` | 可 | 可 | +| `!Point ~p` | 不可 | 可 | +| `~Point !p` | 可 | 不可 | +| `~Point ~p` | 不可 | 不可 | + +プリミティブ型の束縛は、デフォルトで再代入可能です。構造体参照は、デフォルトで固定の束縛と読み取り専用アクセスになります。`Point p` は `~Point ~p` を意味します。初期化子はこれらのデフォルトを変えません。局所変数とフィールドで同じ構文が使えます。 + +```cpp +struct Point { int x; int y; }; +!Point p = new Point; // Fixed binding, writable object +p.x = 3; // Allowed +Point !view = p; // Reassignable binding, read-only object view +view = new Point; // Allowed, view is re-assignable +// view.x = 4; // Error: view is read-only access +// p = new Point; // Error: p is fixed binding +int ~limit = 10; // Explicitly fixed primitive +int count = 0; // Mutable primitive by default +count = count + 1; +return p.x; +``` + +## 構築と固定フィールド + +固定フィールドは、構造体の宣言または割り当てのコンストラクタブロックで初期化できます。デフォルト値のない固定プリミティブには初期化が必要で、null を許容しない参照にも(null でない)値が必要です。null 許容の参照フィールドは null から始められます。コンストラクタは特別で、普通に見えるコードの中で final フィールドを設定しますが、ほかのコードでは final フィールドを設定できません。 + +```cpp +struct Point { int ~x; int ~y; }; // x,y are final +Point p = new Point { x=3; y=4; }; // x,y set in constructor +// p.x = 99; // Error; p is final +return p.x + p.y; +``` + +構造体内でも束縛とアクセスの修飾子は独立しています。`!Point ~origin` は、書き込み可能なポインタを含む固定フィールドです。`origin` を置き換えることと `origin.x` を変更することは別の操作です。 + +## 深い読み取り専用アクセスとほかのエイリアス + +読み取り専用ビューは、それを通じて到達するすべての参照から書き込み権限を取り除きます。最適化やキャストによって書き込みアクセスを復活させることはできません。オブジェクト自体は凍結されて**いません**。別の書き込み可能なエイリアスは依然として変更でき、読み取り専用ビューを通じた読み出しもその変更を観測します。 + +```cpp +struct Point { int x; }; +!Point p = new Point; +Point view = p; +p.x = 3; +int before = view.x; +p.x = 5; // Write to the shared object, via a mutable reference +return before * 10 + view.x; // 35 +``` + +したがって読み取り専用アクセスは、Load を定数にしたり、そのメモリ依存を除去したりしません。背後のオブジェクトは、別の参照から依然として変更できる可能性があります。 + +## 配列: 各層の修飾子 + +新しい配列は、要素を埋められるように書き込み可能な内容から始まります。`[]` はそのアクセスを保ち、`[~]` はその配列の層で深い読み取り専用ビューを要求します。変数自体は、ほかの参照と同じくデフォルトで固定の束縛です。 + +| 宣言 | 意味 | +|---|---| +| `u8[] a` | 書き込み可能なバイト列への固定の束縛 | +| `u8[] !a` | 書き込み可能なバイト列への再代入可能な束縛 | +| `u8[~] a` | 読み取り専用配列への固定の束縛 | +| `Point?[] a` | 読み取り専用 Point 参照を含む書き込み可能な要素 | +| `!Point?[] a` | 書き込み可能な Point 参照を含む書き込み可能な要素 | +| `u8[~]?[] rows` | null または読み取り専用バイト配列を含む、書き込み可能な外側の要素 | +| `u8[]?[~] rows` | null 許容の内側の配列も含む、深い読み取り専用の外側のビュー | + +参照配列を宣言するときは `?` が必要です。初期状態では各要素が null だからです。初期化後には、このような配列を読み取り専用にキャストできます。外側の読み取り専用ビューは、内側の配列にも書き込みを禁止します。それらの配列への書き込み可能なエイリアスが別の場所にあっても同じです。 + +```cpp +u8[] bytes = new u8[2]; +bytes[0] = 7; +u8[~]?[] rows = new u8[~]?[1]; +rows[0] = bytes; // Outer array remains writable +// rows[0][0] = 9; // Error: the inner view is read-only +bytes[0] = 9; // Another alias can still write +return rows[0][0]; // 9 +``` + +書き込み可能な配列スロットには、一致する要素の権限が必要です。たとえば `!Point?[]` を `Point?[]` に代入できません。後者が読み取り専用 Point を挿入すると、前者がそれを書き込み可能として公開してしまうためです。読み取り専用の外側ビューは、この挿入を許しません。要素に書き込み可能な Point 参照を保持させる必要があれば、`new !Point?[n]` を使います。`new Point?[n]` はデフォルトの読み取り専用参照を維持します。 + +## 表現 + +[ScopeNode](src/main/java/com/seaofnodes/simple/node/ScopeNode.java) と [Field](src/main/java/com/seaofnodes/simple/type/Field.java) は、スロットが固定かどうかを記録します。それとは独立して、[TypeMemPtr](src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java) は、ポインタ経由の読み取り専用アクセスを記録します。この権限は、不完全な構造体が名前で解決されたときも付いたままで、現在わかっているフィールド数には依存しません。構造体の定義は、第12章からの不変で浅い参照を維持します。 + +[ReadOnlyNode](src/main/java/com/seaofnodes/simple/node/ReadOnlyNode.java) はポインタ値を変えずに書き込み権限を取り除きます。Load は、最適化が格納済みの値を転送する場合も含め、参照の結果にその権限を伝播します。Store はフィールドの固定束縛フラグと入力ポインタのアクセス権限の両方をチェックします。既存のメモリ SSA 表現は変わりません。 diff --git a/chapter17a/README.md b/chapter17a/README.md index 9c9f9130..97904def 100644 --- a/chapter17a/README.md +++ b/chapter17a/README.md @@ -1,5 +1,7 @@ # Chapter 17a: Mutability and Read-Only Views +English | [日本語](README.ja.md) + [Previous: Chapter 16](../chapter16/README.md) | [Next: Chapter 17b](../chapter17b/README.md) diff --git a/chapter17a/docs/17-grammar.ja.md b/chapter17a/docs/17-grammar.ja.md new file mode 100644 index 00000000..9a71f6d6 --- /dev/null +++ b/chapter17a/docs/17-grammar.ja.md @@ -0,0 +1,16 @@ +# 第17a章の文法の追加 + +[English](17-grammar.md) | 日本語 + +この文書は [原文](17-grammar.md) の日本語訳です。 + +式と文の形式は第16章と同じです。宣言に、独立したアクセスと束縛の修飾子を追加します。 + +```text +declaration = type binding ("," binding)* ";" +binding = ("!" | "~")? identifier ("=" expression)? +type = ("!" | "~")? name suffix* +suffix = "?" | "[]" | "[~]" +``` + +前置の修飾子には構造体参照が必要です。それは名前付きの基本型を修飾し、囲む配列は修飾しません。各角括弧の組は、それぞれ自身の配列の層を修飾します。束縛の修飾子は、その直後の名前だけに適用します。デフォルトとコンストラクタの初期化規則については [README](../README.ja.md) を参照してください。`var`、`val`、インクリメント、複合代入、条件式、`for` ループは第17b章で導入します。 diff --git a/chapter17a/docs/17-grammar.md b/chapter17a/docs/17-grammar.md index 98aef06c..a8cd8818 100644 --- a/chapter17a/docs/17-grammar.md +++ b/chapter17a/docs/17-grammar.md @@ -1,5 +1,7 @@ # Chapter 17a grammar additions +English | [日本語](17-grammar.ja.md) + The expression and statement forms are those of Chapter 16. Declarations add independent access and binding modifiers: diff --git a/chapter17b/README.ja.md b/chapter17b/README.ja.md new file mode 100644 index 00000000..68f5c389 --- /dev/null +++ b/chapter17b/README.ja.md @@ -0,0 +1,94 @@ +# 第17b章: 糖衣構文 + +[English](README.md) | 日本語 + +[前: 第17a章](../chapter17a/README.ja.md) | +[次: 第18章](../chapter18/README.ja.md) + +この文書は [原文](README.md) の日本語訳です。 + +## 目次 + +1. [前置・後置インクリメント](#前置後置インクリメント) +2. [複合代入](#複合代入) +3. [var/val](#var-と-val) +4. [三項演算](#三項演算) +5. [for ループ](#for-ループ) + +> 訳注: 原文の目次にある「Memory effects」に対応する節は、原文本文にはありません。 + +第17a章では、代入で確認する権限を確立しました。この章ではその権限を変えずに、代入と制御フローを短く書く方法を追加します。その後、第18章で関数と呼び出しを導入します。 + +## 前置・後置インクリメント + +`arg++` と `arg--` を、通常の意味で許します。値を更新し、式の値は更新前の値になります。最長一致を使うので、`arg---arg` は `(arg--)-arg` としてパースします。 + +`s.fld++` と `ary[idx]++`、それらの `--` 版も許します。 + +前置インクリメントは、今のところ識別子についてだけ許します。例は `--pc` です。 + +## 複合代入 + +後置インクリメントと同じ方向で、局所変数への `op=` 代入も、ほかの言語に似た意味論で許すようになります。サポートする演算子は `+=`、`-=`、`*=`、`/=`、`&=`、`|=`、`^=`、`<<=`、`>>=`、`>>>=` です。ビット演算とシフトの代入には整数オペランドが必要です。`>>=` は符号を保持し、`>>>=` はゼロを入れます。代入は右辺を1回評価し、結果を変数の型に狭めて格納し、その格納値を式の結果とします。代入は、`x |= y <<= 2` のように右から左に結合します。 + +## var と val + +`var` と `val` は変数を宣言し、その型を推論します。`var` は再代入可能な束縛、`val` は固定の束縛を推論します。両方とも初期化式のアクセス権限を保ちます。どちらも、書き込み可能なオブジェクトを読み取り専用にはしません。この2つの独立した権限は[第17a章](../chapter17a/README.ja.md)から来ています。 + +```cpp +struct Point { int x; }; +val p = new Point; // Fixed binding to a writable new object +p.x = 3; // Allowed +// p = new Point; // Error: fixed binding +Point view = p; // Explicit read-only view +var q = view; // Reassignable binding, still read-only access +// q.x = 4; // Error: cannot restore write permission by inference +``` + +型推論には初期化子が必須です。プリミティブでは広い数値型(`int` または `flt`)を推論し、参照ではアクセス権限を保持します。再代入可能な推論された参照は、後で空の値を保持できるように null を許容します。明示的な型は、宣言された幅と null 許容性を維持します。関数パラメータには依然として明示的な型が必要です。 + +## 三項演算 + +`pred ? e_true : e_false` を許します。`pred ? e_true` も許し、この場合の false の結果は true の結果のゼロ型版です。 + +## for ループ + +C/C++ 形式の `for` ループを許します。 + +`for( init; test; next ) body` + +例: + +```cpp +int sum=0; +for( int i=0; i<arg; i++ ) + sum += i; +return sum; +``` + +`init`、`test`、`next` のどれも空にできます。`init` には新しい変数の宣言を許し、そのスコープは `for` 式に限定されます。 + +```cpp +int sum=0; +for( int i=0; i<arg; i++ ) + sum += i; +return i; // ERROR: Undefined name 'i' +``` + +誤った `find` 呼び出し: + +```cpp +for( int i=0; i<ary#; i++ ) + if( ary[i]==e ) + break; +do_stuff(i); // ERROR: undefined name 'i' +``` + +`find` 呼び出しの例: + +```cpp +for( int i=0; i<ary#; i++ ) + if( ary[i]==e ) + return i; +return -1; +``` diff --git a/chapter17b/README.md b/chapter17b/README.md index 5bdfb9e4..1b3a41af 100644 --- a/chapter17b/README.md +++ b/chapter17b/README.md @@ -1,5 +1,7 @@ # Chapter 17b: Syntax Sugar +English | [日本語](README.ja.md) + [Previous: Chapter 17a](../chapter17a/README.md) | [Next: Chapter 18](../chapter18/README.md) diff --git a/chapter17b/docs/17-grammar.ja.md b/chapter17b/docs/17-grammar.ja.md new file mode 100644 index 00000000..c90a9d5b --- /dev/null +++ b/chapter17b/docs/17-grammar.ja.md @@ -0,0 +1,21 @@ +# 第17b章の文法の追加 + +[English](17-grammar.md) | 日本語 + +この文書は [原文](17-grammar.md) の日本語訳です。 + +[第17a章の宣言構文](../../chapter17a/docs/17-grammar.ja.md)から始めます。この章は、型推論、代入の短縮形、条件式、`for` ループを追加します。アクセスと束縛の権限の意味は変わりません。 + +```text +inferredDeclaration = ("var" | "val") ("!" | "~")? identifier "=" assignment +assignment = expression (assignmentOperator assignment)? +assignmentOperator = "=" | "+=" | "-=" | "*=" | "/=" | "&=" | "|=" | "^=" + | "<<=" | ">>=" | ">>>=" +expression = bitwise ("?" assignment (":" assignment)?)? +prefixUpdate = ("++" | "--") identifier +postfixUpdate = lvalue ("++" | "--") +forStatement = "for" "(" declarationOrExpression? ";" assignment? ";" + assignment? ")" statement +``` + +宣言には、コンマで区切った複数の名前を含められます。`var` は再代入を許し、`val` は束縛を固定します。両方とも初期化子の参照の権限を維持します。後置の更新はフィールドと配列要素もサポートします。前置の更新は、今のところ局所名をサポートします。評価順序については [README](../README.ja.md) を参照してください。 diff --git a/chapter17b/docs/17-grammar.md b/chapter17b/docs/17-grammar.md index 9a70f323..d42fcba1 100644 --- a/chapter17b/docs/17-grammar.md +++ b/chapter17b/docs/17-grammar.md @@ -1,5 +1,7 @@ # Chapter 17b grammar additions +English | [日本語](17-grammar.ja.md) + Start with [Chapter 17a's declaration syntax](../../chapter17a/docs/17-grammar.md). This chapter adds inference, assignment shortcuts, conditional expressions, and `for` loops. Access and binding permissions keep the same meaning. diff --git a/chapter18/README.ja.md b/chapter18/README.ja.md new file mode 100644 index 00000000..2a586c1d --- /dev/null +++ b/chapter18/README.ja.md @@ -0,0 +1,224 @@ +# 第18章: 関数と呼び出し + +[English](README.md) | 日本語 + +[前: 第17b章](../chapter17b/README.ja.md) | +[次: 第19章](../chapter19/README.ja.md) + +この文書は [原文](README.md) の日本語訳です。 + +第17b章までは、プログラムは入力 `arg` と結果を持つ1つの本体からなっていました。分岐、ループ、オブジェクトの割り当て、メモリの更新はできますが、まだ関数に分割できません。この章では、再帰呼び出しも含めた関数値と呼び出しを追加します。 + +グラフ表現には、なじみのある部品を使います。関数の入口は Region、パラメータは Phi で、出口は1つの Return に合流します。呼び出しは関数境界を越えてこれらを結びます。関数の呼び出し元が1つだけなら、境界を取り除くことで通常のピープホール最適化が結合されたグラフを簡略化できます。これが最初の形のインライン化です。 + +## 目次 + +1. [関数を書く](#関数を書く) +2. [グラフ内の関数](#グラフ内の関数) +3. [呼び出しをまたぐメモリ](#呼び出しをまたぐメモリ) +4. [関数型と呼び出し先](#関数型と呼び出し先) +5. [インライン化](#インライン化) +6. [スケジューリングと実行](#スケジューリングと実行) + +[linear](https://github.com/SeaOfNodes/Simple/tree/linear) ブランチの線形の Git リビジョン履歴で[この章](https://github.com/SeaOfNodes/Simple/tree/linear-chapter18)を読むことや、前章との[差分](https://github.com/SeaOfNodes/Simple/compare/linear-chapter17b...linear-chapter18)を見ることもできます。 + +## 関数を書く + +関数式は型付きのパラメータを列挙し、`->` と本体を続けます。 + +```simple +val sq = { int x -> x*x; }; +return sq(arg); +``` + +初期化子は関数値を作り、宣言がそれを `sq` に束縛します。呼び出し `sq(arg)` は引数を評価し、その値を `x` に束縛して本体を実行し、結果を返します。実行が本体の末尾に到達すると、最後の式が結果を供給します。明示的な `return` なら途中で終了できます。 + +関数型は、パラメータ名を含めず、パラメータ型と結果型を列挙します。同じ平方関数を明示的な型で宣言できます。 + +```simple +{int -> int} sq = { int x -> x*x; }; +return sq(4); // 16 +``` + +関数は匿名の値です。変数に代入すれば診断用の名前を付けられますが、束縛はほかの変数と同じ規則に従います。`val` は固定の束縛、`var` は再代入可能な束縛を推論します。関数を引数として渡したり、結果として返したり、式で選んだりできます。 + +パラメータは[第17a章](../chapter17a/README.ja.md)の明示的な宣言構文を使います。`!Point p` は固定されたパラメータ束縛を通じた書き込みを許し、`Point !p` は読み取り専用参照の再代入を許し、`int ~limit` はプリミティブのパラメータを固定します。パラメータ型は必須です。`var` と `val` は、関数を保持する変数に適用するもので、パラメータには適用しません。 + +### 戻り値とスコープ + +本体には周囲の言語と同じ文が使えます。ここでは早期 return で、最初の一致する配列要素を探します。 + +```simple +val find = { int[] es, int e -> + for( int i=0; i<es#; i++ ) + if( es[i] == e ) + return i; + return -1; +}; +val es = new int[3]; +es[0] = 7; +es[1] = 9; +return find(es,9); // 1 +``` + +1つの関数の、到達可能なすべての結果には共通の型が必要です。これは最適化後にチェックするので、到達不能な return は型エラーを起こしません。`void` 型はありませんが、関数は構文を何も書かずに `null` を返せ、呼び出し元は呼び出しを文として使って結果を無視できます。 + +関数は、囲んでいる関数の局所変数を捕捉しません。外側の束縛にアクセスできるのは、それが固定され、値がコンパイル時定数の場合だけです。たとえば次のようになります。 + +```simple +val offset = 2; +val addOffset = { int x -> x+offset; }; +return addOffset(arg); +``` + +`val offset` を `int offset` に変えると束縛が可変になり、関数定義は拒否されます。同様に `val offset = arg` は固定ですが定数ではないので、入れ子の関数から使えません。固定された関数定数はこの規則を満たすため、フレームを捕捉せずに、ある関数から別の関数を参照できます。 + +### 再帰 + +関数は自身の束縛を参照できます。 + +```simple +val fact = { int n -> + n <= 1 ? 1 : n*fact(n-1); +}; +return fact(arg); +``` + +パーサーは、本体をパースする前に関数の入口を登録します。`fact` への前方参照は、宣言がその関数値を受け取ると解決されます。それによって再帰呼び出しも、外からの呼び出しと同じ入口を見つけられます。この章では戻り値の型推論はまだ悲観的です。相互再帰する定義には、[第24章](../chapter24/README.ja.md)の SCCP で導入する、より強力な解析が必要なことがあります。 + +## グラフ内の関数 + +これまでは `Start` が1つのプログラム本体への入力を供給していました。今度はその本体を、整数パラメータ `arg` を持つ暗黙の `main` としてパースします。評価器は `Start` から `main` に入り、結果を外の世界に渡します。そのため既存のプログラムは、ソース構文を維持したまま、明示的に宣言された関数と同じ関数表現を得ます。 + +### 入口、パラメータ、出口 + +[FunNode](src/main/java/com/seaofnodes/simple/node/FunNode.java) は `RegionNode` を継承します。Region が異なる分岐からの制御を合流するのに対し、Fun は異なる呼び出し元からの制御を合流します。リンクされた呼び出しが、1本の流入制御エッジを供給します。 + +[ParmNode](src/main/java/com/seaofnodes/simple/node/ParmNode.java) は `PhiNode` を継承します。Phi が流入分岐に属する値を選ぶのと同じように、各パラメータは流入呼び出しに属する引数を選びます。パラメータの添字が、呼び出し元の供給するものを識別します。 + +| パラメータの添字 | 値 | +|---|---| +| 0 | 復帰地点: 呼び出し元の実行再開位置 | +| 1 | メモリ全体 | +| 2 以降 | ソース言語の引数 | + +各関数は、`Start` で表される暗黙の未知の呼び出し元も持ちます。これは宣言されたパラメータ型と未知のメモリを供給します。呼び出しをまだ発見している間、これによって入口を保守的に保ちます。 + +ソース上のすべての `return` は、1つの出口 Region に寄与します。値の Phi が結果を合流し、BulkMemPhi がそのメモリ状態を合流します。結果の [ReturnNode](src/main/java/com/seaofnodes/simple/node/ReturnNode.java) の入力は `{control, memory, value, return point}` です。出口が1つなら、Region と Phi はしばしば畳み込まれて消えます。`Stop` は、`main` のものも含め、関数の Return を保持します。 + +これにより、本体に分岐、ループ、複数の return 文があっても、各関数は1つの入口と1つの合流した出口を持ちます。 + +### 呼び出しと復帰地点 + +[CallNode](src/main/java/com/seaofnodes/simple/node/CallNode.java) の入力は `{control, memory, arguments..., function pointer}` です。最後の入力が呼び出し先を決めます。定数の関数でも、関数値を持つ式の結果でもかまいません。引数式は、Call が制御とメモリの入力を取る前に評価します。 + +各 Call には [CallEndNode](src/main/java/com/seaofnodes/simple/node/CallEndNode.java) があり、呼び出し元が継続する地点となります。CallEnd は Call と、リンクされた呼び出し先の Return を入力に取ります。その射影は、それぞれスロット 0、1、2 に制御、メモリ、結果を供給します。 + +呼び出される側は、どの CallEnd に戻るかを知る必要があります。隠れたパラメータ 0 が、本体を通じて Return までその復帰地点を運びます。*return program counter*(復帰プログラムカウンタ)の略である `TypeRPC` は、関数ポインタ型が取り得る入口の集合を記述するのと同じように、取り得る復帰地点の集合を記述します。これらは内部値であり、ソースプログラムは RPC 変数を宣言しません。 + +呼び出しグラフのエッジは、呼び出しがどの関数に到達し得るかを記述します。これは1つの関数内の制御フローとは別です。通常グラフをたどるときには、呼び出しと関数の付近で注意が必要です。多くの探索は、単一の関数内でだけ有効だからです。 + +## 呼び出しをまたぐメモリ + +前の章では、フィールドのエイリアスごとにメモリを遅延的に分割しました。関数内では今もそうしますが、関数入口のメモリは未知です。引数が、呼び出し元で割り当てて変更したオブジェクトを参照するかもしれません。流入するヒープが空だとは、もう仮定できません。 + +Call は、MemMerge に包まれたスライスも含め、完全なメモリ状態を受け取ります。その CallEnd は新しいメモリ全体の値を生成します。この章では、すべての呼び出しがすべてのエイリアスを変更し得ると扱います。呼び出される関数がわかっていても、どのフィールドに書くかという要約はまだ得られません。 + +```simple +struct Counter { int n; }; +val bump = { !Counter p -> p.n++; return null; }; +val p = new Counter; +int before = p.n; +bump(p); +bump(p); +return before*10+p.n; // 2 +``` + +最初の `p.n` の読み出しは、初期化された値を観測します。最後の読み出しは両方の呼び出し後のメモリを使う必要があり、最初の読み出しを再利用できません。2つの呼び出しも、メモリの入出力によって順序が維持されます。書き込み可能なパラメータ権限が更新を許し、メモリエッジがその時点を記録します。 + +関数内では、MemMerge、MemPhi、BulkMemPhi が引き続き精密なスライスを露出し、分岐やループの状態を合流します。境界では、メモリ Parm 1 と CallEnd のメモリ結果は不透明なままです。大域的コード移動は、Load を配置するときに呼び出しを潜在的な書き込みとして扱います。呼び出しはブロックを終了するため、Call より前である必要がある Load は、その終端命令より前にスケジュールします。 + +インライン化は呼び出しの境界を取り除き、実際の Load と Store を既存のエイリアス最適化に露出します。グラフに残る呼び出しの作用をより精密に扱うには、後のエスケープ解析が必要です。 + +## 関数型と呼び出し先 + +[TypeFunPtr](src/main/java/com/seaofnodes/simple/type/TypeFunPtr.java) は、引数タプル、結果型、null 許容性、関数添字の集合(`fidxs`)を記録します。ある引数シグネチャ内で、各関数は小さな整数の添字を得ます。ビット集合は、そのシグネチャを持つ1つの既知の関数や、複数の可能な関数を記述できます。`CodeGen` は具体的な関数の識別情報を FunNode に対応付けます。この章の IR の実行に、機械語コードのアドレスは不要です。 + +たとえば、以下の両分岐は型 `{int -> int}` を持ちますが、関数添字は異なります。 + +```simple +val f = arg ? { int x -> x+x; } : { int x -> x*x; }; +return f(3); // 6 when arg is nonzero, otherwise 9 +``` + +`f` の Phi は、2つの関数ポインタ型を組み合わせます。呼び出し先の集合は両方の関数を含みます。関数を選ぶことは、その本体の実行ではありません。 + +関数ポインタも、なじみのある接尾辞 `?` で null を許容できます。 + +```simple +{int -> int}? f = arg ? { int x -> x*x; } : null; +if( f ) return f(3); +return 0; +``` + +ガードは、呼び出し時にポインタが null でないことを証明します。呼び出しは、引数の個数、型、アクセス権限もチェックします。構造体ポインタと関数ポインタは、`Type.NIL` で表される特別な `null` 値を共有します。`TypeNil` が、共通の null 許容性の仕組みを供給します。 + +### 既知の呼び出し先のリンク + +ここで*リンク*とは、呼び出しと関数の間に IR エッジを追加することです。オブジェクトファイルのリンクではありません。既知の各呼び出し先について、コンパイラは Call を Fun の入力として追加し、引数をその Parm に追加し、その Return を CallEnd の入力として追加します。対応する入力位置が、呼び出し元と引数の関係を保ちます。 + +この章ではすでに、上の条件式の例にある両方の関数も含め、既知の呼び出し先の有限集合をリンクします。未知の関数をまだ含む無限集合は列挙できません。この不確実性を、すべての呼び出しからすべての関数へのエッジで表すのは避けます。二次的に増大するためです。 + +代わりに Fun は*未知の呼び出し元*の入力を保持します。これまでに見つかった呼び出しが定数を渡していても、パラメータは宣言型を受け入れる必要があります。呼び出し元が1つ見つかっても、ほかにないという証明にはなりません。同様に、CallEnd は関数ポインタ型から結果型を得て、メモリ結果を未知のままにします。リンクされた Return エッジだけでは、これらの保守的な仮定を取り除きません。 + +インライン化は、関数の呼び出し元が1つだけであるという局所的な証明に依存します。後の SCCP の章では、どの関数や呼び出しに到達可能かについて、より強力な解析を構築し、残る境界を越えてより多くの情報を流せるようにします。 + +## インライン化 + +関数値をちょうど1つの呼び出しが使う関数を考えます。 + +```simple +val inc = { int x -> x+1; }; +return inc(arg); +``` + +この呼び出しが `inc` にリンクすると、CallEnd は、呼び出し先が1つで、その関数ポインタにほかの使用がないことを証明できます。引数が有効で自己再帰呼び出しがなければ、関数本体を呼び出し元の一部にできます。 + +| インライン化前 | インライン化後 | +|---|---| +|  |  | + +これらはグラフの模式的な断片で、書き換え前後でノード ID は変わりません。引数、結果、リンクのエッジを示し、周囲の制御フロー、メモリ、RPC の配線は省略しています。`Parm_x` への `int` 入力は、インライン化前の未知の呼び出し元の引数を表します。 + +書き換えは未知の呼び出し元の経路を取り除き、関数入口を呼び出しの流入制御につなぎます。その後、通常の Region と Phi の簡略化がパラメータを実引数に置き換えます。CallEnd の射影は、呼び出された関数の返す制御、メモリ、値になります。Call、関数境界、復帰地点の仕組みは、使用が消えるにつれて消えます。 + +Add は残りますが、今度は `arg` を直接消費します。引数が `3` なら、第2章から使っている同じ定数畳み込み規則によって `4` を生成します。インライン化で露出した Load と Store も、既存のメモリ規則で同じように簡略化できます。 + +この形のインライン化は、呼び出し元が1つの本体をその呼び出し元へ移動します。複数の呼び出し箇所のために本体を複製するわけではありません。`fact` のような再帰関数は呼び出しを保持し、評価器が処理します。 + +## スケジューリングと実行 + +この章で新しく導入する [CodeGen](src/main/java/com/seaofnodes/simple/CodeGen.java) は、コンパイル処理と関数の検索テーブルを所有します。次のフェーズ順序を強制します。 + +| フェーズ | 目的 | +|---|---| +| Parse | 関数と呼び出しを直接 IR に構築する。 | +| Opto | 呼び出しのリンクと自明なインライン化を含め、ピープホール最適化を反復する。 | +| TypeCheck | 型と死んだ制御が落ち着いた後で、生き残ったグラフをチェックする。 | +| Schedule | 大域的コード移動で演算を基本ブロックに配置する。 | +| LocalSched | 実行のために各ブロック内の演算を順序付ける。 | + +例: + +```java +CodeGen code = new CodeGen("val sq = { int x -> x*x; }; return sq(arg);"); +code.parse().opto().typeCheck().GCM().localSched(); +``` + +大域的コード移動は、[第13章](../chapter13/README.ja.md)の処理を複数の関数に拡張します。関数本体を別々に保ち、共有される定数式には、それを使う各関数内に局所的なコピーを与え、呼び出しをまたぐメモリへの作用を尊重します。 + +その後 [ListScheduler](src/main/java/com/seaofnodes/simple/ListScheduler.java) が各ブロックを順序付けます。未スケジュールの局所依存を数え、準備のできた演算を選び、依存が満たされるにつれてそのユーザーを準備済みにします。Phi は入口に、制御移動は出口に属します。ここには機械のタイミングモデルがなく、さまざまな改善の余地がありますが、この章の範囲外です。目標は、引き続き合法な実行順序です。 + +[Eval2](src/test/java/com/seaofnodes/simple/Eval2.java) はそのスケジュールを実行し、IR の演算を、無制限の値スロットを持つ命令として扱います。各呼び出しには独自のフレームがあるため、同じノードの再帰的な呼び出しも異なる値を保持します。呼び出し時には、呼び出し元のフレームと復帰地点を保存し、呼び出される側のフレームを作り、選ばれた関数に移ります。return 時には呼び出し元を復元し、その CallEnd に値を渡します。Region では、結果を代入する前に、選ばれたすべての Phi 入力を読みます。そのため並列代入は正しさを保ちます。 + +これでスケジュールされた関数からなる実行可能なプログラムができましたが、演算はまだ機械に依存しません。[第19章](../chapter19/README.ja.md)では、スケジューリング前に命令選択を挿入します。これらの理想的な演算は、選んだ CPU のレジスタ制約と呼び出し規約を持つ命令になります。 diff --git a/chapter18/README.md b/chapter18/README.md index 2daff473..8039b4cb 100644 --- a/chapter18/README.md +++ b/chapter18/README.md @@ -1,5 +1,7 @@ # Chapter 18: Functions and Calls +English | [日本語](README.ja.md) + [Previous: Chapter 17b](../chapter17b/README.md) | [Next: Chapter 19](../chapter19/README.md) diff --git a/chapter18/docs/18-grammar.ja.md b/chapter18/docs/18-grammar.ja.md new file mode 100644 index 00000000..dc9bf8c7 --- /dev/null +++ b/chapter18/docs/18-grammar.ja.md @@ -0,0 +1,138 @@ +# 第18章の文法 + +[English](18-grammar.md) | 日本語 + +この文書は [原文](18-grammar.md) の日本語訳です。 + +> 訳注: 原文の見出しは「Chapter 16」となっています。本ファイルは第18章の文法文書です。 + +[第18章に戻る](../README.ja.md) + +```antlrv4 +grammar SimpleLanguage; + +program : block EOF ; + +block : statement+ ; + + +statement + : '{' block '}' + | returnStatement + | ifStatement + | whileStatement + | breakStatement + | continueStatement + | structDeclaration + | expressionStatement + ; + + +returnStatement : 'return' expression ';' ; + +ifStatement : 'if' '(' expression ')' statement ('else' statement)? ; + +whileStatement : 'while' '(' expression ')' statement ; + +breakStatement : 'break' ';' ; + +continueStatement : 'continue' ';' ; + + +structDeclaration : 'struct' IDENTIFIER '{' block '}' ; + +expressionStatement + : type IDENTIFIER ';' + | type IDENTIFIER '=' expression ';' + | IDENTIFIER '=' expression ';' + | expression + ; + +PRIMTYPE + : 'int' + | 'i8' + | 'i16' + | 'i32' + | 'i64' + | 'u8' + | 'u16' + | 'u32' + | 'flt' + | 'f32' + | 'f64' + | 'bool' + ; + +type + : PRIMTYPE + | typeName '?'? + | typeName nestedArray+ + ; + +nestedArray : '?'? '[]'* ; + +typeName : IDENTIFIER ; + + +expression : bitWiseExpression ; + +bitWiseExpression + : comparisonExpression ( '&' | '|' | '^' ) comparisonExpression)* + ; + +comparisonExpression + : shiftExpression (('==' | '!='| '>'| '<'| '>='| '<=') shiftExpression)* + ; + +shiftExpression + : additiveExpression ( '<<' | '>>>' | '>>' ) additiveExpression)* + ; + +additiveExpression + : multiplicativeExpression (('+' | '-') multiplicativeExpression)* + ; + +multiplicativeExpression + : unaryExpression (('*' | '/') unaryExpression)* + ; + +unaryExpression + : ('-') unaryExpression + | '!' unaryExpression + | primaryExpression postAssign + ; + +primaryExpression + : INTEGER_LITERAL + | FLOAT_LITERAL + | '(' expression ')' + | 'true' + | 'false' + | 'null' + | newExpression + | IDENTIFIER + ; + +newExpression + : 'new' IDENTIFIER [ '{' block '}' ] + | 'new' IDENTIFIER '[' expression ']' + ; + +postAssign : postFix [ '=' expression ] | [ '#' ]; + +postFix + : '.' IDENTIFIER postFix + | '[' expression ']' postFix + ; + + +IDENTIFIER : NON_DIGIT (NON_DIGIT | DIGIT)* ; + +INTEGER_LITERAL + : [1-9]DIGIT* + | [0] + ; + +NON_DIGIT: [a-zA-Z_]; +DIGIT: [0-9]; +``` diff --git a/chapter18/docs/18-grammar.md b/chapter18/docs/18-grammar.md index 7da725e9..51539d0b 100644 --- a/chapter18/docs/18-grammar.md +++ b/chapter18/docs/18-grammar.md @@ -1,5 +1,7 @@ # Grammar for Chapter 16 +English | [日本語](18-grammar.ja.md) + ```antlrv4 grammar SimpleLanguage; diff --git a/chapter19/README.ja.md b/chapter19/README.ja.md new file mode 100644 index 00000000..f1de1079 --- /dev/null +++ b/chapter19/README.ja.md @@ -0,0 +1,147 @@ +# 第19章: 命令選択と移植可能なコンパイル + +[English](README.md) | 日本語 + +この文書は [原文](README.md) の日本語訳です。 + +[前の章: 第18章](../chapter18/README.ja.md) | +[次の章: 第20章](../chapter20/README.ja.md) + +[linear](https://github.com/SeaOfNodes/Simple/tree/linear) ブランチ上の直線的な Git リビジョン履歴で [この章](https://github.com/SeaOfNodes/Simple/tree/linear-chapter19) を読み、前の章と [比較](https://github.com/SeaOfNodes/Simple/compare/linear-chapter18...linear-chapter19) することもできます。 + +## 命令選択、レジスタ割り当て、命令エンコーディングにまつわる全体的な問題と開発上の問題 + +命令選択の出力、すなわち理想化された Simple ノードからマシン固有のノードへの対応付けは、実行方法がないと大量にテストするのが困難です。命令選択、レジスタ割り当て、命令エンコーディングの3つすべてが完成するまでは、実行方法が得られません。そのため、エンコーディングの完成前後には、命令選択とレジスタ割り当てで多くのバグが見つかると予想しています。もちろん出力を手作業で調べて明らかな問題は修正しますが、実際にコードを動かせるようになるまで、多くのバグが残ります。 + +つまり、この章で説明する作業に対する更新やバグ修正が、ほかの章に含まれています。 + +## 移植可能なコンパイル + +CPU の移植先ごとに1つの、新しい抽象 `Machine` クラスを追加します。`Machine` は移植先の名前、すべてのレジスタ名、そして *理想* ノードからマシンノードを生成する方法を定義します。さらに、コード生成時にマシン非依存のコンパイラ部分が使う *split* 演算と *jump* の生成も可能にします。 + +各種呼び出し規約を選択できるように、新しい CallingConv 列挙型を追加します。 + +## 命令選択フェーズ + +*大域的コード移動* の前に、新しい *InstructionSelection* コンパイルフェーズを追加します。これは任意ですが、*理想* ノードをマシン固有の派生形に置き換えます。命令選択には対象 CPU 名を渡し、その時点で移植コードを検索して使用します。これによって、少なくとも Java 実装では CPU 移植先を遅延して追加できます。 + +## マシン固有のノード + +`MachNode` インターフェースを実装する「マシン固有」のノードという概念を追加します。`MachNode` ノードは、各命令の入力・出力レジスタ、ビットエンコーディング、利用者向けの表現を定義します。後ほど、局所スケジュールを改善するための機能ユニット情報を追加します。 + +## CodeGen コンパイルドライバの整理 + +さらに多くの静的なグローバル状態を、すべての最上位ドライバ間で受け渡す CodeGen オブジェクトに移します。Node のコンストラクタには渡さないため、idealize() 呼び出しにも渡しません。静的なグローバル状態が必要なケースの大半は、依然としてこれらです。要するに、コンパイラはずっと関数型に近づき、並行コンパイルやマルチスレッドコンパイルに向けて大きく前進します。 + +## 命令選択を通じたメモリの扱い + +パーサーは1つの `$mem` 束縛を保持します。BulkMemPhi は必要に応じてエイリアスを並列の MemPhi に分割し、MemMerge は既定のメモリと明示的なスライスをまとめます。両方のマシン選択器は、これらのノードクラスとエイリアス情報を保持します。メモリの Phi を通常の値の Phi には変換しません。 + +この章では、コンストラクタの非ゼロ値を明示的な Store に変換し、割り当てそのものはゼロ初期化演算として維持します。New は `{ctrl, $mem, size}` を消費し、`{ptr, $mem}` を生成します。その入力は、割り当てる構造体のエイリアスをカバーする部分的な MemMerge です。単一のメモリ結果も、同じエイリアスをカバーします。パーサーは周囲の集合体に無関係なメモリを保持します。割り当てのサイズレジスタは入力2、ポインタ結果は出力0で、メモリにはレジスタがありません。 + +選択されたメモリ演算は、x86 の算術命令や比較命令が Load を吸収しても、フィールドのエイリアスを保持します。そのため大域的スケジューリングは、共通の MemOpNode を通じ、MemMerge 内の該当スライスをたどって反依存関係を見つけます。メモリを結果とする命令は書き手で、ほかのメモリ演算は読み手です。書き手の末尾にある順序付け辺は、レジスタを要求しません。Phi の因数分解で使用する単純な1段階の Load ガードは変更しません。 + +Call は依然としてメモリ全体を消費し、生成します。Call はブロックを終了するため、スケジューリングは引数リストに入力を追加せず、読み取りを Call の前に引き上げます。直接マシン呼び出しは定数の対象を埋め込み、制御、メモリ、引数の入力だけを保持します。 + +## レジスタとレジスタマスク + +`Machines` はレジスタを小さな密な整数として定義します。レジスタ割り当て器は割り当て時にこれらの番号を使います。小さな整数からマシンレジスタへの対応は移植者に任されますが、通常は明らかな1対1の対応があります。X86 では RAX がレジスタ0、RCX がレジスタ1となり、16個の GPR まで続きます。XMM レジスタ番号は16から31です。レジスタ番号は一意でなければなりません。割り当て器はこれによってレジスタを追跡します。 + +レジスタマスククラス `RegMask` は、許可されたレジスタのビット集合を保持します。大半のマシン演算は全レジスタの部分集合に制限され、許可集合を RegMask で記述します。大半の RegMask は、たとえば `Mul` 演算で許可されるレジスタ集合のように不変ですが、割り当て器は可変のレジスタ集合も広範囲に使います。 + +呼び出し規約は多くの固定レジスタを指定し、実際には許可レジスタ集合をかなり「縛る」傾向があります。 + +## SystemV または Win64 呼び出し規約を持つ X86_64_V2 移植 + +`node/cpus/x86_64_v2` ディレクトリには、X86 64 V2 への移植 `x86_64_v2.java` があります。16個の64ビット GPR、16個の XMM レジスタ、X86 フラグをサポートします。理想化された Simple の Sea-of-Nodes から X86 演算を対応付けるパターンマッチャがあります。アドレッシングモードを除けば、理想ノードから X86 演算へほぼ1対1で対応付けます。 + +### 通常の命令選択 + +instSelect フェーズは理想グラフを走査し、理想演算からマシン演算への対応を保持します。先着順で最初に一致したものを採用する、貪欲なパターンマッチを行います。特別な動作のない演算は局所的な検索だけを行います。たとえば `Mul` は X86 の `MulX86` に直接対応します。大半のマッチの右辺は即値定数にでき、即値版(`MulIX86`)に対応します。 + +### アドレッシングモード + +多くの X86 演算では、入力の1つが `Load` なら、メモリを入力にする派生形を使えます。X86 マッチャの専用の `address()` 呼び出しが各種アドレッシングモードを探し、メモリ専用の派生形を構築します。これらは個別に明示する必要がありますが、単純な1行のマッチと、メモリ版を割り当てる1行で済みます。 + +### Compare と Jmp + +Simple の `BoolNode` は `0/1` の値を生成します。X86 では `Cmp/SetXX` という命令列で行います。X86 の `Jxx` は `0/1` ではなく `FLAGS` を受け取るので、`SetXX` を飛ばして直接 `Cmp` を使います。理想 `Bool` を直接使う場合は `SetXX` が必要です。つまり `return a==3` は `CmpX86 RAX,#3; SetEQ RAX; ret;` に対応し、`if( a==3 )` は `CmpX86 RAX,#3; jeq;` に対応します。 + +## RISC-V 移植 + +`node/cpus/riscv` ディレクトリには RISC-V への移植 `riscv.java` があります。32個の64ビット GPR と32個の浮動小数点レジスタをサポートします。理想化された Simple の Sea-of-Nodes から riscv 演算を対応付けるパターンマッチャがあります。現在の対象は `RVA23U64` です。 + +### 通常の命令選択 + +**X86_64_V2** と同様に動作します。riscv には *複雑な* アドレッシングモードがないため、ほとんどの場合は貪欲なパターンマッチを適用しません。特別な動作のない演算は、単純に検索します。 + +### 制限された ISA + +RISC-V は単純なアドレッシングモードとオーバーロードを備える、制限された ISA です。たとえば `lea` はありません。 + +### 分岐 + +`x86_64_v2` の分岐は、フラグを設定する比較命令と、そのフラグを使うジャンプ命令を必要とします。一方 RISC-V は、必要なオペランドをすべて分岐命令そのものに含め、別の比較ステップを不要にします。 + +例: + +``` +beq x10, x11, some_label +``` + +`CBranchRISC` がこの動作を実装します。 + +### R フラグを持たない + +RISC-V には R フラグがありません。同様の機能を得るには、比較結果に応じてレジスタ値を明示的に設定できます。 + +``` +xor a0, a0, a1 +seqz a0, a0 +``` + +最初の `xor` の後、`a0` と `a1` が等しければ `a0` はゼロになります。`seqz` 命令は、前の演算で `a0` がゼロになっていれば `a0` を1に設定します。これは `nodes/cpus/riscv` 移植の `SetRISC` に対応します。 + +### 固定長命令 + +RISC-V の命令長は(ほとんどの場合)32ビット固定であり、エンコーディング側で変位を活用できます。 + +例: + +``` +flw fa5, 0(a0) +flw fa4, 12(a0) +fadd.s fa0, fa5, fa4 +``` + +(`flw` の前にロードは必要ありません。) + +### ABI 名(レジスタ) + +`RISC-V` の汎用レジスタ(`GPR`)は `x0` から `x31`、浮動小数点レジスタは `f0` から `f31` です。 + +読みやすさを高め、呼び出し規約と整合させるため、RISC-V はレジスタの ABI 名を定義し、生の番号よりも直感的な別名を提供します。 + +| レジスタ | 別名 | 用途 | 保存する側 | +|----------|-------|------------------------------|----------| +| x10 | a0 | 関数の引数/戻り値 | 呼び出し側 | +| x11 | a1 | 関数の引数/戻り値 | 呼び出し側 | +| x12 | a2 | 関数の引数 | 呼び出し側 | +| x13 | a3 | 関数の引数 | 呼び出し側 | +| x14 | a4 | 関数の引数 | 呼び出し側 | +| x15 | a5 | 関数の引数 | 呼び出し側 | +| x16 | a6 | 関数の引数 | 呼び出し側 | +| x17 | a7 | 関数の引数 | 呼び出し側 | + +関数に引数を渡すとき、呼び出し側からの最初の8個の引数はレジスタ `a0` から `a7` で渡します。 + +### 浮動小数点値のロード + +現在、浮動小数点定数は、浮動小数点レジスタに変換する前に、まず `integer GPR` にロードします。この処理は RA フェーズで次のように書き換えます。 + +```` +lui a0, 262144 +fmv.w.x fa0, a0 +```` + +非定数値には、GPR と FPR の両方をサポートする、より広いレジスタマスクを使います。この実装は `nodes/cpus/riscv/LoadRISC` にあります。 diff --git a/chapter19/README.md b/chapter19/README.md index ef3ee118..e3d13b57 100644 --- a/chapter19/README.md +++ b/chapter19/README.md @@ -1,5 +1,7 @@ # Chapter 19: Instruction Selection and Portable Compilation +English | [日本語](README.ja.md) + [Previous: Chapter 18](../chapter18/README.md) | [Next: Chapter 20](../chapter20/README.md) diff --git a/chapter19/docs/18-grammar.ja.md b/chapter19/docs/18-grammar.ja.md new file mode 100644 index 00000000..0e944f06 --- /dev/null +++ b/chapter19/docs/18-grammar.ja.md @@ -0,0 +1,138 @@ +# 第19章の文法 + +[English](18-grammar.md) | 日本語 + +この文書は [原文](18-grammar.md) の日本語訳です。 + +> 訳注: 原文の見出しは「Grammar for Chapter 16」です。この日本語版では、配置されている第19章に合わせた見出しにしています。 + +[第19章に戻る](../README.ja.md) + +```antlrv4 +grammar SimpleLanguage; + +program : block EOF ; + +block : statement+ ; + + +statement + : '{' block '}' + | returnStatement + | ifStatement + | whileStatement + | breakStatement + | continueStatement + | structDeclaration + | expressionStatement + ; + + +returnStatement : 'return' expression ';' ; + +ifStatement : 'if' '(' expression ')' statement ('else' statement)? ; + +whileStatement : 'while' '(' expression ')' statement ; + +breakStatement : 'break' ';' ; + +continueStatement : 'continue' ';' ; + + +structDeclaration : 'struct' IDENTIFIER '{' block '}' ; + +expressionStatement + : type IDENTIFIER ';' + | type IDENTIFIER '=' expression ';' + | IDENTIFIER '=' expression ';' + | expression + ; + +PRIMTYPE + : 'int' + | 'i8' + | 'i16' + | 'i32' + | 'i64' + | 'u8' + | 'u16' + | 'u32' + | 'flt' + | 'f32' + | 'f64' + | 'bool' + ; + +type + : PRIMTYPE + | typeName '?'? + | typeName nestedArray+ + ; + +nestedArray : '?'? '[]'* ; + +typeName : IDENTIFIER ; + + +expression : bitWiseExpression ; + +bitWiseExpression + : comparisonExpression ( '&' | '|' | '^' ) comparisonExpression)* + ; + +comparisonExpression + : shiftExpression (('==' | '!='| '>'| '<'| '>='| '<=') shiftExpression)* + ; + +shiftExpression + : additiveExpression ( '<<' | '>>>' | '>>' ) additiveExpression)* + ; + +additiveExpression + : multiplicativeExpression (('+' | '-') multiplicativeExpression)* + ; + +multiplicativeExpression + : unaryExpression (('*' | '/') unaryExpression)* + ; + +unaryExpression + : ('-') unaryExpression + | '!' unaryExpression + | primaryExpression postAssign + ; + +primaryExpression + : INTEGER_LITERAL + | FLOAT_LITERAL + | '(' expression ')' + | 'true' + | 'false' + | 'null' + | newExpression + | IDENTIFIER + ; + +newExpression + : 'new' IDENTIFIER [ '{' block '}' ] + | 'new' IDENTIFIER '[' expression ']' + ; + +postAssign : postFix [ '=' expression ] | [ '#' ]; + +postFix + : '.' IDENTIFIER postFix + | '[' expression ']' postFix + ; + + +IDENTIFIER : NON_DIGIT (NON_DIGIT | DIGIT)* ; + +INTEGER_LITERAL + : [1-9]DIGIT* + | [0] + ; + +NON_DIGIT: [a-zA-Z_]; +DIGIT: [0-9]; +``` diff --git a/chapter19/docs/18-grammar.md b/chapter19/docs/18-grammar.md index 7da725e9..51539d0b 100644 --- a/chapter19/docs/18-grammar.md +++ b/chapter19/docs/18-grammar.md @@ -1,5 +1,7 @@ # Grammar for Chapter 16 +English | [日本語](18-grammar.ja.md) + ```antlrv4 grammar SimpleLanguage; diff --git a/chapter20/README.ja.md b/chapter20/README.ja.md new file mode 100644 index 00000000..c33630e2 --- /dev/null +++ b/chapter20/README.ja.md @@ -0,0 +1,262 @@ +# 第20章: グラフ彩色によるレジスタ割り当て + +[English](README.md) | 日本語 + +この文書は [原文](README.md) の日本語訳です。 + +[前の章: 第19章](../chapter19/README.ja.md) | +[次の章: 第21章](../chapter21/README.ja.md) + +## 参考資料 + +これは Briggs-Chaitin-Click 型の割り当て器であり、過去25年間にわたり大きな成功を収めてきた HotSpot の C2 割り当て器に非常によく似ています。 + +Wikipedia にはレジスタ割り当て全般のよい概説があります: https://en.wikipedia.org/wiki/Register_allocation + +Chaitin の原論文: https://en.wikipedia.org/wiki/Register_allocation#CITEREFChaitin1982 + +Briggs による追加: https://dl.acm.org/doi/10.1145/177492.177575 + +Briggs/Chaitin のよい講義スライド PDF: https://aktemur.github.io/cs544/lectures/chapter%2013%20registerAllocation.pdf + +Briggs や Chaitin を組み合わせて検索すると、ほかにも役立つリンクがいくつか見つかります。基本的に、この時代にはレジスタ割り当ての改良が相次ぎ、その多くが高性能な主流コンパイラに取り込まれました。 + +グラフ彩色割り当て器の高速化に関する非常に古い未発表論文 [Interference Graph Trimming](docs/ifg_trim.pdf) を同梱しました。 + +## 命令選択、レジスタ割り当て、命令エンコーディングにまつわる全体的な問題と開発上の問題 + +レジスタ割り当ての出力、すなわち演算に対するレジスタの割り当ては、実行方法がないと大量にテストするのが困難です。命令選択、レジスタ割り当て、命令エンコーディングの3つすべてが完成するまでは、実行方法が得られません。そのため、エンコーディングの完成前後には、命令選択とレジスタ割り当てで多くのバグが見つかると予想しています。もちろん出力を手作業で調べて明らかな問題は修正しますが、実際にコードを動かせるようになるまで、多くのバグが残ります。 + +つまり、この章で説明する作業に対する更新やバグ修正が、ほかの章に含まれています。 + +特にレジスタ割り当て器については、これからかなりの数の章にわたり、章ごとに徐々に改良を追加します。割り当て器は一般に最終的には多数のヒューリスティックを持ちますが、それらを少しずつ追加します。 + +[linear](https://github.com/SeaOfNodes/Simple/tree/linear) ブランチ上の直線的な Git リビジョン履歴で [この章](https://github.com/SeaOfNodes/Simple/tree/linear-chapter20) を読み、前の章と [比較](https://github.com/SeaOfNodes/Simple/compare/linear-chapter19...linear-chapter20) することもできます。 + +## 基本理論 + +参考資料には基本概念の説明がいくつかありますが、重複を承知で、ここでも短く説明します。 + +読者はレジスタ割り当てが *なぜ* 必要かを知っていると仮定します。マシンレジスタは価値の高い有限資源であり、あらゆる有限資源と同様、適切に割り当てる必要があります。 + +グラフ彩色はそのような割り当て方法の1つです。マシンレジスタを色として扱い、各 *生存区間* が自身のレジスタ/色を必要とする *生存区間の干渉グラフ* を構築します。グラフを彩色することで、各生存区間が、特に別の生存区間と重なる、つまり *干渉する* 場合に、一意のレジスタを得ることを保証します。 + +彩色が成功すれば割り当ては終了し、いくらかの管理処理を経て、プログラムはたとえばコード出力の準備が整います。彩色が失敗したら、衝突する生存区間をスピルするか分割する必要があります。この割り当て器では、生存区間の *分割* を行います。分割により区間は短くなり、干渉するほかの区間が減るため、彩色しやすくなります。一般に、大きな関数は彩色可能になるまで数回の分割を必要とすることがあります。この問題は NP 完全なので保証はなく、最終的な割り当て品質は分割と彩色のヒューリスティックに大きく依存します。 + +したがって、彩色の中核アルゴリズムには多くの「高度な理論」を使いますが、実際の成功の多くは、多数のヒューリスティックに依存します。 + +### 彩色ラウンド + +彩色が得られるまで、次のラウンドを繰り返します。 + +- 生存区間を構築します。区間内のすべての命令は同じレジスタを必要とします。Phi 関数は無関係な定義/使用の辺を同じ生存区間に組み込み、衝突の多い大規模な相互接続区間を生み出すことがあります。一部のハードウェア演算は入力と出力に同じレジスタを要求し(X86 はこの点で悪名高い)、これも区間の数を減らして大きくします。互いに素な集合の Union-Find アルゴリズムを使い、プログラムを前向きに1回走査して高速に区間を構築します。 +- 干渉グラフを構築します。同時に生存する区間どうしは *干渉*、つまり衝突します。CFG を後ろ向きに走査する生存性解析で構築し、ループの入れ子深さに応じて複数回の走査が必要な場合があります。データ構造は通常2次元ビット集合で、三角形の部分だけを保持してサイズを半分にします。生存性解析の一部として、ブロックごとの到達定義集合も持ちます。 +- 干渉グラフを彩色します。グラフのノード(生存区間)のうち、次数が厳密に低いもの(隣接ノードより利用可能レジスタが多いもの)は、必ず色を得られます。衝突する隣接ノードが少ないのでレジスタ(色)を必ず得られるため、グラフから除去できます。この自明なノードの除去により、さらに多くのノードが自明になります。ノードが尽きて彩色が保証されるか、高次数のクリーク、すなわち相互に干渉し、どれも色を保証されない区間集合が見つかるまで繰り返します。後者では、彩色できないかもしれない「危険な」区間を1つ取り出します。グラフが空になったら、逆順に区間を戻しながら彩色します。自明な区間はすべて彩色できますが、危険な区間はできる場合もできない場合もあります。 +- すべての区間が色を得たら、レジスタ割り当ては終了です。そうでなければ分割ラウンドに入ります。彩色できなかった区間を、いくつかのヒューリスティックで多数の小さな区間に分割します。分割には相反する要求があります。多すぎるとスピルが増え、一般に割り当てが悪くなります。少なすぎると彩色不能のまま、割り当てに向けて前進しません。分割が終わったら、生存区間構築、干渉グラフ構築、彩色という新たなラウンドを始めます。 + +## レジスタとレジスタマスク + +レジスタこそ、レジスタ割り当ての対象です。0から始まる小さな密な整数で表し、番号は一般にハードウェアのエンコーディングに従います。X86_64 では RAX が0、RCX が1となり、16個の GPR まで続きます。XMM レジスタ番号は16から31です。番号は一意でなければなりません。割り当て器はこれによってレジスタを追跡します。 + +#### 単純な `long` ではなく RegMask クラスを使う理由 + +レジスタ集合はレジスタマスクに格納します。小さく単純なマシンなら i64 や i128(64または128ビット整数)で十分であり、すべての割り当てがこのビット数に収まるなら、この表現がはるかに優れています。割り当て器は、スタック経由のパラメータと、レジスタを得られない分割にもビットを必要とします。X86 のような32レジスタのマシンでは、フラグ用の1つを加えて33レジスタです。Java の `long` は64ビットなので、スピルとスタック渡しに31ビットが残ります。これはほぼすべての割り当てに十分で、最大級のものだけが使い切ります。しかし64レジスタのチップへ移ると、すぐに不足し、少なくとも128ビットのマスクが必要です。Java では128ビット値を直接 *返せない* ため、Simple は `RegMask` クラスのオブジェクトを使います。 + +#### スタックスロット + +Click による拡張の1つは「スタックスロット」を単なる別のレジスタとして扱う考え方です。ほかのレジスタと同様に彩色することで、スタックを非常に効率よく使えます。C2 は非常に小さいスタックフレームで知られています。もう1つの利点は、callee-save レジスタが優先して分割されることです。生存期間が非常に長く、入口での定義と出口での使用が1つずつしかないためです。その分割は最終的にスタックに彩色され、自然に呼び出しのプロローグとエピローグを構築します。この割り当て器は、それ以外にスタックフレーム管理のための特別な処理を行いません。結果として自然に得られます。 + +多くのレジスタマスクは不変です。たとえば特定のオペコードで許されるレジスタや、呼び出し規約を表すものです。一方、制約集合を蓄積する際に割り当て器が日常的にビットを落とす、可変のビット集合を表すマスクもあります。使い方を区別しやすくするため、`RegMask` クラスには可変版と不変版があります。 + +#### メモリ + +メモリ辺は順序を強制するために存在しますが、レジスタは得ません。割り当て器は基本的にそれらを無視します。 + +### Callee-save / Caller-save レジスタ + +この割り当て器は callee-save と caller-save のレジスタをほぼ特別扱いせず、保存・復元専用のプロローグやエピローグも構築しません。割り当て開始時に、各 `Return` と、各 `Fun` に由来する `CalleeSave` との間に SoN グラフ辺を追加します。通常の ABI が定義する callee-save 集合に固定するレジスタマスクを、これらの辺に与えます。 + +callee-save の生存区間は、関数先頭の `CalleeSave` という単一定義と、`Return` という単一使用を持ちます。安くスピルでき、広い範囲にわたってレジスタを解放するので、理想的なスピル候補です。レジスタを増やすために何かをスピルする必要があると、これらが最初にスピルされます。通常の生存区間分割の結果として、プロローグとエピローグが構築されます。 + +`Calls` は caller-save レジスタを破壊します(callee-save マスクを反転したものです)。これはレジスタを破壊するほかの演算と同様、通常の干渉グラフ構築で計算します。呼び出しで多数のレジスタを破壊すると、その時点で生存しているものから利用可能なレジスタがすべてなくなることが多く、スピルが強制されます。その結果、すぐに理想的な候補である callee-save レジスタがスピルされます。 + +## 生存区間 + +生存区間は、同じレジスタを得なければならないノードと辺の集合です。形状にほとんど制限のない、相互接続された網を形成します。また、すべての構成部分のレジスタ制約を集約します。 + +生存区間は `LRG` クラスのインスタンスとして保持します。そのフィールドの大半はスピルのヒューリスティック用ですが、LRG を定義する重要なフィールドもいくつかあります。`RegAlloc` には、Node から LRG への参照表、すなわち `Node` から `LRG` への対応を含む、LRG を扱う関数がいくつもあります。これは各 Node に専用の LRG フィールドを持つのと非常によく似ていますが、LRG は RegAlloc 中だけ使うので、それ以外ではフィールドが無駄な重荷になります。 + +LRG は自身の名前となる、一意で密な整数 `_lrg` 番号を持ちます。新しい番号は `RegAlloc._lrg_num` カウンタから得ます。LRG は結合できます。これが [Union-Find アルゴリズム](https://en.wikipedia.org/wiki/Disjoint-set_data_structure) です。結合時には、小さい `_lrg` 番号が代表になります。結合は `BuildLRG` 中だけ行い、`Phi` がすべての入出力を同じレジスタに強制するか、2アドレス命令のために発生します。LRG には対応する `union`、`find` 呼び出しと、設定された `_leader` フィールドがあります。 + +割り当て後、`LRG._reg` は選択されたレジスタを保持します。割り当て中、`LRG._mask` は利用可能なレジスタ集合を保持します。通常はすべての既定値から各種の衝突を除いたものです。 + +`LRG._adj` は干渉グラフ全体の一部として隣接ノードのリストを保持し、彩色フェーズでだけ使います。[グラフについての詳細はこちらです。](https://en.wikipedia.org/wiki/Graph_(abstract_data_type)) これはグラフの「隣接リスト」表現で、IFG の2次元ビット集合(それ自体は `BitSet` の1次元リスト)から構築します。グラフ彩色によるレジスタ割り当ては、アルゴリズムの途中でデータ構造のレイアウトを変えることが有効な、数少ない場面の1つです。 + +残りのフィールドは、各種スピルのヒューリスティック専用です。 + +- `_machDef` `_machUse` `_uidx`: 代表となる定義と使用。 +- `_splitDef` `_splitUse`: この LRG に関係する代表的な分割。バイアス付き彩色はレジスタ選択を合わせて、これらを除去しようとします。 +- `_selfConflicts`: *自己衝突* ノードの部分集合。干渉グラフ構築時に検出し、積極的な分割を必要とします。 +- `_1regDefCnt` `_1regUseCnt`: 単一レジスタに固定された定義と使用の数。通常、レジスタ選択の自由を取り戻すため、使用の直前または定義の直後で強制分割を必要とします。 + +## 生存区間と衝突 + +生存区間の `_mask` が空になると、*ハード衝突* か *破壊* が起きた可能性があります。区間は自分自身とも干渉し、*自己衝突* を起こします。彩色中に、干渉区間の彩色不能なクリークが見つかり、*容量不足のスピル* に至ることもあります。このため区間内にはいくつかの衝突レベルがあり、それぞれに異なる対処のヒューリスティックを使います。 + +### ハード衝突 + +生存区間は、レジスタ要求が衝突し、有効な選択肢がなくなる *ハード衝突* を起こすことがあります。明らかな例は、入口のパラメータが `rdx` に固定されながら、出口の値として `rax` にも必要になる場合です。両方を同時に選べないので分割が必要です。生存区間構築フェーズでは、すべての定義と使用のレジスタ制約を AND します。`RegMask` は有効レジスタをすべて失うか、わずかな選択肢だけを残すことがあります。 + +ハード衝突は通常、生存区間構築時に見つかり、干渉グラフ構築や彩色の前に分割ラウンドを引き起こします。干渉グラフ構築中にも見つかる場合があり、一般に1レジスタの破壊が原因です。 + +### 干渉の回避 + +Briggs-Chaitin 割り当て器に対する Click の拡張の1つは、レジスタ制約を利用して干渉グラフの次数を下げることです。そうでなければ辺集合は O(n^2) になります。LR1 が、`rdx` を必要とする呼び出し引数などによって単一レジスタに制限され、より多くの選択肢を持つ LR2 と本来衝突する場合、LR2 の選択肢から `rdx` を除きます。`rdx` がなければ両者に共通レジスタがなくなり、衝突しません。その間の干渉グラフ辺を単に追加しないのです。呼び出し引数の制限は非常に一般的で、この辺除去は O(n^2) の辺集合を大きく縮小し、割り当てを大きな定数倍だけ高速化できます。 + +### 自己衝突 + +生存区間は、異なる2つの値が同時に生存しながら同じレジスタを要求する、*自己衝突* を起こせます。この区間は *必ず* 分割し、その結果としてコピーを得ます。単純な `fib` プログラムなどの、ループをまたぐ依存関係で起きます。 + +```java + int x=1, y=1; + while( true ) { + tmp = x + y; + y = x; + x = tmp; + }; + +``` + +SoN の SSA 形式では: + +```java + int x0 = 1, y0 = 1; + while( true ) { + x1 = phi(x0,x2); + y1 = phi(y0,x1); + x2 = x1 + y1; + } +``` + +変数 `x0,x1,x2,y0,y1` はすべて `Phi` で結合されるので、同じ生存区間の一部です。しかし `x1` と `y1` は `fib` の異なる反復に由来する別々の値を保持します。SSA 形式にはコピーがありませんが、レジスタ割り当て後の形式は、元のコードと同様にコピーを必要とします。 + +自己衝突は干渉グラフ構築中に見つかり、彩色フェーズを試す前に分割ラウンドを引き起こします。 + +## 彩色のヒューリスティック + +レジスタの彩色も分割もヒューリスティックが多数あり、その質が最終的な割り当て品質を大きく左右します。グラフ彩色は NP 完全であり、効率的な彩色アルゴリズムはありません。いずれにせよ、多くのプログラムは分割しなければ彩色できません。 + +### バイアス付き彩色 + +バイアス付き彩色は、Split を除去できるように「よりよい色を選ぶ」、あるいは「色の選択に偏りを付ける」ヒューリスティックです。Split は1つの生存区間を2つに分けますが、必要のない分割が挿入されることもあります。つまり、すでに過剰な分割が起きています。片側の区間が彩色済みで、もう片側も同じ色にできれば、その分割は除去できます。どちらも未彩色でも、片側が再帰的に彩色済み区間に接続されていれば、分割の連鎖がすべて同じ色になって除去されることを期待して、色に偏りを付けられます。 + +彩色処理が逆順に戻り、区間の色を選ぶとき、多くのレジスタ選択肢があることがよくあります。まず任意に、番号が最小のレジスタを選び、それからよりよい色を探します。バイアス付き彩色は代表的な定義を先に調べ(1つしかなければその唯一定義)、次に代表的な使用を調べます。分割の「反対側」が彩色済みかを確認し、そうであり、その色を使えれば採用します。分割があっても未彩色なら、その反対側で再帰的に繰り返します。2アドレス演算のもう1つの入力や、Phi ノードの入力の1つについても行います。 + +どの場合も、現在利用でき、将来この Split の反対側にも選べるかもしれない有効な色を選ぶのが目標です。それによって両側が同じ色になり、分割を除去できます。 + +### 危険な区間の選択 + +ここでは、大きなクリークを作る複数の *生存区間* から1つを選ばざるを得ません。相互の隣接ノードが多すぎて、どれも色を保証されません。ここには相反する要求があります。 + +「面積」が大きく、つまりプログラムの広い範囲を覆い、スピルの「コスト」が低い、つまり定義と使用が少なく、ループ外にある区間を選べば、大きな効果があります。プログラムの広い範囲でレジスタが空き、ほかの場所で彩色が成功できます。通常の ABI の callee-save レジスタは理想的です。入口で定義し出口でだけ使うため、プログラム全体を覆います。スピルも入口と出口で1回ずつ分割するだけです。このような区間のスピルは、基本的に古典的な関数のプロローグ/エピローグ列を構築します。ただし、この割り当て器は、たとえば高速経路の出口より外側でスピルすることも許されます。 + +``` + // No registers saved if the initial RDI is null + test rdi + jne BIG_AND_SLOW + xor rax,rax + ret // Fast path exit: null argument means null result +BIG_AND_SLOW: + add rsp,#12 + mov [rsp+0],rbx // begin spilling callee-save registers + mov [rsp+4],r12 // + // lots of code needing callee-save registers +``` + +もう一方の要求は、偶然によって彩色できそうな区間を選ぶことです。「ほとんど彩色可能」、たとえば利用可能なレジスタ/色より隣接ノードが1つだけ多い区間なら、偶然やバイアス付き彩色によって隣接ノードのいくつかが同じ色を使い、色が空いて、この危険な区間も結局彩色できるかもしれません。 + +## 分割 + +ここでは彩色に失敗したので、分割が必要です。特定の区間が彩色できず、分割方法を決めなければなりません。ここにも相反する要求があります。分割が多すぎると割り当てが悪くなり、少なすぎると進捗しないバグになり、任意の上限を超えるほど多くの分割ラウンドとして現れます。 + +まずスピルをソートして決定的にします。そうしないと HashMap の巡回順になります。それから各スピル対象の区間を順に調べ、分割戦略を決めます。 + +### 自己衝突の分割 + +自己衝突区間は「干渉グラフ構築」段階で発見します。この場合、自己衝突がある状態での彩色には意味がないので、彩色を試さず直接分割へ進みます。IFG 構築中に、この区間の衝突する定義の部分集合を収集しました。それらの定義位置を巡回し、スピルを挿入します。 + +- Phi のスロット #1 の前(ループ入口の Phi をループ本体から切り離す)と、Phi の後。 +- 2アドレス命令の前。演算の前後で区間を分断します。 +- 生存区間を延長する使用の前。 + +この分割集合はかなり積極的ですが、列挙した各位置で分割を必要とするテストを第20章に含めています。自己衝突がある間は彩色を試すことさえできないため、素早く区間を切り分けることが重要です。過剰分割になりがちなので、割り当て器はバイアス付き彩色と最終コピー整理に頼って余分なものを除去します。保守的な併合は後の章で導入します。 + +### ハード衝突の分割 + +直接の定義/使用の衝突(たとえば `rdi` で定義され、`rax` で使われる)は「生存区間構築」走査で発見します。「干渉グラフ構築」走査でも発見できます。たとえば、`rdi` で入る引数が後で使われるものの、`Call` に破壊される場合です。 + +単一定義と単一使用の単純なハード衝突では、正確な定義と使用を生存区間自身に保持しています。定義の後と使用の前で1回ずつ、レジスタが制限された側にだけ分割を行います。 + +より複雑な場合は、容量不足のスピル/ループ入れ子の手法を使います。複製可能な値は使用の近くで再生成できますが、その複製の出力レジスタクラスが使用を満たせる場合だけです。そうでなければ移動が必要です。固定レジスタの定数に柔軟な使用がある場合も、使用で分割しなければなりません。定義で何もしないと、同じハード衝突を永遠に繰り返します。 + +よく使われる定数(たとえば0)は、異なるレジスタを要求する多くの使用を持ちます。使用をレジスタクラスごとにまとめれば移動を減らせますが、このヒューリスティックは後の章まで延期します。 + +### ループの入れ子に沿った分割 + +ここでは通常、容量不足のスピルが起きています。単にもっとレジスタが必要です。`Call` が一時レジスタをすべて破壊し、値を呼び出しの先へ運ぶ必要があるときに起きやすいですが、大きな関数で多くのことが同時に起きる場合にも発生します。 + +ヒューリスティックは、頻繁に実行されるループ本体がスピルしないよう、利用可能レジスタを残すためにループの *周囲* で分割します。分割ラウンドが進むにつれ、区間をより深い入れ子へ分割します。最後には、最内周ループであっても各定義の後と各使用の前で分割します。 + +まず、すべての定義と使用の最小・最大ループ深さを求めます。それらが異なれば最外周ループの *周囲*、すなわち境界で分割し、外側からの制約を持たない内側の区間はそのまま保持します。それでも彩色できなければ、次のラウンドでは最外周ループが1段内側へ移り、次の内側の入れ子の周囲で分割します。最内周ループ内で分割するまで、この処理を繰り返します。 + +### 補助関数 + +RA は、比較的自己完結した一連の補助関数を使います。 + +- **findAllLRG**: 区間のすべての定義と使用を、O(N) ではなく O(|LRG|) 時間で見つけます。 +- **insertBefore** と **insertAfter**: 使用の前か定義の後に分割を挿入します。基本ブロック内の正しい順序に置く処理と、すべての辺の保守を含みます。 +- **makeSplit**: 分割を作ります。複製可能な定数を複製するか、`Machine` に移植先固有の `SplitNode` を要求します。 +- **ldepth**: `long` に詰めた最小・最大ループ深さを受け取り、指定ノード `n` に対する深さ範囲を拡張します。定義や使用がループ「内」にあるかを判定し、ループ周囲の分割に使います。 + +## 割り当て後 + +彩色成功後、両側が同じレジスタの分割を取り除きます。分割を素早く走査し、レジスタを確認して不要なものを除去します。残ったものは割り当ての「採点」用に追跡します。また、分割の後の分割の一部を迂回し、冗長なコピーを除去できることもあります。 + +レジスタは `RegAlloc` オブジェクトの `alloc.regnum( Node n )` から引き続き利用でき、次の命令エンコーディング走査で使います。 + +## RegAlloc の改良: 正しさと測定された基準値 + +この章は小さな割り当て器から始まります。ハード衝突は実際に断ち切らなければなりません。同じブロックに別のコピーがあるだけで分割を省略したり、衝突を復活させるような除去をしたりしてはいけません。ループの後退辺も、特に Phi が並列代入を表す場合には同じ注意が必要です。出力値のない命令にもレジスタ破壊は適用されます。最後に、冗長なコピーの除去には、間にある定義やレジスタ破壊を探す後ろ向き走査が必要です。これらはスピル品質とは独立した、正しさの規則です。 + +分割前に失敗した区間をソートし、同一性マップの順序の変化が割り当てを決めないようにします。割り当て器には null に安全な分割表示と、任意の関数内辺の診断もあります。テストはレジスタマスク、2アドレスのオペランド、Phi のレジスタ一致を確認します。小さなマシングラフが、最適化器の選択とは独立にハード衝突を検証します。ネイティブ実行は、第21章のエンコーディングとともに導入します。 + +### テスト全体で合計したスピル数 + +`make spill-stats` を実行すると、テストを実行し、各コンパイルの数、CPU/ABI ごとの小計、以下の合計を表示します。アサーションは有効なままで、テストが失敗すればコマンドも失敗します。最適化器の既定シードは123です。 + +第20章のプログラム群は `Chapter20Test` の11のソース例すべてと、`BrainFuckTest.testBrainfuck`、`MergeSortTest.testMergeSort` を、x86-64、RISC-V、ARM の SystemV でコンパイルしたものです。スピル0のケースを含む13プログラム、39コンパイルです。人工的な割り当て器の回帰グラフは正しさの確認であり、追加のプログラムベンチマークではありません。 + +| プログラムテスト群 | 割り当て器の章 | コンパイル数 | 分割による移動 | ループ重み付き移動 | +|---|---:|---:|---:|---:| +| 第20章 | 20 | 39 | 234 | 360 | + +`_spills` はレジスタ間コピーも含む、残存した分割移動を数えます。スタックへのストアだけではありません。`_spillScaled` は各移動を `8^loopDepth` で重み付けします。これは粗いコスト推定で、測定された実行頻度ではありません。重み付き小計は x86-64 が161、RISC-V が100、ARM が99です。 + +### 測定についてのコメント + +正しさの修正前、この群は235移動/354重み付き移動でした。遅延メモリ導入前の修正済み基準値は、両方が3増えました。そのうち2移動は、最終的な x86 ストアマスク制限のバックポートによるものです。整数のバイト/short ストアは XMM レジスタを消費できません。これは正しさのためのコストで、品質の向上ではありません。固定隣接ノードへの色バイアスの修正は、この群の数を変えません。 + +遅延メモリと選択済み読み取りの順序付けにより、同じ39コンパイルは導入前の238/357から、234移動/360重み付き移動になりました。すべての対象でレジスタの合法性が確認でき、割り当て器のヒューリスティックは変更していません。変化したプログラムは次のとおりです。 + +| プログラム | 対象 | 移動の前/後 | 重み付きの前/後 | +|---|---|---:|---:| +| 配列アクセス(`testAlloc2`) | x86-64 | 1 / 2 | 1 / 2 | +| 配列の累積和(`testArray1`) | x86-64 | 5 / 6 | 5 / 13 | +| 文字列ハッシュ | x86-64 | 7 / 5 | 21 / 19 | +| 文字列ハッシュ | RISC-V | 4 / 3 | 4 / 3 | +| 文字列ハッシュ | ARM | 5 / 3 | 5 / 3 | +| マージソート | ARM | 37 / 36 | 37 / 36 | + +累積和の追加のループ移動は、選択済み読み取りを、それを破壊する書き込みより前にスケジュールすることから生じます。その順序を省く診断ビルドでは5/5に戻りますが、有効な代替スケジュールではありません。これはフロントエンドとスケジューリングのグラフの変化であり、割り当て器のヒューリスティックの比較ではありません。新しいメモリ割り当ての回帰テストは合法性を確認しますが、この測定群に項目を追加しません。 + +後の章では、品質向上の手法を1つずつ追加し、このような表で締めくくります。第21章では自身の割り当て器で第20章と第21章のプログラム群を測定し、第25章では第20〜25章の群の行を持ちます。章をまたいで行を比較するときは、プログラム/対象の構成とシードを固定し、最適化器や命令選択の変化を割り当ての変化から分けてください。全体が改善するなら局所的な悪化に価値がある場合もありますが、割り当ての失敗や違法なレジスタ割り当てに価値はありません。 diff --git a/chapter20/README.md b/chapter20/README.md index f3c566e5..b4d4209f 100644 --- a/chapter20/README.md +++ b/chapter20/README.md @@ -1,5 +1,7 @@ # Chapter 20: Graph Coloring Register Allocation +English | [日本語](README.ja.md) + [Previous: Chapter 19](../chapter19/README.md) | [Next: Chapter 21](../chapter21/README.md) diff --git a/chapter20/docs/18-grammar.ja.md b/chapter20/docs/18-grammar.ja.md new file mode 100644 index 00000000..f0b12acb --- /dev/null +++ b/chapter20/docs/18-grammar.ja.md @@ -0,0 +1,138 @@ +# 第20章の文法 + +[English](18-grammar.md) | 日本語 + +この文書は [原文](18-grammar.md) の日本語訳です。 + +> 訳注: 原文の見出しは「Grammar for Chapter 16」です。この日本語版では、配置されている第20章に合わせた見出しにしています。 + +[第20章に戻る](../README.ja.md) + +```antlrv4 +grammar SimpleLanguage; + +program : block EOF ; + +block : statement+ ; + + +statement + : '{' block '}' + | returnStatement + | ifStatement + | whileStatement + | breakStatement + | continueStatement + | structDeclaration + | expressionStatement + ; + + +returnStatement : 'return' expression ';' ; + +ifStatement : 'if' '(' expression ')' statement ('else' statement)? ; + +whileStatement : 'while' '(' expression ')' statement ; + +breakStatement : 'break' ';' ; + +continueStatement : 'continue' ';' ; + + +structDeclaration : 'struct' IDENTIFIER '{' block '}' ; + +expressionStatement + : type IDENTIFIER ';' + | type IDENTIFIER '=' expression ';' + | IDENTIFIER '=' expression ';' + | expression + ; + +PRIMTYPE + : 'int' + | 'i8' + | 'i16' + | 'i32' + | 'i64' + | 'u8' + | 'u16' + | 'u32' + | 'flt' + | 'f32' + | 'f64' + | 'bool' + ; + +type + : PRIMTYPE + | typeName '?'? + | typeName nestedArray+ + ; + +nestedArray : '?'? '[]'* ; + +typeName : IDENTIFIER ; + + +expression : bitWiseExpression ; + +bitWiseExpression + : comparisonExpression ( '&' | '|' | '^' ) comparisonExpression)* + ; + +comparisonExpression + : shiftExpression (('==' | '!='| '>'| '<'| '>='| '<=') shiftExpression)* + ; + +shiftExpression + : additiveExpression ( '<<' | '>>>' | '>>' ) additiveExpression)* + ; + +additiveExpression + : multiplicativeExpression (('+' | '-') multiplicativeExpression)* + ; + +multiplicativeExpression + : unaryExpression (('*' | '/') unaryExpression)* + ; + +unaryExpression + : ('-') unaryExpression + | '!' unaryExpression + | primaryExpression postAssign + ; + +primaryExpression + : INTEGER_LITERAL + | FLOAT_LITERAL + | '(' expression ')' + | 'true' + | 'false' + | 'null' + | newExpression + | IDENTIFIER + ; + +newExpression + : 'new' IDENTIFIER [ '{' block '}' ] + | 'new' IDENTIFIER '[' expression ']' + ; + +postAssign : postFix [ '=' expression ] | [ '#' ]; + +postFix + : '.' IDENTIFIER postFix + | '[' expression ']' postFix + ; + + +IDENTIFIER : NON_DIGIT (NON_DIGIT | DIGIT)* ; + +INTEGER_LITERAL + : [1-9]DIGIT* + | [0] + ; + +NON_DIGIT: [a-zA-Z_]; +DIGIT: [0-9]; +``` diff --git a/chapter20/docs/18-grammar.md b/chapter20/docs/18-grammar.md index 7da725e9..51539d0b 100644 --- a/chapter20/docs/18-grammar.md +++ b/chapter20/docs/18-grammar.md @@ -1,5 +1,7 @@ # Grammar for Chapter 16 +English | [日本語](18-grammar.ja.md) + ```antlrv4 grammar SimpleLanguage; diff --git a/chapter21/README.ja.md b/chapter21/README.ja.md new file mode 100644 index 00000000..dbf11374 --- /dev/null +++ b/chapter21/README.ja.md @@ -0,0 +1,118 @@ +# 第21章: 命令エンコーディングと ELF + +[English](README.md) | 日本語 + +この文書は [原文](README.md) の日本語訳です。 + +[前の章: 第20章](../chapter20/README.ja.md) | +[次の章: 第22章](../chapter22/README.ja.md) + +この章では、スケジューリングとレジスタ割り当てを終えたグラフを実行可能な命令に変換します。各マシンノードがバイトを書き込み、コンパイラがブロックを配置し、局所参照を解決し、リンカ向けの ELF オブジェクトを出力します。これで生成コードを実行できるため、命令選択、レジスタ割り当て、呼び出し規約も一緒にテストできます。 + +直線的なリビジョン履歴で [この章](https://github.com/SeaOfNodes/Simple/tree/linear-chapter21) を読み、[第20章と比較](https://github.com/SeaOfNodes/Simple/compare/linear-chapter20...linear-chapter21) することもできます。 + +## マシンノードからバイトへ + +命令選択は演算とレジスタ制約を選びます。レジスタ割り当ては物理レジスタとスタックスロットを割り当てます。その後、エンコーディングがオペコード、割り当て済みレジスタ、即値オペランドを命令バイトへ組み合わせます。この時点で命令は別のレジスタを選べません。周囲の命令が、すでに割り当て器の選択に依存しているためです。 + +[Encoding.java](src/main/java/com/seaofnodes/simple/codegen/Encoding.java) がバイトストリームを所有し、各ノードのオフセットとエンコード後の長さを記録します。ドライバは次を行います。 + +1. 基本ブロックを配置し、必要に応じて分岐を挿入または反転する。 +2. スケジュール順に `MachNode.encoding(Encoding)` を呼ぶ。 +3. 短い命令形式と長い命令形式を確定する。 +4. このコンパイル内に対象がある参照を修正する。 + +補助関数 `add1`、`add2`、`add4`、`add8` は、リトルエンディアンの値を追加します。エンコーディング関数は `Encoding.reg` で割り当て済みレジスタを得ます。スタックオペランドは、保存レジスタや出力引数の領域も含む、関数のフレーム配置を使います。彩色成功だけでは呼び出し規約の正しさは証明できません。生成プログラムは呼び出し側の値も保存しなければなりません。 + +## 3つの命令セット + +対象はこのドライバを共有します。マシンノードがコンパイラのオペランドを解決し、共有の [ISA エンコーダ](../isa/README.md) を呼びます。 + +| 対象 | 主なエンコーディング上の課題 | +|---|---| +| RISC-V | 命令語内のレジスタと即値のフィールド。大きい定数とアドレスには複数命令が必要。 | +| AArch64 | レジスタ形式、制限された即値エンコーディング、複数命令による定数構築。 | +| x86-64 | 可変命令長、レジスタ接頭辞、アドレッシング形式、短い分岐と長い分岐。 | + +たとえば小さな整数は命令に直接収まる一方、大きな整数には複数命令や定数プールからのロードが必要なことがあります。その結果、固定幅命令を持つ対象でも、マシンノードによって異なるバイト数を出力できます。 + +[エンコーディングリファレンス](docs/encoding-reference.ja.md) は、詳細なビット配置、具体例、ISA リンクを保持しています。個々の命令実装ではそれらの詳細が重要ですが、ここで説明するコンパイラのパイプラインは変わりません。マシンノードの接続は [node/cpus](src/main/java/com/seaofnodes/simple/node/cpus) にあります。共有の [X86 エンコーダ](../isa/src/main/java/com/seaofnodes/isa/X86.java) が x86 のバイト出力を所有し、[Arm64](../isa/src/main/java/com/seaofnodes/isa/Arm64.java) と [RiscV](../isa/src/main/java/com/seaofnodes/isa/RiscV.java) は命令語を詰めます。 + +たとえば `AddX86` はオペコード `0x03` を選びます。その基底 `RegX86` は割り当て済みの2レジスタを読み、`X86.reg(enc,opcode(),dst,src)` を呼びます。共有メソッドは REX.W、オペコード、ModRM を `Encoding` のバイトバッファへ直接書き込みます。`add r9,r10` のバイト列は `4d 03 ca` です。レジスタ制約と2アドレスの要求は、この章のマシンノードに残ります。これによって、ADD を使う判断と、命令の固定されたビット表現を分離します。 + +ロードも同じ境界を使います。この章は、型と割り当て済みオペランドから、幅、符号拡張、レジスタバンクを解決します。`X86.load` はそれらの情報と具体的なアドレスを受け取り、この章のグラフや型の束に依存しません。後の章はコンパイラが進化しても、これらのエンコーディングを再利用します。 + +ARM と RISC-V の実行テストも `isa/src/test-support` の評価器を再利用します。この章のテストハーネスがメモリイメージを準備してリンクし、評価器がそのバイト列を実行します。こうして命令の意味を1か所に保持しつつ、イメージ構築は各章のコンパイラに従います。評価器はテストとともにコンパイルします。Make のリリース jar は、引き続きコンパイラとテストハーネスの両方を同梱します。 + +## 再配置と ELF + +前方分岐は、最初にバイトを書き込む時点では最終的な変位を知りません。対象のオフセットは、間にあるすべてのエンコーディングに依存します。別の分岐を短縮・拡張すると、対象が再び動く場合があります。配置走査が命令サイズを確定してから、局所再配置が最終的な変位を修正します。 + +外部参照には、後での修正が必要です。たとえばコンパイラは、リンカが `malloc` をどこへ配置するか知らなくても、その呼び出しを出力できます。オブジェクトファイルはシンボル、修正位置、再配置種別を記録します。種別は命令と一致しなければなりません。x86 の呼び出し変位と、分割された RISC-V のアドレスは別のフィールドを占めるので、同じ修正演算は使えません。 + +[ElfFile.java](src/main/java/com/seaofnodes/simple/codegen/ElfFile.java) は、オブジェクトのコード、データ、シンボル、再配置記録を書き込みます。大きな定数も外部呼び出しと同じ配置問題を生じます。最終的なアドレスを、それらをロードする命令に接続しなければなりません。局所再配置とオブジェクトファイル再配置は、参照を正しくするという同じ責務の2段階です。 + +## 結果の実行 + +第11章の遅延メモリ分割が、エンコーディングまで引き継がれます。パラメータ、呼び出し、戻りはメモリ全体を運び、`BulkMemPhi`、`MemPhi`、`MemMerge` が各関数内の正確なエイリアスを発見します。New は `{ctrl, $mem, size}` を消費し、`{ptr, $mem}` を生成します。そのメモリは割り当てた構造体のエイリアスを覆い、周囲のマージは無関係なエイリアスを保持します。非ゼロ初期化は依然として明示的な Store を使います。メモリノードと順序付け辺はレジスタを必要としません。 + +スケジューリングは集合体を通じてエイリアスをたどり、対応する Store、割り当て、呼び出しの前に読み取りを順序付けます。Call は順序付けオペランドを追加せず、引数リストを保持します。合成されたループ出口は、エイリアスごとに正確なメモリを集めます。 + +実行の回帰テストは、コンストラクタ、インライン化されない呼び出しと再帰呼び出し、インライン化、配列更新を、ネイティブ x86 と両エミュレータで検証します。整数が XMM にある場合もあるため、全幅の x86 ロードはエンコーディング選択時に割り当て済みのレジスタバンクを使います。ARM エミュレーションは、これらの確認で使う大きな定数に対する MOVZ/MOVK の半語構築を処理します。 + +`make tests` は、生成 x86 コードをリンクして実行する C ハーネスと、RISC-V/AArch64 エミュレータの確認を含みます。結果とスピル数の両方を比較します。ネイティブプロセスの失敗は完全な終了ステータスを保持します。エミュレータの確認は、自身の結果メモリを読まなければなりません。別の対象の結果と一致するだけでは不十分です。 + +`make spill-stats` を実行すると、アサーションを有効にして同じテストから割り当てを収集します。各テスト/CPU/ABI を報告してから、群の合計を表示します。スピルの期待値との不一致は、残りの対象や実行確認を省略せず記録します。そのような不一致が1つでもあればコマンドは失敗します。構造的な割り当て器の回帰テストはスピル合計から除外します。 + +## RegAlloc の改良: コピーの除去 + +分割は2つの生存区間間にコピーを挿入します。両方を安全に1区間へまとめられるなら、彩色前にコピーを除去できます。[Coalesce.java](src/main/java/com/seaofnodes/simple/codegen/Coalesce.java) は、区間が干渉しないことを確認し、許可レジスタマスクの共通部分を取り、隣接ノードをまとめます。結果の隣接ノード数が利用可能レジスタ数より小さいときだけ併合します。この単純で保守的な規則は、各隣接ノードが異なるレジスタを取っても、色の選択肢を残します。 + +併合は隣接するノードの隣接リストも更新し、削除したコピーへの代表参照を除去します。拒否された併合は元の隣接リストを復元します。マスクの互換性だけでは不十分です。同時に生存する2値は、両方が同じレジスタを許しているだけでは共有できません。 + +彩色後にも、直後の命令に1つだけ使用があるコピーは、そのオペランドがコピー元レジスタを受け入れ、2アドレスのオペランドでなければ除去できます。たとえば `mov rpc=s8; st1 [rpc+4],s1` は `st1 [s8+4],s1` になります。隣接していることで、間にコピー元を上書きするものがないと証明できます。Phi と制御フローの使用側は除外します。この局所整理はスピル選択を変更せず、別の CFG 解析をせずに併合を補完します。 + +互換性のある再実体化や、破壊を考慮するコピー再利用など、第20章の合法性と進捗の修正も引き継ぎます。第21章はネイティブフレームと ABI のサポートを保持します。より強い色バイアスと安いスピルの順位付けは第22章、多数の使用のグループ化は第23章、低頻度側を先にするループ分割は第24章、面積/コストの順位付けは第25章に残します。 + +以下は **この章のコンパイラ** を使い、最適化器の既定シード123で Windows x86-64 上で測定した合計です。分割はレジスタ移動を含む残存 `SplitNode` であり、必ずしもメモリスピルではありません。重み付き数は各分割に `8^loopDepth` を掛けます。 + +| プログラム群 | コンパイル数 | 残存する移動 | ループ重み付き移動 | +|---|---:|---:|---:| +| 第20章 | 39 | 380 | 576 | +| 第21章 | 52 | 467 | 1,076 | +| **合計** | **91** | **847** | **1,652** | + +第20章の行は、BrainFuck と MergeSort を含む元の13入力すべてを、3つの SystemV 対象について固定しています。これらは `Chapter20Test` にあります。以前そのファイルにあった改訂済みソース/ABI ケースは `Chapter21AllocTest` に保持し、`Chapter21Test`、ネイティブ BrainFuck/MergeSort テストと合わせて第21章の行を構成します。その行には ARM/SystemV が17、RISC-V/SystemV が17、x86/SystemV が8、x86/Win64 が10コンパイルあります。ネイティブホスト ABI の変更はこの行に影響するため、同じホストと対象で比較してください。 + +隣接コピー転送の前、メモリ移植によって、群の構成と割り当て器のヒューリスティックを変えずに、以前の合計840/1,456が845/1,559になりました。変化した行は以下のすべてです。各項目は残存/ループ重み付き分割です。 + +| 群/プログラム | 対象/ABI | 変更前 | 遅延メモリ | +|---|---|---:|---:| +| 20 / MergeSort | ARM / SystemV | 44 / 44 | 41 / 41 | +| 20 / BrainFuck | RISC-V / SystemV | 35 / 42 | 37 / 58 | +| 20 / Alloc2 | x86 / SystemV | 3 / 3 | 4 / 4 | +| 20 / Alloc2 | ARM / SystemV | 10 / 10 | 9 / 9 | +| 20 / String | x86 / SystemV | 7 / 14 | 8 / 15 | +| 20 / String | RISC-V / SystemV | 14 / 14 | 10 / 10 | +| 20 / String | ARM / SystemV | 13 / 13 | 10 / 10 | +| 21 / Sieve | x86 / Win64 | 24 / 178 | 24 / 185 | +| 21 / Sieve | RISC-V / SystemV | 19 / 89 | 22 / 92 | +| 21 / Sieve | ARM / SystemV | 23 / 93 | 21 / 91 | +| 21 / BrainFuck | RISC-V / SystemV | 35 / 42 | 46 / 130 | + +重み付き増加の大半は RISC-V BrainFuck によるものです。read-before-New の順序付けだけを省く診断実行では、その2行は35/42と37/58になります。これでコストの大半を説明できますが、コンパイラは順序制約を保持します。メモリ移植にヒューリスティックの調整は含めていません。 + +続く Load 検索の改良は、ループの N 方向メモリマージを通じて BrainFuck の固定プログラム長を畳み込みます。すべての後退辺の枝が畳み込めるか、同じポインタについて変更されていないメモリへ戻る必要があります。RISC-V の Hello World 実行で907回のヒープロードを除去し、スタックアクセスを1,006ロード/572ストアから15/15へ減らします。コピー転送前、その行は144移動/165重み付き移動で、91項目全体は943/1,594でした。隣接コピー転送は、それぞれ35/42と821/1,444へ減らします。RISC-V の Hello World 実行は、ヒープとスタックのアクセス数を変えずに、21,920ではなく21,787命令を実行します。これらはエミュレータの命令数で、ハードウェアの時間測定ではありません。現在のネイティブ BrainFuck テストと、すべての実行時アサーションは有効です。 + +その後 GCM の反依存関係の支配ノード走査を復元したことで、RISC-V BrainFuck の各行は35/42から48/146へ変わり、上の現在の表になりました。条件付き書き手は、後の Load 配置を支配するとは限らないため、書き手の正確なブロックだけを確認すると、必要な順序付けを見逃します。復元した走査は、条件付きストア前の読み取りプログラムが120でなく140を返していた問題を修正します。両方の BrainFuck 実行確認と、91項目全体のスピル監査が成功しています。移動数の増加は、割り当て器のヒューリスティックを変えずに記録しています。 + +次の併合比較は遅延メモリより前のものです。両側はその監査の同じコンパイラを使います。現在のグラフに対する新たな機能無効化比較ではありません。 + +| 以前の制御された比較、同じ91コンパイル | 分割 | ループ重み付き分割 | +|---|---:|---:| +| 併合を無効にした監査コンパイラ | 1,070 | 1,889 | +| 併合を有効にした監査コンパイラ | 840 | 1,456 | + +制御実行は `graphColor` 内の `Coalesce.coalesce` だけを無効にします。両実行で、割り当て、レジスタ制約、実行時確認が成功します。無効化した実行は、スピル期待値の9件の変化を報告し、意図どおり失敗終了します。併合は230移動(21.5%)と433重み付き移動(22.9%)を減らします。局所的な増加は起きる場合があり、1例ではなく集計がトレードオフを判断します。 + +最終監査は x86/RISC-V の狭いストアマスクをバックポートし、ARM のレジスタバンク選択と浮動小数点メモリ演算を修正しました。バイトストアを合法なレジスタに制限すると、RISC-V BrainFuck は2つの群それぞれで211から42重み付き移動となり、以前の表から44の単純移動/338の重み付き移動が減ります。これらの正しさの修正は比較の両側に含まれます。より広いマスクで行った以前の測定は過去の記録で、現在の基準値ではありません。第20章の357重み付き移動も併合の基準値ではありません。第21章は機械命令への変換を変更し、ネイティブ ABI の責務を実装しているためです。 diff --git a/chapter21/README.md b/chapter21/README.md index 5e7baaa1..57705fa7 100644 --- a/chapter21/README.md +++ b/chapter21/README.md @@ -1,5 +1,7 @@ # Chapter 21: Instruction Encoding and ELF +English | [日本語](README.ja.md) + [Previous: Chapter 20](../chapter20/README.md) | [Next: Chapter 22](../chapter22/README.md) diff --git a/chapter21/docs/18-grammar.ja.md b/chapter21/docs/18-grammar.ja.md new file mode 100644 index 00000000..dbd7f0fd --- /dev/null +++ b/chapter21/docs/18-grammar.ja.md @@ -0,0 +1,138 @@ +# 第21章の文法 + +[English](18-grammar.md) | 日本語 + +この文書は [原文](18-grammar.md) の日本語訳です。 + +> 訳注: 原文の見出しは「Grammar for Chapter 16」です。この日本語版では、配置されている第21章に合わせた見出しにしています。 + +[第21章に戻る](../README.ja.md) + +```antlrv4 +grammar SimpleLanguage; + +program : block EOF ; + +block : statement+ ; + + +statement + : '{' block '}' + | returnStatement + | ifStatement + | whileStatement + | breakStatement + | continueStatement + | structDeclaration + | expressionStatement + ; + + +returnStatement : 'return' expression ';' ; + +ifStatement : 'if' '(' expression ')' statement ('else' statement)? ; + +whileStatement : 'while' '(' expression ')' statement ; + +breakStatement : 'break' ';' ; + +continueStatement : 'continue' ';' ; + + +structDeclaration : 'struct' IDENTIFIER '{' block '}' ; + +expressionStatement + : type IDENTIFIER ';' + | type IDENTIFIER '=' expression ';' + | IDENTIFIER '=' expression ';' + | expression + ; + +PRIMTYPE + : 'int' + | 'i8' + | 'i16' + | 'i32' + | 'i64' + | 'u8' + | 'u16' + | 'u32' + | 'flt' + | 'f32' + | 'f64' + | 'bool' + ; + +type + : PRIMTYPE + | typeName '?'? + | typeName nestedArray+ + ; + +nestedArray : '?'? '[]'* ; + +typeName : IDENTIFIER ; + + +expression : bitWiseExpression ; + +bitWiseExpression + : comparisonExpression ( '&' | '|' | '^' ) comparisonExpression)* + ; + +comparisonExpression + : shiftExpression (('==' | '!='| '>'| '<'| '>='| '<=') shiftExpression)* + ; + +shiftExpression + : additiveExpression ( '<<' | '>>>' | '>>' ) additiveExpression)* + ; + +additiveExpression + : multiplicativeExpression (('+' | '-') multiplicativeExpression)* + ; + +multiplicativeExpression + : unaryExpression (('*' | '/') unaryExpression)* + ; + +unaryExpression + : ('-') unaryExpression + | '!' unaryExpression + | primaryExpression postAssign + ; + +primaryExpression + : INTEGER_LITERAL + | FLOAT_LITERAL + | '(' expression ')' + | 'true' + | 'false' + | 'null' + | newExpression + | IDENTIFIER + ; + +newExpression + : 'new' IDENTIFIER [ '{' block '}' ] + | 'new' IDENTIFIER '[' expression ']' + ; + +postAssign : postFix [ '=' expression ] | [ '#' ]; + +postFix + : '.' IDENTIFIER postFix + | '[' expression ']' postFix + ; + + +IDENTIFIER : NON_DIGIT (NON_DIGIT | DIGIT)* ; + +INTEGER_LITERAL + : [1-9]DIGIT* + | [0] + ; + +NON_DIGIT: [a-zA-Z_]; +DIGIT: [0-9]; +``` diff --git a/chapter21/docs/18-grammar.md b/chapter21/docs/18-grammar.md index 7da725e9..51539d0b 100644 --- a/chapter21/docs/18-grammar.md +++ b/chapter21/docs/18-grammar.md @@ -1,5 +1,7 @@ # Grammar for Chapter 16 +English | [日本語](18-grammar.ja.md) + ```antlrv4 grammar SimpleLanguage; diff --git a/chapter21/docs/encoding-reference.ja.md b/chapter21/docs/encoding-reference.ja.md new file mode 100644 index 00000000..8ccdc728 --- /dev/null +++ b/chapter21/docs/encoding-reference.ja.md @@ -0,0 +1,777 @@ +# 命令エンコーディングの参考資料 + +[English](encoding-reference.md) | 日本語 + +この文書は [Instruction encoding reference](encoding-reference.md) の日本語訳です。 +元の第21章の README から、詳細な説明を残したものです。 +コンパイラのパイプラインと実験については[章の概要](../README.ja.md)を参照してください。 + +# 目次 + +1. [命令エンコーディング](#命令エンコーディング) +2. [基盤](#基盤) +3. [エンディアン](#エンディアン) +4. [RISCV](#riscv) + - [命令形式](#命令形式) + - [浮動小数点数](#浮動小数点数) + - [間接参照](#間接参照memop) + - [分岐](#分岐) + - [即値](#即値) + - [関数内の定数](#関数内の定数) +5. [ARM](#arm) + - [レジスタ形式](#レジスタ形式) + - [即値シフト](#即値シフト) + - [論理演算の即値](#論理演算の即値) + - [浮動小数点数](#浮動小数点数-1) + - [大きな定数](#大きな定数) + - [関数定数](#関数定数) + - [条件フラグ](#条件フラグ) + - [間接参照](#間接参照memop-1) + - [分岐](#分岐-1) +6. [AMD64 X86-64](#amd64-x86-64) + - [REX プレフィックス](#rex-プレフィックス) + - [MODR/M](#modrm) + - [SIB](#sib) + - [変位](#変位) + - [即値](#即値-1) + - [浮動小数点数](#浮動小数点数-2) + - [大きな定数](#大きな定数-1) + - [関数定数](#関数定数-1) + - [条件フラグ](#条件フラグ-1) +7. [再配置](#再配置) + +> 訳注:原文の目次には本文に対応する節がない項目や、別の節を指すリンクがあります。 +> 日本語版の目次は本文の見出しに合わせています。本文の技術的な説明とコード例は原文に沿っています。 + +この章では命令エンコーディングを追加します。命令エンコーディングとは、 +プロセッサの命令セットアーキテクチャ(ISA)が定義する特定のバイナリ形式で機械命令を表すことです。 + +命令選択が完了したら、ELF オブジェクトファイルに書き出す前に、命令をエンコードします。 + +曖昧さを避けるため、命令のエンコードに使う情報の出典を明示します。 +現在、次のアーキテクチャをサポートしています。 + +### RISC-V: + +*riscv* では現在、[RVA23U64](https://msyksphinz-self.github.io/riscv-isadoc/html/) を対象にしています。 + +### ARMV8/AArch64: + +*arm*(aarch64)では、この資料にまとめられた[エンコーディング規則](https://docsmirror.github.io/A64/20a23-06/index.html)を使います。 +*注*:サポートするのは64ビット ARM のエンコーディングだけです。 + +### AMD64: + +*x86-64(amd64)* では、最新の Intel マニュアルの[エンコーディング規則](https://www.felixcloutier.com/x86/)を使います。 + +-------------------------------- + +## 命令エンコーディング + +## 基盤 + +命令選択フェーズは、理想ノードから機械向けのノードを作ります。 +これらの機械向けノードには *encoding* 関数があり、CodeGen 内の Encoding ドライバから呼び出します。 + +```java +for( CFGNode bb : _code._cfg ) + for( Node n : bb._outputs ) + if( n instanceof MachNode mach ) { + int off = _bits.size(); + mach.encoding(this); + _opLen[n._nid] = (byte)(_bits.size()-off); + } +``` + +選んだターゲットに応じて、リトルエンディアンまたはビッグエンディアンのエンコーディングに注意する必要があります。 +これは `add4` が扱い、正しい順序でビットストリームに追加します。 +現時点のビットストリームは、単純な ByteArrayOutputStream です。 + +```java +@Override public void encoding( Encoding enc ) { + enc.add4(0); // adds 4 bytes to the bitstream +} +``` + +資料がビッグエンディアンでエンコーディングを示す *RISC-V* では、 +この関数がリトルエンディアンへの変換も行います。ほかのアーキテクチャでは変換は不要です。 + +## エンディアン + +実際に使う ARM、RISC-V、x86 はすべてリトルエンディアンで、最下位バイトを先に格納します。 +したがって、命令エンコーディングがリトルエンディアン形式で生成されるようにします。 + +```java +// Little endian write of a 32b opcode +public void add4( int op ) { + _bits.write(op ); + _bits.write(op>> 8); + _bits.write(op>>16); + _bits.write(op>>24); +} +``` + +たとえば、固定長命令の32ビットのエンコーディング *10110101100111101011001001101101* を考えます。 + +最下位バイトを最初に追加し、次のバイトを追加していきます。 +``` +_bits.write(op ); +``` + +`_bits.write()` が追加するのは最下位バイトだけです。 +値を右にシフトすると、次のバイトを最下位の位置へ移動でき、各バイトを順番に書き込めます。 + +-------------------------------- + +## RISCV + +RISCV の命令はすべて32ビット幅で、常にリトルエンディアンです。 +RISCV は RISC アーキテクチャなので「レジスタ間」の形式を優先し、エンコーディングは非常に規則的です。 +最も単純なエンコーディングであるため、RISCV から説明します。 + +#### 命令形式 + +##### R-TYPE + +このエンコーディング形式は、`AddRISC`、`MulRISC` などのレジスタ間の演算に使います。 +```java +public static int r_type(int opcode, int rd, int func3, int rs1, int rs2, int func7) { + return (func7 << 25) | (rs2 << 20) | (rs1 << 15) | (func3 << 12) | (rd << 7) | opcode; +} +``` + +##### I-TYPE + +この配置は、`AddIRISC` などの即値形式に使います。 + +```java + public static int i_type(int opcode, int rd, int func3, int rs1, int imm12) { + assert opcode >= 0 && rd >=0 && func3 >=0 && rs1 >=0 && imm12 >= 0; // Zero-extend by caller + return (imm12 << 20) | (rs1 << 15) | (func3 << 12) | (rd << 7) | opcode; + } +``` + +##### S-TYPE + +このエンコーディング形式は `StoreRISC` に使います。 + +```java +public static int s_type(int opcode, int func3, int rs1, int rs2, int imm12) { + assert imm12 >= 0; // Masked to high zero bits by caller + int imm_lo = imm12 & 0x1F; + int imm_hi = imm12 >> 5; + return (imm_hi << 25) | (rs2 << 20) | (rs1 << 15) | (func3 << 12) | (imm_lo << 7) | opcode; +} +``` + +##### B-TYPE + +このエンコーディングの配置は `BranchRISC` が使います。 +```java + // immf = first imm + // immd = second imm + // BRANCH +public static int b_type(int opcode, int immf, int func3, int rs1, int rs2, int immd) { + return (immd << 25 ) | (rs2 << 20) | (rs1 << 15) | (func3 << 12) | (immf << 7) | opcode; + } +``` + +##### U-TYPE + +この配置は `AUIPC` と `LUI` に使います(上位即値は20ビットです)。 + +```java + public static int u_type(int opcode, int rd, int imm20) { + return (imm20 << 12) | (rd << 7) | opcode; + } +``` + +##### J-TYPE + +無条件ジャンプ(`UJmpRISC`)に使います。 +```java + public static int j_type(int opcode, int rd, int imm20) { + return imm20 << 12 | rd << 7 | opcode; + } +``` + +#### 浮動小数点数 + +浮動小数点定数のロードには、間接メモリロード(PC 相対)を使います。 +``` +AUIPC dst,#hi20_constant_pool +Load dst,[dst+#low12_constant_pool] +``` + +#### 間接参照(MemOp) + +[ロード](https://msyksphinz-self.github.io/riscv-isadoc/html/rvi.html#lw):`lw rd,offset(rs1)` + +[ストア](https://msyksphinz-self.github.io/riscv-isadoc/html/rvi.html#sw):`sw rs2,offset(rs1)` + +#### 分岐 + +RISC-V は、分岐命令が比較と分岐先を1つの命令に含むという点で特徴的です。 + +#### 即値 + +*ALU* 演算では、即値が符号付き12ビットに収まる場合だけ、即値形式をエンコードします。 +それ以外では Load Upper Immediate(上位20ビット)を使うか、両方を使います。 + +``` +addiw a5,a5,123 +``` + +これに対して、追加のロードを使う場合は次のようになります。 +``` +addw a5,a5,a4 +``` + +### 浮動小数点定数 + +- **定数プール**に格納します。 +- **PC 相対ロード**でアクセスします。 + +### 整数定数 + +- **12ビット定数**:`ADDI` を使います。 +- **20ビット定数**:`LUI` を使います。 +- **32ビット定数**:`LUI` と `ADDI` を組み合わせます。 + +#### 関数内の定数 + +RISC-V のコードで関数内の定数をロードするときは、現在の PC をレジスタに格納し、 +それを基底として定数へアクセスします。ただし、加算のような単純な算術演算のほうが +メモリアクセスより速く効率的なので、通常はロードを避けるようにします。 + +```java +// auipc t0,0 +// addi t1,t0 + #0 +``` + +-------------------------------- + +## ARM + +Arm の命令はすべて32ビット幅で、常にリトルエンディアンです。 +Arm は RISC アーキテクチャなので、「レジスタ間」の形式を優先します。 + +#### レジスタ形式 + +レジスタ形式は、[シフト付きレジスタ](https://docsmirror.github.io/A64/2023-06/orr_log_shift.html)としてエンコードします。 +``` +orr reg1, reg2 +``` +``` +x0 = x0 | (x1 << 0) +``` + +ほかのレジスタ形式にも同じことが当てはまります。 +この共通性によって、レジスタ形式のエンコーディングを共通関数にまとめられます。 + +```java +short self = enc.reg(n); +short reg1 = enc.reg(n.in(1)); +short reg2 = enc.reg(n.in(2)); +int body = r_reg(opcode, 0, reg2, 0, reg1, self); +enc.add4(body); +``` + +レジスタ形式では *imm6* と *shift* は関係しないため、両方を0に設定します。 + +### 即値シフト + +*asri、lsli、lsri* を指します。シフト量が0以上63以下なら、即値シフトとしてエンコードします。 + +#### ASR(即値) + +即値の算術右シフトでは、*immr* をエンコードしたい即値に、imms を `63` = `111111` に設定します。 +これは [asri のエンコーディング](https://docsmirror.github.io/A64/2023-06/asr_sbfm.html)の規則に従っています。 + +`sf` ビットはオペコードに含まれ、`N` は `imm_shift` 関数の実行後、暗黙にビットストリームへ追加されます。 +```java +enc.add4(arm.imm_shift(0b100100110,_imm, 0b111111, rn, rd)); +``` + +#### LSL(即値) + +```java +// UBFM <Xd>, <Xn>, #(-<shift> MOD 64), #(63-<shift>) +// immr must be (-<shift> MOD 64) = 64 - shift +enc.add4(arm.imm_shift(0b110100110, 64 - _imm, (64 - _imm) - 1, rn, rd)); +``` + +`_imm` は負にならず、0〜63の間にあるので、`64 - _imm` を `immr` の値として安全に使えます。 +この形式の即値を64から引いて、ビットを反転します。 + +*imms* については、条件 *imms + 1 = immr* が成り立たなければなりません。 +値を代入すると、次のようになります。 + +> imms + 1 = 64 - _imm; + +> imms = (64 - _imm) - 1; + +したがって、`imms` の値は `(64 - _imm) - 1` です。 + +#### LSR(即値) + +*ASR* と同じです。 + +#### 論理演算の即値 + +論理演算の即値は *andi、xori、orri* を指します。 + +```java +// Can we encode this in ARM's 12-bit LOGICAL immediate form? +// Some combination of shifted bit-masks. +private static int imm12Logical(TypeInteger ti) { + if( !ti.isConstant() ) return -1; + if( !ti.isConstant() ) return -1; + long val = ti.value(); + if (val == 0 || val == -1) return -1; // Special cases are not allowed + int immr = 0; + // Rotate until we have 0[...]1 + while (val < 0 || (val & 1)==0) { + val = (val >>> 63) | (val << 1); + immr++; + } + int size = 32; + long pattern = val; + // Is upper half of pattern the same as the lower? + while ((pattern & ((1L<<size)-1)) == (pattern >> size)) { + // Then only take one half + pattern >>= size; + size >>= 1; + } + size <<= 1; + int imms = Long.bitCount(pattern); + // Pattern should now be zeros followed by ones 0000011111 + if (pattern != (1L<<imms)-1) return -1; + imms--; + if (size == 64) return 0x1000 | immr << 6 | imms; + return (32-size)<<1 | immr << 6 | imms; +} +``` + +imm12Logical は、エンコードできる場合は論理演算の12ビットの即値エンコーディングを返し、 +できない場合は -1 を返します。 +```java +int imm12; +return and.in(2) instanceof ConstantNode off && +off._con instanceof TypeInteger ti && (imm12 = imm12Logical(ti)) != -1 +? new AndIARM(and, imm12) +``` +```java +arm.imm_inst(enc,this,0b100100100,_imm); +``` + +#### 浮動小数点数 + +x86 と同じように、*D* レジスタに対応する汎用レジスタ(GPR)のペアを得るために、オフセットを引く必要があります。 +``` +short dst = (short)(enc.reg(this ) - arm.D_OFFSET); +``` + +浮動小数点定数をロードするには、`ldr(literal)` 命令を使います。 +オフセットを PC レジスタに加算して、リテラルのアドレスを求めます。 + +```java +// Any number that can be expressed as +/-n * 2-r,where n and r are integers, 16 <= n <= 31, 0 <= r <= 7. +int body = arm.load_pc(0b01011100, 0, dst); +``` + +#### 大きな定数 + +##### 整数:mov(movz、movk、movn)をさまざまに組み合わせて、PC 相対ロードを避けられます。 + +##### 浮動小数点数:PC 相対の *LDR(immediate, SIMD&FP)* 命令を使います。 + +#### 関数定数 + +関数定数をロードするには、現在の PC を x0 に格納し、それを使って定数へアクセスします。 +一般に、加算のほうが速いためロードを避けるようにします。 +``` +adrp x0, 0 +add x0, x0, 0 +``` + +#### 条件フラグ + +arm では比較後に条件フラグを設定します。これは x86 と同じように動作します。 + +``` +int square(int a) { +// subs +// cset w8, eq // eq = none +bool da = a == 1; + // bne + if(da == 1) { + return 1; + } + return 2; +} +``` + +arm の比較は `subss` で行い、2つのオペランドの減算とフラグの設定を同時に行います。 +`CSET` はフラグを調べ、その結果をレジスタに保存します。 + +後で `b.ne` が条件付き分岐を実行しようとするときにも、このフラグに依存します。 + +#### 間接参照(MemOp) + +Arm は次の間接参照形式をサポートします。 + +[ロード](https://docsmirror.github.io/A64/2023-06/ldr_reg_gen.html):`ldr dst, [base + index]`。base と index はどちらもレジスタです。 + +[ロード](https://docsmirror.github.io/A64/2023-06/ldr_imm_gen.html):`ldr dst, [base + #offset]`。base はレジスタ、offset は即値です。 + +[ストア](https://docsmirror.github.io/A64/2023-06/str_reg_gen.html):`str dst, [base + index]` + +[ストア](https://docsmirror.github.io/A64/2023-06/strb_imm.html):`str dst, [base + #offset]` + +オフセットが定数でなければ……(訳注:原文のこの文は途中で途切れています)。 + +#### 分岐 + +`B.cond` は、PC 相対オフセットのラベルへ[条件付き](https://developer.arm.com/documentation/dui0802/b/A32-and-T32-Instructions/Condition-codes?lang=en)で分岐します。 + +> cond は標準的な条件のいずれかです。 + +-------------------------------- + +## AMD64 X86-64 + +命令幅が固定の riscv アーキテクチャとは異なり、*x86-64* の命令は可変長です。 +これは *CISC(Complex Instruction Set Computer)* アーキテクチャで一般的であり、 +命令幅は1〜15バイトまで変わります。AMD64 は多くの間接アドレッシングモードをサポートするため、 +*CISC* の一般的な目標は、できるだけ少ないアセンブリの行数で処理を完了することです。 + +### REX プレフィックス + +64ビット版の x86 を対象にしているため、このプレフィックスを扱う必要があります。 +一方、32ビットモードでは通常は不要です。 + +一般に、次の場合は *REX* プレフィックスをエンコードしなければなりません。 + +- 拡張レジスタ(R8〜R15、XMM8〜XMM15、YMM8〜YMM15、CR8〜CR15、DR8〜DR15)を使う。 +- オペランドサイズが64ビットで、命令の既定のオペランドサイズが64ビットではない(大部分の ALU 演算)。 +- RSI、RDI、RBP で8ビットのオペランドを使う。 + +次の場合は *REX* プレフィックスをエンコードしてはなりません。 + +- 上位バイトのレジスタ AH、CH、BH、DH を使う(現在の Simple では使っていません)。 + +*注*:SSE 命令のエンコーディングでは、*REX* プレフィックス(0x40)を `SSE` プレフィックスの後に置く必要があります。 +それ以外の場合は無視されます。 + +Simple では、REX プレフィックスをビットストリームの先頭に追加するだけです +(SEE の浮動小数点命令を除く。訳注:「SEE」は原文の表記です)。配置は次のとおりです。 + +| **フィールド** | **長さ** | **説明** | +|:---------:|:----------:|---------------------------------------------------------------------------------| +| `0100` | 4ビット | 固定のビットパターン | +| `W` | 1ビット | 1なら64ビット、それ以外なら32ビットのオペランドサイズを使う | +| `R` | 1ビット | **MODRM.reg** フィールドの拡張 | +| `X` | 1ビット | **SIB.index** フィールドの拡張 | +| `B` | 1ビット | **MODRM.rm** または **SIB.base** フィールドの拡張 | + +```java + public static int REX_W = 0x48; +``` + +```java +public static int rex(int reg, int ptr, int idx, boolean wide) { + // assuming 64 bit by default so: 0100 1000 + int rex = wide ? REX_W : REX; + if( 8 <= reg && reg <= 15 ) rex |= 0b00000100; // REX.R + if( 8 <= ptr && ptr <= 15 ) rex |= 0b00000001; // REX.B + if( 8 <= idx && idx <= 15 ) rex |= 0b00000010; // REX.X + return rex; +} + + ... + enc.add1(x86_64_v2.rex(dst, src, 0)); +``` + +`W` ビットを設定すると、`0b01001000 = 0x48;` になります。 + +#### オペコード + +この場合、オペコードは実行する操作を指定する1バイトのフィールドです。 +``` +enc.add1(opcode()); // opcode +``` + +#### MODR/M + +| **フィールド** | **長さ** | **説明** | +|:-------------:|:----------:|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **MODRM.mod** | 2ビット | 一般に、このフィールドが `b11` ならレジスタ直接アドレッシングモード、それ以外ならレジスタ間接アドレッシングモードを使う | +| **MODRM.reg** | 3ビット | 2種類の値を取れる。<br><br>• 一部の命令が使う3ビットのオペコード拡張。ほかの命令と区別する以上の意味は持たない。<br><br>• 命令によってソースまたは宛先として使う3ビットのレジスタ参照。参照するレジスタは、命令のオペランドサイズと命令自体に依存する。`REX.R`、`VEX.~R`、`XOP.~R` フィールドによって最上位ビットを1つ追加し、合計4ビットに拡張できる | +| **MODRM.rm** | 3ビット | 直接または間接のレジスタオペランドを指定し、任意で変位を伴う。`REX.B`、`VEX.~B`、`XOP.~B` フィールドによって最上位ビットを1つ追加し、合計4ビットに拡張できる | + +MODR/M バイトも同じように扱います。 + +modrm バイトに最初に必要なのは、2ビットの mod です。 + +##### mod + +mod の配置は次のとおりです。 +``` +public enum MOD { + INDIRECT, // [mem] + INDIRECT_disp8, // [mem + 0x12] + INDIRECT_disp32,// [mem + 0x12345678] + DIRECT, // mem +}; +``` + +##### reg + +reg フィールドは3ビットのフィールドで、15種類のレジスタをエンコードできます。 +(訳注:ビット数とレジスタ数について、ここでは原文の記述を保持しています。) + +##### r/m + +rm フィールドは直接または間接のレジスタオペランドを指定し、15種類のレジスタをエンコードできます。 + +```java +public static int modrm(MOD mod, int reg, int m_r) { + // combine all the bits + return (mod.ordinal() << 6) | ((reg & 0x07) << 3) | m_r & 0x07; +} +``` + +`modrm` 関数を使って `ModR/M` バイトを構築します。通常、これはオペコードの後に続きます。 + +``` +enc.add1(x86_64_v2.modrm(x86_64_v2.MOD.DIRECT, dst, src)); +``` + +レジスタ形式のエンコーディングは、次のようになります。 +``` +short dst = enc.reg(this ); // src1 +short src = enc.reg(in(2)); // src2 +enc.add1(x86_64_v2.rex(dst, src, 0)); +enc.add1(opcode()); // opcode +enc.add1(x86_64_v2.modrm(x86_64_v2.MOD.DIRECT, dst, src)); +``` + +#### SIB + +SIB バイトには次のフィールドがあります。 + +| **フィールド** | **長さ** | **説明** | +|:-------------:|:----------:|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **SIB.scale** | 2ビット | `SIB.index` のスケーリング係数を示す。表で使う **s** は 2^SIB.scale^ に等しい | +| **SIB.index** | 3ビット | 使用するインデックスレジスタ。各レジスタの値は Registers を参照。`REX.X`、`VEX.~X`、`XOP.~X` フィールドによって最上位ビットを1つ追加し、合計4ビットに拡張できる | +| **SIB.base** | 3ビット | 使用するベースレジスタ。各レジスタの値は Registers を参照。`REX.B`、`VEX.~B`、`XOP.~B` フィールドによって最上位ビットを1つ追加し、合計4ビットに拡張できる | + +MODR/M バイトと同じ配置です。 +```java +public static int sib(int scale, int index, int base) { + return (scale << 6) | ((index & 0x07) << 3) | base & 0x07; +} +``` + +```java +enc.add1(x86_64_v2.sib(_scale, idx, x86_64_v2.RBP)); +``` + +#### 変位 + +変位には2つの形式があります。 +```java +public enum MOD { +INDIRECT, // [mem] +INDIRECT_disp8, // [mem + 0x12] +INDIRECT_disp32,// [mem + 0x12345678] +DIRECT, // mem +}; +``` + +```java +if( mod == MOD.INDIRECT_disp8 ) { + enc.add1(offset); +} else if( mod == MOD.INDIRECT_disp32 ) { + enc.add4(offset); +} +``` + +変位は即値の前に置きます。たとえば、次のようになります。 +``` +sub [eax + 2], 4 +``` +``` +bytes.write(0x83); +bytes.write(modrm(INDIRECT8, 5, 0 /* eax */)); +bytes.write(2); // displcement goes first +bytes.write(4); +``` + +#### 即値 + +即値のバイト列は `ModR/M` バイトの後に置きます。必要に応じて4バイトの即値をエンコードできます。 +```java +// immediate(4 bytes) 32 bits or (1 byte)8 bits +if( x86_64_v2.imm8(_imm) ) enc.add1(_imm); +else enc.add4(_imm); +``` + +#### 間接参照(MemOp) + +X86-64 は複数の間接アドレッシングモードをサポートします。 + +#### 浮動小数点数 + +浮動小数点演算には、XMM レジスタまたはメモリにあるスカラーの単精度浮動小数点値を操作する +`SEE SIMD` 命令を使います(訳注:命令名の表記は原文どおりです)。 +*注*:ALU 演算で XMM レジスタを使う場合、エンコーディングは非常に似ていますが、 +レジスタ番号から `XMM_OFFSET` を引く必要がある点が異なります。 +レジスタを連続した番号で数えているため、XMM レジスタに対応する GPR のペアを得るには、 +オフセットを引く必要があります。 + +```java +short dst = (short)(enc.reg(this ) - x86_64_v2.XMM_OFFSET); +``` + +```java +public static int XMM_OFFSET = 16; +``` + +| **X.Reg** | **8ビット GP** | **16ビット GP** | **32ビット GP** | **64ビット GP** | **80ビット x87** | **64ビット MMX** | **128ビット XMM** | **256ビット YMM** | **16ビット Segment** | **32ビット Control** | **32ビット Debug** | +|:--------:|:------------:|:-------------:|:-------------:|:-------------:|:-------------:|:-------------:|:--------------:|:--------------:|:-----------------:|:-----------------:|:---------------:| +| `0.000 (0)` | AL | AX | EAX | RAX | ST0 | MMX0 | XMM0 | YMM0 | ES | CR0 | DR0 | + +`XMM0` が `RAX` に対応し、以降も同様であることがわかります。 + +#### 大きな定数 + +##### 整数:`MOV r64, imm64` 命令形式を使うだけです。 + +##### 浮動小数点数:再配置を使い、メモリ内の定数を使います。 + +#### 関数定数 + +関数定数をロードするには lea を使います。これは、命令ポインタからの相対アドレスを指定したレジスタにロードします。 +`lea rax, [rip+disp32]` + +#### 条件フラグ + +FLAGS レジスタは、x86 CPU の現在の状態を保持するステータスレジスタです。 + +条件付きコードのためにエンコードする必要のあるすべての命令を示す、単純な例を考えます。 + +``` +int square(int a) { + // cmp DWORD PTR [rbp-0x8],0x1 + // `sete al` + bool da = a == 1; + + // cmp eax,0x1 + if(da == 1) { + return 1; + } + return 2; +} +``` + +まず、`a` と1を比較する比較命令が必要です。 +``` +cmp DWORD PTR [rbp-0x8],0x1 +``` + +この比較命令が条件フラグを設定します。 + +> 結果に従って CF、OF、SF、ZF、AF、PF フラグを設定します。 + +続いて `sete` 命令を使い、比較の結果をレジスタへ保存します。 + +`sete al` + +if 条件のために、最初の真偽値の結果に基づく追加の比較を行います。 +``` + cmp eax,0x1 +``` + +そして、比較(`a == 1`)が設定したフラグに依存する条件付き分岐を行います。 +``` +jne 1159 <square(int)+0x29> +``` + +-------------------------------- + +## 再配置 + +再配置によって、コードを新しいコードオフセットへ*移動*できます。 +コードは、ほかのコードやデータを参照することがよくあります。 +`call` 命令はサブルーチンを、`branches` はほかの命令を対象とし、 +大きな定数は多くの場合*定数プール*からロードします。 + +### 局所的な再配置 + +局所的な再配置は、局所的または自己参照的なコードを修正します。最も一般的なのは分岐先です。 +`BAOS`(Byte Array Output Stream)に命令を書き込む際、後に続く命令へ向かう前方分岐のオフセットは未知です。 +そのオフセットは、分岐と分岐先の間にあるエンコーディングのサイズに依存します。 +`riscv` と `arm` のターゲットでは簡単に見えます(命令は4バイトなので数えればよい)……。 +しかし、複数の命令を使うエンコーディングもあるため、実際には4バイトとは限りません。 +X86 では命令長が擬似的に不規則に見えるほど多様なので、オペコードの長さを求めること自体が、 +オペコードを書き出すのと同じくらい難しくなります。 + +X86 には、短形式と長形式の分岐という追加の複雑さもあります。 +分岐先が符号付きバイトの範囲(+/-127)にあるなら2バイトの分岐エンコーディングを、 +それ以外では6バイトのエンコーディングを使います。 +Simple には、分岐を縮め、できるだけ2バイト形式を使うパスがあります。 +ただし、このパスはコードをずらす必要があり、その結果、ほかの分岐オフセットもすべて変わります。 +この「圧縮」パスは実際には、すべての分岐が2バイト形式を使えると楽観的に仮定して始め、 +必要に応じて分岐を拡張します(したがって、実際にはコードを*拡張*します)。 + +コードの「形」が最終的に決まったら、局所的な再配置パスを実行して自己参照をすべて更新します。 +基本的には、バイト配列内の分岐オフセットを実際の値で上書きします。 + +このパスの終了時点で、コードはあるオフセット(通常は0)で局所的に正しくなります。 +しかし、これで終わりではありません。別のコンパイルで生成されたコードと*リンク*することが多く、 +その際にもさまざまなオフセットを調整する必要があります。 + +### 大域的な再配置 + +あるコンパイル単位のコードから、別のコンパイル単位のコードを呼びたい場合、*大域的な再配置*が必要です。 +よくある例は、*ローダー*が *ELF* ファイルをロードすることです。 +通常は新しいプログラムの実行ごとに起こり、再配置情報をディスク(たとえば ELF ファイル内)に保存しておく必要があります。 +JIT システムでも、異なるコンパイルをまたいで同じことが必要ですが、再配置情報はメモリに保持できます。 + +#### 再配置の整合性を保つ + +再配置をディスクに書き込めるので、ディスク形式、ローダー、「コードの形」がすべて一致しなければなりません。 +あるコンパイル単位から別の単位を呼ぶ場合、たとえばプログラムが `libc` の `malloc` を呼ぶ場合を考えます。 +呼び出し元は Simple の汎用 `CallNode` の機械固有版を使い、呼び出し先は C でコンパイルされたコードです。 +ロード中に、ローダーが `call` と `libc` の配置を決めます。 +2つの異なるコードブロックを何らかの順序で(通常は多くのほかのブロックとともに)メモリに書き込みます。 +そこで、`call` の呼び出し先を、`libc` のコードブロック内の `malloc` に修正する必要があります。 + +X86 の外部呼び出しは、1バイトのオペコードと4バイトの PC 相対宛先アドレスからなる5バイト命令です。 +必要な修正値は、X86 固有の `call` 命令の*末尾*から `malloc` の先頭までの差であり、 +`call` の最後の4バイトに直接書き込みます。ELF ファイル内のエンコーディングの種類は、これを記述しなければなりません。 + +RISCV の外部呼び出しは、ビットを分担する2つの4バイト命令でエンコードします。 +`LUI` が宛先の上位20ビットをロードし、`JALR` が下位12ビットを加算します(32ビットの絶対宛先範囲)。 +`LUI` を `AUIPC` に置き換えると、32ビットの PC 相対範囲を得られます。 +修正では、一方の命令の20ビットと、もう一方の命令の12ビットを更新します。 +ここでも ELF ファイルのエンコーディングの種類が、この方法を記述する必要があります。 +X86 式の修正をここに使うと、誤ったビットを壊し、プログラムが動かなくなります。 + +#### 大きな定数 + +大きな定数とは、レジスタへ簡単に直接ロードできず、代わりにメモリからロードする定数です。 +セキュリティ上の理由で、*実行*権限ではなく*読み取り専用*権限などを付けた別のアドレス空間に置くことがよくあります。 +そのため、コードに対する最終的な相対アドレスは、配置によって変わります。 +これは、コンパイル単位をまたぐ呼び出しと同じです。 + +大きな定数は、コンパイル単位間で共有することもできます(HPC コードで `pi` を繰り返し使う場合など)。 +共有すると、キャッシュの占有量を減らせます。 +いずれにせよ、コードと定数の配置が決まったら、正しいオフセットからロードするようにコードを修正する必要があります。 +上の `call` の例のように、ロード命令の種類と修正方法の種類が一致しなければなりません。 +X86 では、その種類によって4バイトまたは8バイトのアドレスを使います。 +RISCV では、おそらく `LUI` と `Load` の何らかの組み合わせで、即値を20/12ビットに分けます。 +また、RISCV では同じ `LUI` で定数プールの上位20ビットを「共有」し、 +同じプールの異なる定数には異なるロードを使いたいでしょう。ARM には、さらに別の方法があります。 diff --git a/chapter21/docs/encoding-reference.md b/chapter21/docs/encoding-reference.md index e273caec..5853b380 100644 --- a/chapter21/docs/encoding-reference.md +++ b/chapter21/docs/encoding-reference.md @@ -1,5 +1,7 @@ # Instruction encoding reference +English | [日本語](encoding-reference.ja.md) + Detailed notes retained from the original Chapter 21 README. See the [chapter overview](../README.md) for the compiler pipeline and experiments. diff --git a/chapter22/README.ja.md b/chapter22/README.ja.md new file mode 100644 index 00000000..ad2e6315 --- /dev/null +++ b/chapter22/README.ja.md @@ -0,0 +1,161 @@ +# 第22章: Hello, World! + +[English](README.md) | 日本語 + +この文書は [原文](README.md) の日本語訳です。 + +[前の章: 第21章](../chapter21/README.ja.md) | +[次の章: 第23章](../chapter23/README.ja.md) + +そろそろ文字列と表示を扱う頃です。この章では基本的な文字列を作るために何が必要かを見ます。本格的な String クラスは、別の章まで待ちます。 + +```java +sys.io.p("Hello, World!"); +``` + +できました。1行の `Hello, World` です。この例は `docs/examples/A_helloWorld.smp` にあります。 + +分解して見てみましょう。 + +- `sys`: 単なる変数名です。ほかの変数と同様、現在のスコープで検索します。そのスコープは完全に空に見えますが、今ではすべての Simple プログラムがスコープ内に `sys` 変数を持って始まります。`sys` 自体は普通の `struct` で、`src/main/smp/sys.smp` に定義されています。 +- `sys.io.p`: `sys` 構造体で `io` フィールドを検索すると `sys.io` 構造体が得られ、そこで `p`(print の略)を検索します。これによって関数を返します。 +- `p("Hello, World!")`: `p` フィールドからロードしたばかりの関数を、文字列引数 `"Hello, World!"` で呼び出します。 +- `"Hello, World!"`: `u8`(文字)の定数配列を生成する新しい構文です。それ以外は通常の範囲検査付き配列で、先頭に長さフィールド `#` があります。配列の実体データは、最終的に *定数プール*、そして ELF ファイルの RODATA セクションに入ります。 + +第11章の遅延メモリ分割はここでも続きます。パーサーは1つの `$mem` を保持し、BulkMemPhi/MemPhi が最適化中にフィールドのエイリアスを発見します。割り当ては部分的な MemMerge を消費し、`{ptr, $mem}` を生成します。Call はメモリ全体を運びます。ループ内の Load 検索はメモリマージのすべての枝をたどるので、BrainFuck の不変のプログラム長は主ループの外へ畳み込まれます。 + +[linear](https://github.com/SeaOfNodes/Simple/tree/linear) ブランチ上の直線的な Git リビジョン履歴で [この章](https://github.com/SeaOfNodes/Simple/tree/linear-chapter22) を読み、前の章と [比較](https://github.com/SeaOfNodes/Simple/compare/linear-chapter21...linear-chapter22) することもできます。 + +## 入れ子の型と静的フィールド + +Simple は入れ子の型をサポートします。これは名前空間だけの変更で、新しい意味規則はなく、型定義を入れ子にできるだけです。 + +```java +struct Outer { + struct Inner { + int in; + flt pi = 3.14; + }; + int out; +}; +``` + +コンストラクタではなく宣言内で設定した final フィールドは、自動的に「静的」フィールドになります。すべてのインスタンスで final なので、各インスタンスに保存する必要がありません。代わりに大域領域に1つだけ保持し、そこからロードします。まだ本当の *クラス* オブジェクトではありません。 + +この例では: + +- `Outer` 構造体のインスタンスは、単一の `int` フィールド `out` を持ちます。 +- `Inner` 構造体のインスタンスは、単一の `int` フィールド `in` を持ちます。 +- 大域領域には、値 `3.14` の `Outer.Inner.pi` フィールドが存在します。 + +`sys` 構造体も、少なくとも入れ子の `libc` と `io` 構造体を持つ例です。これらの構造体のフィールドはすべて final なので静的です。 + +## FFI と I/O 呼び出し + +外部/FFI 関数を呼び出せるようになりました。ほかの変数と同様に宣言しますが、`"C"` を代入します。libc の `write` 呼び出しの束縛は次のとおりです。 + +`{ i32 fd, i64 buf, u32 len -> u32 } write = "C";` + +`"C"` を代入された final 変数は、外部で定義されたとみなします。同じ `"C"` 文字列の代入との曖昧さを避けるため、構文は少し変わるかもしれません。 + +正しいシグネチャや名前空間を取得するリンカのサポートはありません。リンク時に名前 `write` が一意である必要があります。 + +## 自動インポートと「sys.」名前空間 + +`sys` 構造体には `libc` 束縛と、表示やコレクションなどの既定ライブラリコードを含みます。この章の時点では、最小限の libc 束縛と簡単な表示があります。以下の抜粋は、上で使った部分を示します。 + +```java +// top-level default import +struct sys { + // libc bindings + /** https://www.man7.org/linux/man-pages/man2/<libc call>.2.html */ + struct libc { + // fd buf len -> len + { i32 i64 u32 -> u32 } write = "C"; + // addr len prot flags fd off -> void* + { i64 i64 i32 i32 i32 i32 -> i64 } mmap = "C"; + // mmap flags + i32 PROT_EXEC = 4; + i32 PROT_READ = 1; + i32 PROT_WRITE= 2; + i32 PROT_NONE = 0; + i32 MAP_PRIVATE = 2; + i32 MAP_ANON = 32; + }; + struct io { + val p = { u8[~] str -> + i64 ptr = str; // cast array base to i64 + return sys.libc.write(1,ptr,str#); + }; + }; +}; +``` + +`sys` インポート自体は、ほかと同じ単なる `struct` です。内部にフィールド、代入、型を宣言します。これらの代入はすべて final で構造体宣言内にあるため、すべて *静的* フィールドであることに注意してください。 + +## ポインタから `i64` へのキャスト + +ポインタを i64 にキャストできるようになりました。ただし逆方向はできません。 +`i64 ptr = str; // cast array base to i64` + +配列の場合、長さフィールド `#` を飛ばした配列の *先頭* へのキャストであり、外部の「C」呼び出しへ渡すのに適しています。構造体の場合は単に構造体の先頭です。上の `sys.io.p` 関数では、長さ検査付き Simple 配列を libc の `write` 呼び出しへ渡すために使います。 + +## 定数の配列と定数配列 + +Simple の型システムは「定数配列」、すなわち固定された定数の配列をサポートします。ELF ファイルの RODATA セクションに保存します。現在、パーサーのレベルでは文字列だけをサポートします。`"Hello, World!"` は13個の ASCII バイトを含む `u8[13]` 配列を作ります。すべての配列と同様に長さがあり、どこかで範囲検査を受けます。 + +非定数配列の定数版は `u8[~]` 構文で代入できます。 + +```java +// Make a mutable array of ints +int N=4; +i32[] is = new i32[N]; +// Mutate the array +for( int i=0; i<N; i++ ) + is[i] = i*i; +// Function "sum" can read but not write the array +val sum = { i32[~] nums -> // Notice the [~] signature marking nums as immutable + int sum = 0; + for( int i=0; i<nums#; i++ ) + sum += nums[i]; + return sum; +}; +return sum(is); +``` + +これは配列変数そのものではなく、配列の深い内部の内容に影響することに注意してください。 + +## 引用された文字 + +形式 ``` `0` ``` は、ASCII 文字 `0` を持つ `u8` 値を作ります。 + +## 継続的な改良 + +より大きなプログラムを実行すると、グラフだけのテストが見逃す誤りが明らかになります。この章の初期開発では、x86 命令エンコーディングとフラグ破壊、ループ後退辺やメモリストア周辺のスケジューリング、インライン化中の古い支配ノード深さなどがありました。読者が動作するコンパイラのスナップショットを土台にできるよう、修正を適用可能な最も早い章へ移しています。ネイティブテストは出力とプロセス終了ステータスの両方を確認します。エミュレータテストは、実行成功に加えて戻り値とメモリを確認します。 + +## RegAlloc の改良: よりよい色バイアス + +第21章では保守的なコピー併合を導入しました。ここでは、関係する区間を併合できない場合のレジスタ優先度を改善します。コピーの連鎖は、末端の定義でも割り当て済みレジスタを使えます。ループの Phi は後退辺のレジスタを優先し、可能なら各反復の移動を避けます。 + +自明に彩色可能な区間がないとき、安い値はよりよいスピル候補です。唯一の使用にすでに隣接するものより、遠い使用や複数の使用を持つ複製可能な定義を優先し、次に長い範囲を安く分割できる callee-save 値を優先します。callee-save 間の同点はレジスタ順で決めます。これは小さな優先設定で、一般的なコストモデルではありません。よく使う値をレジスタクラスでまとめる処理は第23章、低頻度ループの分割は第24章、面積/コストの順位付けは第25章に残します。 + +第21章の隣接コピー転送も引き継ぎます。彩色後、次の命令に唯一の使用がある場合、オペランドマスクが許し、2アドレスの結び付きを壊さなければコピー元レジスタを読めます。以下の表は、この整理と遅延メモリの前方移植を含みます。第22章の群には、以前の監査後に追加した移動0の C 戻り値 ABI 確認2件も含むようになりました(24ではなく26項目)。 + +このディレクトリで `make spill-stats` を実行してください。以下の各行は **この章のコンパイラ**、ワークリストの既定シード123、以前の群と同じソース/対象の組み合わせを使います。Windows 実行は x86 SystemV/Win64 と RISC-V/ARM SystemV を合わせます。診断グラフとマスクの回帰テストは合計に寄与しません。 + +| プログラム群 | コンパイル数 | 残存する移動 | ループ重み付き移動 | +|---|---:|---:|---:| +| 第20章 | 39 | 324 | 443 | +| 第21章 | 52 | 432 | 964 | +| 第22章 | 26 | 67 | 67 | +| **合計** | **117** | **823** | **1,474** | + +`_spills` はレジスタ移動を含む残存 SplitNode を数え、`_spillScaled` は各々を `8^loopDepth` で重み付けします。これはコンパイラの推定で、測定された実行時メモリアクセスではありません。報告器は個別の割り当てと CPU/ABI 合計も表示します。第21章の元の `int age` の person 例は `person21` として保存し、この章の `i32 age` 版と改訂された無限ループ例は第22章の群に属します。 + +隣接コピー転送前、同じコンパイラと正しさの修正で、第21章の色優先度とスピル順序を使った制御比較は、885移動と1,543重み付き移動でした。この章は **47重み付き移動(3.0%)** を減らします。RISC-V BrainFuck は2つの群それぞれで42から28、ARM は34から28に改善します。一部の MergeSort と RISC-V Sieve は各1移動増えます。スイート全体の合計が、その局所的なトレードオフに見返りがあるかを示します。 + +後のヒューリスティックをいくつか混在させた元の第22章スナップショットは、同じ115コンパイルで854移動、1,736重み付き移動でした。修正して段階化した版は240重み付き移動(13.8%)を減らします。どちらの比較も、コンパイラの章全体どうしの比較と混同してはいけません。下位表現への変換処理も変わるためです。たとえば固定した第20章の String 入力には明示的な return がなく、以前の監査では第22章の既定戻り値処理がその処理対象を除去しました。遅延メモリを使うと、一部の対象コンパイルは呼び出し可能な本体を残し、現在の表にはそれらの移動が含まれます。 + +完全なスイートと統計実行器は成功します。通常のバックエンド比較では最適化器のシードを固定します。最適化器のワークリストをシャッフルしても本質的に同じグラフへ正規化するはずなので、それらのグラフで割り当てを繰り返しても、検証範囲はあまり増えません。最適化器の正規化やワークリスト順序の失敗を調べるときに、シードを変えてください。 + +[バックポート記録](../docs/chapter-backports.md) には、解決された String の fall-through と、返される関数の保持/再配置の失敗が、再現方法と検証履歴を含めて記録されています。 diff --git a/chapter22/README.md b/chapter22/README.md index a7eb5738..0f19f293 100644 --- a/chapter22/README.md +++ b/chapter22/README.md @@ -1,5 +1,7 @@ # Chapter 22: A Hello, World! +English | [日本語](README.ja.md) + [Previous: Chapter 21](../chapter21/README.md) | [Next: Chapter 23](../chapter23/README.md) diff --git a/chapter22/docs/18-grammar.ja.md b/chapter22/docs/18-grammar.ja.md new file mode 100644 index 00000000..29263fc2 --- /dev/null +++ b/chapter22/docs/18-grammar.ja.md @@ -0,0 +1,138 @@ +# 第22章の文法 + +[English](18-grammar.md) | 日本語 + +この文書は [原文](18-grammar.md) の日本語訳です。 + +> 訳注: 原文の見出しは「Grammar for Chapter 16」です。この日本語版では、配置されている第22章に合わせた見出しにしています。 + +[第22章に戻る](../README.ja.md) + +```antlrv4 +grammar SimpleLanguage; + +program : block EOF ; + +block : statement+ ; + + +statement + : '{' block '}' + | returnStatement + | ifStatement + | whileStatement + | breakStatement + | continueStatement + | structDeclaration + | expressionStatement + ; + + +returnStatement : 'return' expression ';' ; + +ifStatement : 'if' '(' expression ')' statement ('else' statement)? ; + +whileStatement : 'while' '(' expression ')' statement ; + +breakStatement : 'break' ';' ; + +continueStatement : 'continue' ';' ; + + +structDeclaration : 'struct' IDENTIFIER '{' block '}' ; + +expressionStatement + : type IDENTIFIER ';' + | type IDENTIFIER '=' expression ';' + | IDENTIFIER '=' expression ';' + | expression + ; + +PRIMTYPE + : 'int' + | 'i8' + | 'i16' + | 'i32' + | 'i64' + | 'u8' + | 'u16' + | 'u32' + | 'flt' + | 'f32' + | 'f64' + | 'bool' + ; + +type + : PRIMTYPE + | typeName '?'? + | typeName nestedArray+ + ; + +nestedArray : '?'? '[]'* ; + +typeName : IDENTIFIER ; + + +expression : bitWiseExpression ; + +bitWiseExpression + : comparisonExpression ( '&' | '|' | '^' ) comparisonExpression)* + ; + +comparisonExpression + : shiftExpression (('==' | '!='| '>'| '<'| '>='| '<=') shiftExpression)* + ; + +shiftExpression + : additiveExpression ( '<<' | '>>>' | '>>' ) additiveExpression)* + ; + +additiveExpression + : multiplicativeExpression (('+' | '-') multiplicativeExpression)* + ; + +multiplicativeExpression + : unaryExpression (('*' | '/') unaryExpression)* + ; + +unaryExpression + : ('-') unaryExpression + | '!' unaryExpression + | primaryExpression postAssign + ; + +primaryExpression + : INTEGER_LITERAL + | FLOAT_LITERAL + | '(' expression ')' + | 'true' + | 'false' + | 'null' + | newExpression + | IDENTIFIER + ; + +newExpression + : 'new' IDENTIFIER [ '{' block '}' ] + | 'new' IDENTIFIER '[' expression ']' + ; + +postAssign : postFix [ '=' expression ] | [ '#' ]; + +postFix + : '.' IDENTIFIER postFix + | '[' expression ']' postFix + ; + + +IDENTIFIER : NON_DIGIT (NON_DIGIT | DIGIT)* ; + +INTEGER_LITERAL + : [1-9]DIGIT* + | [0] + ; + +NON_DIGIT: [a-zA-Z_]; +DIGIT: [0-9]; +``` diff --git a/chapter22/docs/18-grammar.md b/chapter22/docs/18-grammar.md index 7da725e9..51539d0b 100644 --- a/chapter22/docs/18-grammar.md +++ b/chapter22/docs/18-grammar.md @@ -1,5 +1,7 @@ # Grammar for Chapter 16 +English | [日本語](18-grammar.ja.md) + ```antlrv4 grammar SimpleLanguage; diff --git a/chapter23/README.ja.md b/chapter23/README.ja.md new file mode 100644 index 00000000..b83b0914 --- /dev/null +++ b/chapter23/README.ja.md @@ -0,0 +1,343 @@ +# 第23章: メソッドと型の再検討 + +[English](README.md) | 日本語 + +この文書は [原文](README.md) の日本語訳です。 + +[前の章: 第22章](../chapter22/README.ja.md) | +[次の章: 第24章](../chapter24/README.ja.md) + +この章では *メソッド* を追加します。構造体内で定義し、隠れた `self` 引数を取り、構造体のフィールドへアクセスできる関数です。文字列のようなクラスの `indexOf` メソッドを示します。 + +```java +// A String-like class, with methods +struct String { + u8[~] buf; // Buffer of read-only characters + + // Return the index of the first character 'c' or -1 + val indexOf = { u8 c -> // Hidden 'self' argument + for( int i=0; i<buf#; i++ ) // Direct access to `buf` field + if( buf[i]==c ) + return i; + return -1; // 'c' not in 'self' + }; +}; +``` + +この章では型も再検討し、大きく変更します。 + +- 型は *循環* するようになります。型は間接的に自身を指せます。 +- 型は引き続き [*インターン*](https://en.wikipedia.org/wiki/Interning_(computer_science)) または [*ハッシュコンシング*](https://en.wikipedia.org/wiki/Hash_consing) されます。 +- 型オブジェクトはオブジェクトプールで管理します。 +- 新しく作った型はインターン表を検索し、見つかれば元のものを使い、新しい型をプールへ返します。 +- インターン表のヒット率は99.9%ほどです。作られる型のほとんどは、すでに作られたことがあるものです。この点は興味深い性能向上につながります。最適化コンパイラの作業の多くは型の操作なので、型の数を小さく保ち、高速な階層のキャッシュに収めることには大きな見返りがあります。 +- インターン処理の多くは、何らかの「訪問ビット」で循環を扱う必要があります。簡単な方法の1つは、各型に *一意な ID*(UID)を追加することです。小さな密な整数をビット集合で使い、同じ型を複数回訪問しないようにします。UID が小さければビット集合も小さくなり、これも高速なキャッシュ階層に収まります。 +- 結果として、型は循環し(これが目標です)、**高速** になり、実装は複雑になります。型の **使い方** は同じです。作成、MEET、読み取り専用化、表示などでは同じ Type API を使い、実装の詳細だけが変わります。 + +第11章の遅延メモリモデルは、新しい型システムでも続きます。BulkMemPhi/MemPhi が最適化中にエイリアスを発見します。各 New はインスタンスフィールドだけを覆い、クラス全体の(`_one`)フィールドは部分メモリから除外します。フィールドオフセットは配置まで記号的なままであり、Load は正確なエイリアスの内容を問い合わせながら、循環するフィールド型と深い final 情報を保持します。 + +[linear](https://github.com/SeaOfNodes/Simple/tree/linear) ブランチ上の直線的な Git リビジョン履歴で [この章](https://github.com/SeaOfNodes/Simple/tree/linear-chapter23) を読み、前の章と [比較](https://github.com/SeaOfNodes/Simple/compare/linear-chapter22...linear-chapter23) することもできます。 + +## なぜ循環する型なのか + +循環する型は代替手法より精密な解析を可能にし、より多くのプログラムや、よりよい最適化、あるいは両方を許します。Simple では型を型検査にも使うので、より精密な型により、より多くの正しいプログラムを許容します。 + +**なぜ今、循環する型なのか**。構造体を引数に取る関数定義を構造体内で許すとすぐ、循環が現れるからです。つまり *self* 引数を取る *メソッド* です。 + +上の `String.indexOf` の例を見ましょう。ここでは `indexOf` メソッドを持つ `String` を定義します。隠れた文字列引数 `self` が渡され、検索されます。`struct String` の型は何でしょうか。 + +`struct String { u8[~] buf; { !String !self, u8 c -> int } indexOf; }` + +これは `String` という名前の型で、フィールド `u8[~] buf` と、final で代入済みの定数フィールド `indexOf` を持ちます。後者自身の型は `{ !String !self, u8 c -> int }` です。つまり `String` の型は、`indexOf` の型の内側に自分自身への参照を持ちます。`String` の型は *循環* しています。 + +## 循環の少ない型 + +循環する型の問題は古くからあり、対処のための実績ある方法がいくつもあります。簡単なものの1つは、型の *定義* と、それとは別の型の *参照* を持つ方法です。参照は何らかの検索で型を参照します。たとえば型名とパーサーのシンボル表を使う、ハッシュ表検索です。 + +このモデルでは循環を実質的に回避します。型の循環内の「後退辺」は、すべて参照辺、場合によってはハッシュ表検索で実現します。 + +では、なぜこの方法を採用しないのでしょうか。 + +すべての型参照が同じ型定義へ戻るためです。型情報を特化しても失われます。*すべての* 参照が同じ定義へ戻るため、*すべての* 経路の MEET を取ることになるのです。 + +Java の `Object`、または C の `void*` をデータとして持つ連結リストの例を示します。 + +```java +struct List { + !List !next; + Object payload; // equivalently for C: void* +} +// Then walk a collection of ints and build a List: +List nums = null; +for( int x : ary_ints ) + nums = new List{next=nums; payload=x; } + +// Again for strings: +List strs = null; +for( String x : ary_strs ) + strs = new List{next=strs; payload=x; } +``` + +`nums` の型は何でしょうか。`*List`、つまり次の型です。 + +``` +struct List { List *next; Object payload } +``` + +`Object` を `int` や `String` に精密化できません。すべての List オブジェクトのデータ型は `Object` で、`Object` MEET `int` は `Object` に戻るためです。 + +真に循環する型を許せば、このような場面で `*List<int> { *List<int> next, int payload }` という精密な型を発見、あるいは推論できます。基本的に最適化器内で *ジェネリクス* を構築するのです。言語のジェネリクスや型変数そのものではありませんが、それにかなり近づきます。 + +## 循環する型の扱い + +この章で Simple は循環する型を直接扱います。子の型が元の型へ戻る `Type` オブジェクトを持つため、型に対して [*再帰下降*](https://en.wikipedia.org/wiki/Recursive_descent_parser) がそのままでは使えなくなります。 + +これまでは型構築に再帰下降を使ってきました。子の型を先に作ってインターンし、インターン済みの子で次の階層を構築します。各階層がインターンされ、再帰下降がインターン済みの型で止まるので、共有する型での重複作業を避けられます。このため現在の型に対する演算は、旧型と新型の *差分* に対して線形となり、増分的で高速です。 + +新たな循環する型では、循環全体を見るまでインターンできない問題に直面します。またハッシュ表は等値検査を必要としますが、循環を等値比較するのに通常の再帰下降は使えません。 + +グラフの循環を扱う標準的な方法は、「以前ここに来た」ことを示す *訪問済み* の概念です。一意な型 ID をキーにするハッシュ表を使います。「訪問済み」は、一意な ID が表のキーにあるという意味です。各 Type は大域カウンタから一意な ID を得ます。型をインターンするので、必要な一意 ID 数は一意な型の数と、循環を構築・インターンするためのわずかな予備だけです。過去の経験では通常数千程度が最大なので、小さな整数で十分です。 + +もう1つ、既存の基盤をできる限り残したいので、既存の再帰下降による巡回を少しだけ修正してすべて保持します。 + +すべての型循環には `TypeStruct` が存在します。ただし関数が *自身* を引数に取る純粋な関数型の循環は例外です。それは奇妙なので、どう扱うかはそれほど気にしません。各 `TypeStruct` 生成器の中核で大域 `VISIT` ハッシュ表に要素を設定し、循環型作成の開始を示します。そしてその要素を検査し、循環を止めます。 + +例を見ましょう。 + +異なるデータ型の循環を持つかもしれない連結リストを使います。ただし、ここでは文字列だけです。連結リストは、簡単な代替と比べてひどく非効率なので、ほぼどんな仕事でも間違った構造であることが多いのですが、教材用のデータ構造としては優れています。 + +```java +struct List { + !List? !next; // Next pointer or null + str !name; // Payload +}; +``` + +この構造体の型は宣言に一致します。 + +`struct List { List*? next, str !name };` + +これで文字列リストを構築できます。 + +```java +var list = new List{ name="Hello"; }; +list = new List { list, name="World"; }; +``` + +### 循環する型の読み取り専用/final の検査 + +どこかで読み取り専用の不変な版を作りたいとします。 + +```java +val hello = list; // Make a read-only version +print(hello); // Pass along the read-only version +``` + +`hello` の型は何でしょうか。`List` の読み取り専用型です。しかし `List` は循環する型です。`Type.makeRO` を見ましょう。 + +> 訳注:原文では上の `val hello = list` を読み取り専用版の作成と説明しています。一方、第17b章では `val` は束縛を固定し、初期化式のアクセス権限を保持すると説明しています。この訳では、原文の例と説明を保持しています。 + +```java +public final Type makeRO() { + if( isFinal() ) return this; // First check if already read-only + return recurOpen()._makeRO().recurClose(); +} +``` + +そして `isFinal()`: + +```java +// Are all reachable struct Fields are final? +public final boolean isFinal() { return recurClose(recurOpen()._isFinal()); } +boolean _isFinal() { assert _type < TCYCLIC; return true; } +``` + +`recurOpen()` を呼んで再帰走査を開始/開き、`_isFinal()` を呼びながら再帰し、走査を終了/閉じて、真偽値を返します。`_isFinal()` は Java の final ではなく、Type のサブクラスでオーバーライドします。大半のサブクラスは単に問題を転送します。 + +`@Override boolean _isFinal() { return _obj._isFinal(); }` + +そして `TypeStruct` に到達します。 + +```java +@Override boolean _isFinal() { + if( _open ) return false; // May have more non-final fields + if( VISIT.containsKey(_uid) ) // Test: been here before? + return true; // Cycles assume final + VISIT.put(_uid,this); // Set: don't do this again + for( Field fld : _fields ) + if( !fld._isFinal() ) + return false; + return true; +} +``` + +ここには循環処理の共通パターン、**test-and-set** があります。まず確定できる答えを調べます。開いた構造体は部分的にしか定義されず、未解析の non-final フィールドが現れる可能性があるので、決して読み取り専用ではありません。次に以前訪問したかを **検査** し、そうなら再帰を止めて何らかの答えを返します。`isFinal` では、循環のほかのものもすべて final なら「final である」を返します。 + +まだこの型を調べていなければ、ここを訪れたことを示す訪問ビットを **設定** し、通常の再帰下降でフィールドに同じ質問をします。 + +この例では、`VISIT` に番兵を置いて再帰を開き、`struct List` に `makeRO` を呼びます。それが `_isFinal` を呼び、`_open`(開いていない)、`VISIT`(未訪問)を確認し、UID を `VISIT` に設定してフィールドを走査します。フィールドの検査は次のとおりです。 + +`@Override boolean _isFinal() { return _final && _t._isFinal(); }` + +最初のフィールド `List*? !next` は non-final なので、すぐ `false` を返します。これで `List` の `_isFinal` が止まり、`isFinal()` 全体が `false` を返して再帰を終了/閉じます。`VISIT` もクリアします。 + +### 循環する読み取り専用/final 型の作成 + +構築してからインターンする手順を明確にするため、すべての型に一意 ID を付けましょう。一意な型の後ろに UID の例を付けた `List` 型を示します。フィールドも型なので UID を持ちます。 + +``` +struct List#2 { // struct List has UID#2 + List#2*?#3 !next#4, // Field next has UID#3, type ptr-or-null#3 to List#2 + str#1 !name#5 // Field name has UID#5, type str#1 +}; +``` + +同じ UID が繰り返されることから、`List#2` が2回現れています。List が循環しているためです。 + +今度は実際に循環する型を作る必要があり、`makeRO()` は次の行に進みます。 + +`return recurOpen()._makeRO().recurClose();`. + +再び `recurOpen` が再帰走査を開始/開き、それから子の Type クラスそれぞれでオーバーライドする `_makeRO()` を呼びます。`TypeStruct` 版を見ます。 + +```java +// Make a read-only version +@Override TypeStruct _makeRO() { + // Check for already visited + TypeStruct ts = (TypeStruct)VISIT.get(_name); + if( ts!=null ) return ts; // Already visited + ts = recurPre(_name,_open); // Make a new type with blank fields + Field[] flds = ts._fields; + for( Field fld : flds ) fld._final = true; + + // Now start the recursion + for( int i=0; i<flds.length; i++ ) + flds[i].setType(_fields[i]._t._makeRO()); + + return ts; +} +``` + +最初は **test-and-set** です。**検査** は型名に基づきます。`VISIT` から `List` TypeStruct の取得を試し、成功したら再帰せず早期に返します。つまり読み取り専用の型循環内では `List` オブジェクトは1つだけです。「整数の List」や「List の List の……」を含む複数の入れ子循環の絡み合いも考えられますが、ここでは簡単さのため、`List` のインスタンスを1つだけに近似します。 + +この例で初めて `List` を訪問すると `VISIT` に見つかりません。次の `ts = recurPre(_name,_open)` は共通の再帰前処理です。新しい `TypeStruct` を作り、`VISIT` 表に **設定** します。新しいものは、すべてのフィールドを備え、まだその型が接続されていない、空の `List` 版です。すぐにフィールドを final にしますが、型はまだありません。 + +> 訳注:原文の「all !fields !attached」は、続く説明と図に合わせて、フィールドの型が未設定である状態として訳しています。 + +``` +struct List#12 { + ____ next#14, + ____ name#15 +} +``` + +最後にすべてのフィールド型で `_makeRO` を再帰呼び出しし、設定します。`TypeMemPtr` に `_fields[i]._t._makeRO()` を呼ぶと、再帰する `List` の `_makeRO()` を呼んだ後に、新しい TMP を作ります。 + +この再帰は `TypeStruct._makeRO()` へ戻り、同じ型名で `VISIT` 表にヒットします。最初のフィールド設定を巻き戻すと、次になります。 + +``` +struct List#12 { + List#12 *? #13 next#14, // Recursive next field makes a cycle + ____ name#15 +} +``` + +すべての型が新しい UID になっていることに注目してください。まだインターンできないので、以前の型にはできません。2番目のフィールド `str name` はすでに存在するため、次になります。 + +``` +struct List#12 { + List#12 *? #13 next#14, // Recursive next field makes a cycle + str#2 name#15 // Reuse the interned type str#2 +} +``` + +ここで再帰を巻き戻し、複雑な `Type.recurClose()` に入ります。いくつかの例でコードを1ステップずつ実行してみるとよいでしょう。`TypeTest.testList` がこれらの型を構築します。高水準での概要は次のとおりです。 + +管理を容易にするため、訪問したすべての型を `VISIT` からコピーします。すでにインターン済みのものを発見したら後続の処理から除去しますが、HashMap 内にない方が簡単です。 + +**走査 #1:** 循環に属さない既存のインターン済み部分を探し、置き換えます。新しい型循環から古いインターン済み型へポインタが出ているのに、そのコピーを作ってしまっている場合があります。この段階の後も新しい循環は残りますが、出力辺は可能なら以前の型を指します。置き換えた型は、遅延した `FREES` リストへ入ります。 + +**走査 #2:** すべての型は *双対* を事前計算して直接使えるようにしています。ここで再帰的な循環する双対を作ります。 + +**走査 #3:** 新しい型を登録します。新しくインターンされた型への共通ポインタをここで発見する *可能性がある* ので、進みながら再び検査・置換し、使わない型を再度遅延 `FREES` リストへ入れます。この段階はハッシュ表検索を必要とし、それには循環ハッシュと循環 equals が必要です。 + +**走査 #4:** 遅延解放するすべての型をここで解放し、将来の型生成で再利用します。 + +最終的に、新しい `List` の循環型は完全にインターンされます。読み書き可能版 `List#2` は同じ読み書き可能版 `List#2` を指し、読み取り専用版 `List#12` は同じ読み取り専用版 `List#12` を指します。 + +### 循環する HashCode と Equals + +循環する `hashCode` と `equals` はさらに説明が必要です。2つの循環を等価比較するとき、比較の開始位置に関係なく等価であってほしいのです。つまり循環 `A<->B` は循環 `B<->A` と等しいべきです。両方のハッシュコードは同じでなければならず、順序で変化するハッシュは使えません。 + +たとえば A の静的ハッシュ値が1で、さらに B のハッシュから再帰的な成分があり、逆に B の静的値が3で A からの再帰成分があるとします。`parent.hash*7 + child.hash` で計算すると、A からは `A.hash * 7 + B.hash`、つまり `1 * 7 + 3 == 10` です。後で B から始めると `B.hash * 7 + A.hash`、つまり `3 * 7 + 1 == 22` になります。ハッシュが異なるため、ハッシュ表が等価性を見逃す可能性があります。また循環で単純に再帰下降すれば、スタックオーバーフローまで再帰してクラッシュします。 + +`TypeStruct.hash()` が再帰しないようにして解決します。そのためハッシュ関数はやや弱く、フィールド名とエイリアスだけに依存します。 + +*循環 equals* は `VISIT` 表が空でないことにより発動し、`VISIT` と関心を分離するため、別の訪問表 `CEQUALS` を使います。循環 equals 検査は通常の `eq` と同様、構成部分を再帰下降し、最初に確定的な静的検査を行い、再帰前にはおなじみの **test-and-set** を使います。 + +同じ2型を再び循環 equals で比較したら、ほかがすべて等しい限り、循環についても等しいと仮定します。 + +## メソッド、final フィールド、クラス + +メソッドは、構造体内で宣言され、暗黙の `self` 引数を取る、単なる関数値(final フィールド)です。 + +元の構造体定義で宣言した final フィールドは *値* であり、変化できません。すべてのインスタンスで同じです。構造体のメモリ占有部分から、final フィールドの値の集まりである *クラス* へ移動します。クラスは単なる名前空間で具体的な実装を持たず、実体化されません。 + +```java +struct vecInt { + u32 !len; // Actual number of elements + int[] !buf; // Array of integers. + + // Since "add" is declared with `val` in the original struct definition, + // it is a final field and is moved out of here to the class. + + // Method: Internal: copy buf to a new size + val _grow = { int sz -> + var buf2 = new int[sz]; + for( int i=0; i<len; i++ ) + buf2[i] = buf[i]; + buf = buf2; + }; + + // Method: Add an element, growing the backing array when needed. + val add = { int e -> + if( len >= buf# ) _grow(buf#*2); + buf[len++] = e; + return self; + }; + + +}; + +val primes = new vecInt{buf = new int[4];}.add(2).add(3).add(5).add(7).add(11); +``` + +ここで `val add` フィールドは、定数関数値である *値* フィールドです。基本的な `vecInt` オブジェクトから `vecInt` の *クラス* へ移り、オブジェクト内で記憶領域を占めません。`_grow` も同様です。そのため `vecInt` インスタンスのサイズは、`u32` の長さと配列ポインタに、配置調整のパディングを加えたものです。配列ヘッダと要素は別の割り当てを占めます。拡張後の古いバッファの回収は、この例の範囲外です。`return primes.len*100+primes.buf[4];` を追加すると、RISC-V と ARM の両方で511になります。 + +## RegAlloc の改良: 多数の使用のグループ化 + +第22章では色の優先度と安いスピルの順序を改善しました。この章では、使用が互換性のないレジスタを要求する、よく使う値を分割する方法を追加します。たとえば複数のシフトが同じカウントレジスタを必要とし、別の使用が異なるレジスタを必要とする場合です。互換グループごとに1コピーを共有すると、使用ごとに別のコピーを挿入するのを避けられます。 + +割り当て器は、空のレジスタマスクと、2つより多い固定レジスタ使用を持つ、単一定義の区間にこれを適用します。重なる使用マスクの共通部分を取り、グループ化します。グループを狭めても以前の使用と互換なので、1走査で十分です。交わらないマスクは別グループにします。入力を接続し直す前に異なる使用側のスナップショットを取り、スケジューリング専用辺は無視し、値が複数引数を供給しても呼び出しを1回だけ数えます。 + +複数の呼び出しで使う値は、レジスタ破壊が共有コピーの利点を打ち消しがちなので、グループ化しません。すべての使用のループ深さが同じ場合に既存コピーを分割することも含め、通常のループ境界分割を代替として保持します。低頻度側を先にするループ分割は第24章、面積/コストのスピル順位付けは第25章に残します。 + +第21章の隣接コピー転送も引き継ぎます。彩色後、次の命令に唯一の使用がある場合、オペランドマスクが許し、2アドレスの結び付きを壊さなければコピー元レジスタを読めます。以下の表は、この整理と遅延メモリの前方移植を含みます。第22章の群には、以前の監査後に追加した移動0の C 戻り値 ABI 確認2件も含むようになりました(24ではなく26項目)。 + +このディレクトリで `make spill-stats` を実行してください。以下の全行は **この章のコンパイラ**、シード123、各群の固定されたソース/対象の組み合わせを使います。Windows 実行は x86 SystemV/Win64 と RISC-V/ARM SystemV を合わせます。診断マシングラフは別に確認し、数には寄与しません。 + +| プログラム群 | コンパイル数 | 残存する移動 | ループ重み付き移動 | +|---|---:|---:|---:| +| 第20章 | 39 | 323 | 442 | +| 第21章 | 52 | 433 | 965 | +| 第22章 | 26 | 67 | 67 | +| 第23章 | 30 | 78 | 225 | +| **合計** | **147** | **901** | **1,699** | + +`_spills` はレジスタ移動を含む残存 SplitNode を数え、`_spillScaled` は `8^loopDepth` で重み付けします。生成移動のコンパイラによる推定であり、実行時メモリアクセスではありません。報告器は個々のコンパイルと CPU/ABI 合計を表示し、スピル期待値や実行確認が失敗すれば、依然として失敗を報告します。 + +以前の監査では、隣接コピー転送前にグループ化を無効にしても、**同じ933移動/1,745重み付き移動** でした。元のグループ化コードも同じ合計です。したがってこのスイートには、グループ化によるスピル改善は見られません。縮小したマシングラフの回帰テストは、旧実装の null マスクによるクラッシュも含め、互換性と呼び出しの規則を検証します。もっともらしい割り当て器のヒューリスティックであっても、効果を主張するには、それが実際に適用される処理対象での測定が必要です。 + +以前の群だけでは、第22章コンパイラは838移動/1,496重み付き移動で、このコンパイラは849/1,514でした。この比較は割り当て以外の変更を含みます。特に第20章の固定した String 入力は、ここでは最適化後も残り16重み付き移動を要しますが、第22章はこのシードで除去しました。その差をグループ化に帰するのは誤解を招きます。 + +元の64ビット `person21` とガード付き `stringHash21` 入力は、第21章の群を維持します。改訂した String 入力とこの章で有効にした2つのエンコーディングテストは第23章の群に属します。群を固定することで、言語とテストが成長しても、以後の比較に意味が保たれます。 diff --git a/chapter23/README.md b/chapter23/README.md index ee17f9f2..1f073c6b 100644 --- a/chapter23/README.md +++ b/chapter23/README.md @@ -1,5 +1,7 @@ # Chapter 23: Methods and Revisiting Types +English | [日本語](README.ja.md) + [Previous: Chapter 22](../chapter22/README.md) | [Next: Chapter 24](../chapter24/README.md) diff --git a/chapter23/docs/18-grammar.ja.md b/chapter23/docs/18-grammar.ja.md new file mode 100644 index 00000000..c2b37f67 --- /dev/null +++ b/chapter23/docs/18-grammar.ja.md @@ -0,0 +1,138 @@ +# 第23章の文法 + +[English](18-grammar.md) | 日本語 + +この文書は [原文](18-grammar.md) の日本語訳です。 + +> 訳注: 原文の見出しは「Grammar for Chapter 16」です。この日本語版では、配置されている第23章に合わせた見出しにしています。 + +[第23章に戻る](../README.ja.md) + +```antlrv4 +grammar SimpleLanguage; + +program : block EOF ; + +block : statement+ ; + + +statement + : '{' block '}' + | returnStatement + | ifStatement + | whileStatement + | breakStatement + | continueStatement + | structDeclaration + | expressionStatement + ; + + +returnStatement : 'return' expression ';' ; + +ifStatement : 'if' '(' expression ')' statement ('else' statement)? ; + +whileStatement : 'while' '(' expression ')' statement ; + +breakStatement : 'break' ';' ; + +continueStatement : 'continue' ';' ; + + +structDeclaration : 'struct' IDENTIFIER '{' block '}' ; + +expressionStatement + : type IDENTIFIER ';' + | type IDENTIFIER '=' expression ';' + | IDENTIFIER '=' expression ';' + | expression + ; + +PRIMTYPE + : 'int' + | 'i8' + | 'i16' + | 'i32' + | 'i64' + | 'u8' + | 'u16' + | 'u32' + | 'flt' + | 'f32' + | 'f64' + | 'bool' + ; + +type + : PRIMTYPE + | typeName '?'? + | typeName nestedArray+ + ; + +nestedArray : '?'? '[]'* ; + +typeName : IDENTIFIER ; + + +expression : bitWiseExpression ; + +bitWiseExpression + : comparisonExpression ( '&' | '|' | '^' ) comparisonExpression)* + ; + +comparisonExpression + : shiftExpression (('==' | '!='| '>'| '<'| '>='| '<=') shiftExpression)* + ; + +shiftExpression + : additiveExpression ( '<<' | '>>>' | '>>' ) additiveExpression)* + ; + +additiveExpression + : multiplicativeExpression (('+' | '-') multiplicativeExpression)* + ; + +multiplicativeExpression + : unaryExpression (('*' | '/') unaryExpression)* + ; + +unaryExpression + : ('-') unaryExpression + | '!' unaryExpression + | primaryExpression postAssign + ; + +primaryExpression + : INTEGER_LITERAL + | FLOAT_LITERAL + | '(' expression ')' + | 'true' + | 'false' + | 'null' + | newExpression + | IDENTIFIER + ; + +newExpression + : 'new' IDENTIFIER [ '{' block '}' ] + | 'new' IDENTIFIER '[' expression ']' + ; + +postAssign : postFix [ '=' expression ] | [ '#' ]; + +postFix + : '.' IDENTIFIER postFix + | '[' expression ']' postFix + ; + + +IDENTIFIER : NON_DIGIT (NON_DIGIT | DIGIT)* ; + +INTEGER_LITERAL + : [1-9]DIGIT* + | [0] + ; + +NON_DIGIT: [a-zA-Z_]; +DIGIT: [0-9]; +``` diff --git a/chapter23/docs/18-grammar.md b/chapter23/docs/18-grammar.md index 7da725e9..51539d0b 100644 --- a/chapter23/docs/18-grammar.md +++ b/chapter23/docs/18-grammar.md @@ -1,5 +1,7 @@ # Grammar for Chapter 16 +English | [日本語](18-grammar.ja.md) + ```antlrv4 grammar SimpleLanguage; diff --git a/chapter24/README.ja.md b/chapter24/README.ja.md new file mode 100644 index 00000000..dd650ba3 --- /dev/null +++ b/chapter24/README.ja.md @@ -0,0 +1,260 @@ +# 第24章: 関係演算の連鎖と疎な条件付き定数伝播 + +[English](README.md) | 日本語 + +この文書は [原文](README.md) の日本語訳です。 + +[前の章: 第23章](../chapter23/README.ja.md) | +[次の章: 第25章](../chapter25/README.ja.md) + +この章では、たとえば `(60 <= score < 90)` のような条件の *連鎖* を許し、中央の `score` を繰り返さずに済むようにします。 + +コンパイラの主な変更は、手続き間の疎な条件付き定数伝播(SCCP)です。呼び出しグラフの辺も発見する、楽観的な型の求解です。メモリは第11章の遅延分割に従います。BulkMemPhi はメモリ全体を表し、MemPhi は正確なエイリアスをマージし、割り当てはインスタンスフィールドの部分メモリを消費・生成します。SCCP 中、メモリ Phi は到達不能入力を無視し、割り当て結果は制御が到達するまで高いままです。第25章には、不完全な型のための大きな SSA 再設計、非公開のコンストラクタメモリ、分割コンパイルを残します。[バックポートのレビュー待ち一覧](../docs/chapter-backports.md) を参照してください。 + +[linear](https://github.com/SeaOfNodes/Simple/tree/linear) ブランチ上の直線的な Git リビジョン履歴で [この章](https://github.com/SeaOfNodes/Simple/tree/linear-chapter24) を読み、前の章と [比較](https://github.com/SeaOfNodes/Simple/compare/linear-chapter23...linear-chapter24) することもできます。 + +## 関係比較の連鎖 + +関係比較の連鎖により、同じ変数を繰り返さず、連続する比較をすっきりと読みやすく書けます。 + +```java +if (min <= x && x < max) +``` + +は、次になります。 + +```java +if (min <= x < max) +``` + +この構文は場所を節約し、数学で範囲検査を書く方法に似た、直感的な条件にします。たとえば: + +```java +int score = 75; +if (60 <= score < 90) { + sys.io.p("Pass"); +} +``` + +これは `score` を繰り返さず、60以上90未満かを検査します。 + +`<=` と `>=` の両方を使うなど、逆向きの比較を混ぜる式は、Simple の方向規則では許されません。 + +```java +if (a <= b >= c) +``` + +### 演算子の方向規則 + +重ねた比較は、すべての比較演算子が同じ方向を「向く」場合 *だけ* 有効です。 + +- `<` と `<` +- `<` と `<=` +- `>` と `>` +- `>` と `>=` + +等値演算子(`==` と `!=`)は通常どおり、より低い優先順位を持ちます。関係演算の連鎖をつなぐ要素ではありません。 + +1つの連鎖で方向を混ぜることはできません。 + +```java +if (a <= b >= c) // Invalid +``` + +真偽値を使えば、比較を数値の文脈に強制できます。次は有効ですが、珍しい書き方です。 + +```java +return (0 < arg) > 1; +``` + +`(0 < arg)` の結果に応じて、次のいずれかになります。 + +```java +return 0 > 1; +``` + +または + +```java +return 1 > 1; +``` + +等値演算子もこの形式で有効ですが、比較より優先順位が低いため、 + +```java +a < b == c +``` + +は次のように解析されます。 + +```java +(a < b) == c +``` + +これは意図した連鎖比較とは違うかもしれません。より明示的な代替は次です。 + +```java +a < b && b == c +``` + +比較は任意の長さで連鎖でき、同じ方向を向く限り、異なる比較演算子を自由に混ぜられます。中間の各オペランドは1回だけ評価します。後のオペランドは、前の比較が成功した場合だけ評価するため、連鎖内の呼び出しや更新は短絡評価されます。 + +例: + +``` +return 0 < arg < arg+1 < 4; +``` + +### 注意が必要な境界例 + +```java +return 0 != arg != 1; +``` + +これは連鎖した不等比較ではなく、標準の優先順位規則により次のように解析します。 + +```java +return ((0 != arg) != 1); +``` + +```java +return a == b == c; +``` + +同様に、これは次のように解析します。 + +```java +return ((a == b) == c); +``` + +## 疎な条件付き定数伝播 + +[SCCP](https://en.wikipedia.org/wiki/Sparse_conditional_constant_propagation) は、これまで行ってきたピープホール最適化/定数畳み込みの、上から下へ進む版です。アルゴリズムの規則は文献でよく知られ、大学で一般に教えられています。[スライドの例](https://www.cse.psu.edu/~gxt29/teaching/cse522s23/slides/08monotoneFramework.pdf) があります。 + +- すでに束を持っています。 +- すでに SSA 形式を持っています。したがってアルゴリズムは *疎* です。 +- ノードから型への対応(`Node._type`)を持っています。 +- すでに伝達関数(`Node.compute`)を持っています。 + +すべての `_type` を、⊥ ではなく ⊤ に初期化し、すべてのノードをワークリストに置きます。そこからノードを取り、伝達関数 `node.compute()` を実行します。結果が `node._type` と変われば更新し、その出力側の隣接ノードをワークリストへ入れます。アサーションとデバッグ補助をすべて取り除いた簡略版は次のとおりです。 + +```java +while( (n = code._iter._work.pop()) != null ) { + Type nval = n.compute(); + if( n._type != nval ) { + assert n._type.isa(nval); // Types start high and always fall + n._type = nval; // Update type in Node + code._iter.addAll(n._outputs);// Neighbors might change, need to inspect + } +} +``` + +このアルゴリズムは必ず停止し、一般にはかなり速く、束と伝達関数の制限内での「最良の答え」である *不動点* に必ず到達します。得られる型はピープホール規則で見つけたものより単調に改善されます。その後、型が改善されたノードでピープホール規則を実行し、無関係なピープホール最適化が適用できるかを調べられます。たとえば新しく発見した定数が、計算を ConstantNode に置き換えます。 + +以下は SSA の擬似コードです。`phi` はマージを意味し、Simple の構文ではありません。 + +```text +int x0 = 1; // x0 = 1 +while( rand ) { + x1 = phi(x0,x2); // x1 = BOT = phi(1,BOT) + x2 = 2 - x1; // x2 = BOT = 2 - BOT +} +return x1 +``` + +SCCP がなければ、この小さなプログラムは `x` の計算を畳み込めません。見れば `x` は常に `1` とわかりますが、下から上へ進む悲観的手法は、x1 が BOT なので x2 が BOT、したがって x1 が BOT と判断します。行き詰まりを解くには楽観的手法が必要です。x1 が `1` と *仮定* すれば、x2 も `1` と発見できます。値の畳み込みは、ループ停止を証明したり、その条件と副作用を除去可能にしたりするものではありません。 + +アルゴリズムは左から右へ進み、変化がなくなったら停止します。 + +| 変数 | 初期 | x0=1 | x1=phi(1,T) | x2=2-x1 | x1=phi(1,1) | +|---|----|----|-----------|-------|-----------| +| x0 | T | 1 | 1 | 1 | 1 | +| x1 | T | T | 1 | 1 | 1 | +| x2 | T | T | T | 1 | 1 | + +### 条件付き + +制御フローがデータフローと同じグラフ内にあるため(これが Sea-of-Nodes の大きな要点です)、特別なことをする必要はなく、アルゴリズムはすでに *条件付き* です。ここでも例が役に立ちます。 + +再び SSA の擬似コードを使います。 + +```text +int x0 = 1; // x0 = 1 +while( rand ) { + x1 = phi(x0,x4); // x1 = BOT = phi(1,BOT) + if( x1 == 1 ) // Since x1 is BOT, both true and false arms are taken + x2 = 2 - x1; // x2 = BOT = 2 - BOT + else x3 = 99; // x3 = 99 + x4 = phi(x2,x3); // x4 = BOT = phi(BOT,99) +} +return x1 +``` + +再び左から右へ進みますが、今度はすべての経路が到達可能とは限りません。`if` の結果は Control 値の対で、経路が生きていれば `C`、そうでなければ `X` で示します。これは `x4 = phi(x2,x3)` の計算で使います。`PhiNode` は `if` の対応する制御入力 *も* 確認し、生きた経路だけをマージします。 + +| 変数 | 初期 | x0=1 | x1=phi(1,T) | 1 == 1? | x2=2-x1 | x3=99 | x4=phi(C/1,X/99) | x1=phi(1,1) | +|---|-----|----|-----------|---------|---------|---------|----------------|-----------| +| x0 | T | 1 | 1 | 1 | 1 | 1 | 1 | 1 | +| x1 | T | T | 1 | 1 | 1 | 1 | 1 | 1 | +| if | T | T | T | [C,X] | [C,X] | [C,X] | [C,X] | [C,X] | +| x2 | T | T | T | T | 1 | 1 | 1 | 1 | +| x3 | T | T | T | T | T | 99 | 99 | 99 | +| x4 | T | T | T | T | T | T | 1 | 1 | + +この種の条件付き定数伝播は、条件が真か偽と証明できると、最適化後のプログラムから検査と分岐を除去するので、非常に役立ちます。よくあるのが null 検査です。同じプログラムで、整数の代わりに null ポインタを使います。 + +```java +List head = notNullList(); +while( rand ) { + if( head ) head = new List(head,payload); + else { head = null; default_action(); } +} +``` + +`head` は非 null で始まるため、決して null にならず、`default_action` も起きません。ループは簡略化できます。 + +```java +List head = notNullList(); +while( rand ) { + head = new List(head,payload); +} +``` + +### 手続き間 + +**Simple** では、ここでもほぼ何もせず、SCCP を手続き間で実行します。`CallNode` は `FunNode` を呼び、`ReturnNode` は `CallEndNode` に戻り、まったく通常の方法で型情報を渡します。ただし問題が1つあります。`Call` と `Fun`、および `Return` と `CallEnd` の間に辺がありません。そのような辺が [呼び出しグラフ](https://en.wikipedia.org/wiki/Call_graph) を構成します。 + +通常の SCCP を拡張し、`Call` の関数ポインタ入力が変わり、新しい関数対象を得る時点を監視します。対象の数は有限なので、前の章で説明した *関数インデックス* で正確に表します。分割コンパイルでは、このコンパイル単位の外で呼ばれる無限の未知関数に対して、特別なインデックスを追加します。 + +`Call` で新しい *fidx* を見つけたら、`Call` と `Fun`、`Return` と `CallEnd` を *リンク* します。このリンクは、`Call` から `Fun`、そして `Call` の引数から `Fun` の `ParmNode` へ CFG 辺を追加したかのように動きます。`FunNode` は `RegionNode` を、`ParmNode` は `PhiNode` を拡張するため、同じ意味規則を使います。`Fun` は呼ばれると到達可能になり、`Parm` はすべての呼び出し引数をマージ(*meet*)します。同様に `CallEnd` は、すべての `Return` の *meet* を計算して、呼ばれたすべての関数の結果をマージします。 + +この *リンク* 段階はアルゴリズムの内部ループにいくつかの検査を加え、正確な呼び出しグラフも構築し、SCCP の手続き間実行を可能にします。進行中に新しい辺を少し発見する以外は、中核アルゴリズムは変わりません。不動点解で停止することが保証され、たとえば特定の呼び出しパラメータが、すべての呼び出し経路で定数、あるいは非 null と発見できることがあります。 + +## RegAlloc の改良: 低頻度側のループ分割を先に試す + +ループの Phi と後退辺は、ある命令で値が共存しなければならなくても、同じ生存区間に属する場合があります。この自己衝突は区間の分割を必要とします。第23章は Phi の定義と入力値をすぐに分割しました。この章では先に、高頻度の定義/後退辺をそのままにし、ループ外のコピーを試します。彩色が成功すれば各反復の移動を避けられます。2アドレス演算を含むほかの衝突は、既存の処理を保持します。 + +割り当て器は、どの Phi にこの機会を与えたかを記憶します。同じ Phi が再び自己衝突すると、次は通常の積極的な分割を使います。区間は各ラウンドで再構築するため、記録には Phi の同一性を使います。最初の試行でコピーを挿入しない、後退辺だけの衝突も対象です。その試行も、1回だけ許された遅延を消費しなければなりません。回帰テストは、安い最初の試行と、必須の代替処理の両方を確認します。 + +第21章の隣接コピー転送も引き継ぎます。彩色後、次の命令に唯一の使用がある場合、オペランドマスクが許し、2アドレスの結び付きを壊さなければコピー元レジスタを読めます。以下の表は、この整理と遅延メモリの前方移植を含みます。第22章の群には、以前の監査後に追加した移動0の C 戻り値 ABI 確認2件も含むようになりました(24ではなく26項目)。 + +`make spill-stats` を実行してください。全行は **第24章のコンパイラ**、最適化器のシード123、以前の群の表と同じソース/対象の組み合わせを使います。この Windows 結果は、x86 SystemV/Win64 と RISC-V/ARM SystemV を合わせ、遅延メモリの前方移植後に再測定したものです。 + +| プログラム群 | コンパイル数 | 残存する移動 | ループ重み付き移動 | +|---|---:|---:|---:| +| 第20章 | 39 | 320 | 439 | +| 第21章 | 52 | 420 | 952 | +| 第22章 | 26 | 67 | 67 | +| 第23章 | 30 | 78 | 225 | +| 第24章 | 67 | 422 | 1,325 | +| **合計** | **214** | **1,307** | **3,008** | + +`_spills` はレジスタ移動を含む残存 SplitNode を数えます。`_spillScaled` は `8^loopDepth` で重み付けするコスト推定で、測定されたメモリアクセスではありません。診断マシングラフはこの表の割り当てに寄与しません。報告器は、スピル期待値が異なっても、割り当ての合法性とネイティブ/エミュレーション結果を確認し、いずれの失敗でも失敗終了します。 + +後のコンパイラ修正より前の、元の2026-09-20割り当て器監査では、表の合計は1,332/2,956でした。低頻度側を先にする規則だけを無効にすると、同じコンパイラとプログラムは **1,338移動/2,962重み付き移動** でした。この規則は各6を減らします。RISC-V MergeSort の1ケースは1移動増えますが、ほかの MergeSort、Sieve、ARM ループの1ケースの改善が相殺します。集計測定はこの局所的な悪化を受け入れる根拠となります。ここでの改善は小さく、普遍的ではありません。 + +その監査で第20〜23章の群だけを見ると、第23章は933/1,745、第24章は900/1,614でした。それぞれの削減のうち、この割り当て器変更によるものは6だけです。残りは SCCP を含むほかのコンパイラ変更です。章だけの比較では、ヒューリスティックの効果を過大に見積もってしまいます。 + +以前の群は64ビット Person、ガード付き String、短いエミュレータ引数リストを保持します。改訂した Jig、BubbleSort、Newton、スタック引数入力は、既存のテストとともに第24章の群に属します。固定した String 処理対象は、インライン化の支配ノード走査で null 検査が欠けていたことも明らかにしました。その正しさの修正はヒューリスティック比較の両側に含めています。面積/コストのスピル順位付けは第25章に残します。 + +現在の関数ポインタ規則は、アドレスが直接呼び出し対象としてだけでなく、値として使われるたびに、未知の呼び出し側を保持します。これにより脱出解析なしで呼び出し可能な本体を保存しますが、Phi で選ばれた関数は一般的なパラメータ型を保持します。return のない String ケースは、今では未使用のハッシュ本体を除去し、分割を必要としません。ソース/対象の構成は変わりません。これらのグラフ変更は、上の低頻度側優先の割り当て器比較とは別です。 diff --git a/chapter24/README.md b/chapter24/README.md index 6adbe50a..ce8afae0 100644 --- a/chapter24/README.md +++ b/chapter24/README.md @@ -1,5 +1,7 @@ # Chapter 24: Chaining Relationals and Sparse Conditional Constant Propagation +English | [日本語](README.ja.md) + [Previous: Chapter 23](../chapter23/README.md) | [Next: Chapter 25](../chapter25/README.md) diff --git a/chapter24/docs/18-grammar.ja.md b/chapter24/docs/18-grammar.ja.md new file mode 100644 index 00000000..a36d9504 --- /dev/null +++ b/chapter24/docs/18-grammar.ja.md @@ -0,0 +1,138 @@ +# 第24章の文法 + +[English](18-grammar.md) | 日本語 + +この文書は [原文](18-grammar.md) の日本語訳です。 + +> 訳注: 原文の見出しは「Grammar for Chapter 16」です。この日本語版では、配置されている第24章に合わせた見出しにしています。 + +[第24章に戻る](../README.ja.md) + +```antlrv4 +grammar SimpleLanguage; + +program : block EOF ; + +block : statement+ ; + + +statement + : '{' block '}' + | returnStatement + | ifStatement + | whileStatement + | breakStatement + | continueStatement + | structDeclaration + | expressionStatement + ; + + +returnStatement : 'return' expression ';' ; + +ifStatement : 'if' '(' expression ')' statement ('else' statement)? ; + +whileStatement : 'while' '(' expression ')' statement ; + +breakStatement : 'break' ';' ; + +continueStatement : 'continue' ';' ; + + +structDeclaration : 'struct' IDENTIFIER '{' block '}' ; + +expressionStatement + : type IDENTIFIER ';' + | type IDENTIFIER '=' expression ';' + | IDENTIFIER '=' expression ';' + | expression + ; + +PRIMTYPE + : 'int' + | 'i8' + | 'i16' + | 'i32' + | 'i64' + | 'u8' + | 'u16' + | 'u32' + | 'flt' + | 'f32' + | 'f64' + | 'bool' + ; + +type + : PRIMTYPE + | typeName '?'? + | typeName nestedArray+ + ; + +nestedArray : '?'? '[]'* ; + +typeName : IDENTIFIER ; + + +expression : bitWiseExpression ; + +bitWiseExpression + : comparisonExpression ( '&' | '|' | '^' ) comparisonExpression)* + ; + +comparisonExpression + : shiftExpression (('==' | '!='| '>'| '<'| '>='| '<=') shiftExpression)* + ; + +shiftExpression + : additiveExpression ( '<<' | '>>>' | '>>' ) additiveExpression)* + ; + +additiveExpression + : multiplicativeExpression (('+' | '-') multiplicativeExpression)* + ; + +multiplicativeExpression + : unaryExpression (('*' | '/') unaryExpression)* + ; + +unaryExpression + : ('-') unaryExpression + | '!' unaryExpression + | primaryExpression postAssign + ; + +primaryExpression + : INTEGER_LITERAL + | FLOAT_LITERAL + | '(' expression ')' + | 'true' + | 'false' + | 'null' + | newExpression + | IDENTIFIER + ; + +newExpression + : 'new' IDENTIFIER [ '{' block '}' ] + | 'new' IDENTIFIER '[' expression ']' + ; + +postAssign : postFix [ '=' expression ] | [ '#' ]; + +postFix + : '.' IDENTIFIER postFix + | '[' expression ']' postFix + ; + + +IDENTIFIER : NON_DIGIT (NON_DIGIT | DIGIT)* ; + +INTEGER_LITERAL + : [1-9]DIGIT* + | [0] + ; + +NON_DIGIT: [a-zA-Z_]; +DIGIT: [0-9]; +``` diff --git a/chapter24/docs/18-grammar.md b/chapter24/docs/18-grammar.md index 7da725e9..51539d0b 100644 --- a/chapter24/docs/18-grammar.md +++ b/chapter24/docs/18-grammar.md @@ -1,5 +1,7 @@ # Grammar for Chapter 16 +English | [日本語](18-grammar.ja.md) + ```antlrv4 grammar SimpleLanguage; diff --git a/chapter25/README.ja.md b/chapter25/README.ja.md new file mode 100644 index 00000000..fc4d96e4 --- /dev/null +++ b/chapter25/README.ja.md @@ -0,0 +1,123 @@ +# 第25章: モジュール、分割コンパイル、SSA 構築 + +[English](README.md) | 日本語 + +この文書は [原文](README.md) の日本語訳です。 + +[前の章: 第24章](../chapter24/README.ja.md) | +[章の一覧](../README.ja.md#各章) + +この章では Simple のソースファイルを再利用可能なオブジェクトファイルへコンパイルし、その型と理想 IR を後のコンパイルに読み込み、別に構築したシステムライブラリとネイティブプログラムをリンクします。また、不完全な情報を扱うため、パーサー、型、メモリ SSA を大幅に再設計します。 + +これらが現在のスナップショットの内容です。独立した正しさの修正を先に以前の章へ移します。大きな SSA 再設計を以前へ移すことや、この章の分割は延期します。[バックポート待ち一覧](../docs/chapter-backports.md) を参照してください。 + +第21〜24章のメモリ処理はここにも引き継ぎます。Load 検索は読み取りをループ外へ移す前にマージの全枝を証明し、Phi の因数分解はメモリ破壊を確認し、スケジューリングはメモリ集合体を通じてエイリアスをたどります。検索は `EscapeNode` から非公開または公開メモリへ進み、未解決の Store エイリアスを待ちます。配列長の読み取りには要素の境界検査がなく、配列要素の読み取りは制御を保持します。整数値の Phi は、既存の型付き変換機構で、ループの拡大を通じてもロード幅を保持します。New は引き続き非公開のコンストラクタメモリを生成し、公開は Escape が扱います。以前の章の割り当て入力設計は、これを置き換えません。 + +[直線的な履歴](https://github.com/SeaOfNodes/Simple/tree/linear) で [この章](https://github.com/SeaOfNodes/Simple/tree/linear-chapter25) を読み、[第24章と比較](https://github.com/SeaOfNodes/Simple/compare/linear-chapter24...linear-chapter25) することもできます。 + +## コンパイル単位と名前 + +`CompUnit` はソースまたはオブジェクトファイルを表します。`ParseAll` は依存関係を発見し、必要なソースを解析し、以前コンパイルした単位を読み込みます。ソースツリーとビルドツリーは対応する相対パスを使い、クラス名はドット区切りのパスを使います。たとえば `a/b/foo.smp` は名前空間 `a.b.foo` を供給します。 + +各単位は Start/Stop ノードを持ちます。関数がコンパイル単位に所有されることと、未知の外部呼び出し側を表す辺とは別です。出力のためにコードを保持することが、任意の呼び出し側から到達できることを意味してはいけません。 + +ドライバは、プロジェクトルートの `--root`、出力の `-o`、外部 Simple オブジェクト検索パスの `-L` を受け付けます。検索パスにはオブジェクトファイルやディレクトリを指定できます。コンパイラはネイティブリンク用の外部 C シンボルも解決します。`_` で始まる名前は非公開性を制御し、公開到達性は外側の名前にも依存します。 + +明示的な数値型の C 宣言は、可変のネイティブ記憶領域へ直接束縛します。 + +```java +i32 counter = "C"; +counter += 1; +return counter; +``` + +ネイティブリンカが `counter` を解決します。読み書きはその C 変数にアクセスし、宣言はコピーも初期化もしません。繰り返した宣言とモジュールインポートは同じ記憶領域エイリアスを共有します。整数と浮動小数点データの束縛をサポートし、ポインタと集合体のデータ束縛はまだサポートしません。外部フィールドは記憶領域フラグを持ち、宣言元クラス内でバイトを占めません。配置はフィールドオフセットを `ExternOffset` シンボルへ解決し、命令選択はクラス先頭+オフセットをネイティブシンボルアドレスへ置き換えます。ネイティブオブジェクト出力は既存の AMD64 ELF ライタを使い、ARM と RISC-V はメモリ内リンカを通じてこれらのアドレスをサポートします。スレッドローカル変数と C マクロにはアクセサ関数が必要です。システムライブラリは小さなネイティブランタイムアクセサを通じ、`libc.errno()` を公開します。 + +## オブジェクトファイルは理想 IR を含む + +ELF ライタは、ネイティブコードと、正規化された型、理想グラフノード、依存関係を含む `.simple` セクションを保存します。リーダはそのグラフを再構築し、インポートした関数をインライン化などの最適化に参加させます。インライン化せず残った呼び出しは、ネイティブリンクで解決します。 + +`GlobalBits` はファイルの同一性とファイルごとのインデックスを、エイリアス、関数対象、戻り位置の局所的な密なインデックスへ対応付けます。独立したコンパイルの等しい局所番号を、等しい同一性と取り違えてはいけません。読み込み後に型を閉じて更新し、単位をまたぐ循環参照も扱います。 + +シリアライズは、宣言型を精密化されたフロー型とは別に保持します。意味を持つすべてのノード/型のフィールドには、対応する読み書きサポートが必要です。ソースにある高コストなシリアライズの全単射検査は現在無効で、より大きな増分再ビルドテスト `testModule0` は無視されています。どちらも有効な検証保証ではありません。 + +外部フィールドのフラグと記号的オフセットは、シリアライズされた配置を変更します。マジックは `C0DE` のままで、独立した形式バージョンはまだありません。このコンパイラへインポートする前に、すべての Simple オブジェクトファイルを再ビルドしてください。 + +> 訳注:原文のこの段落は `C0DE` としていますが、後の「ビルドと確認」では `C0D2` ヘッダを使うと記しています。この訳では、原文の両方の記述を保持しています。 + +## 型が完成する前の SSA 構築 + +Simple は引き続き AST を介さず直接 SSA に解析します。前方参照により、解析時点では式の最終的な型が利用できない場合があります。そのため構文と解決済みの字句的束縛でトポロジーを選び、後から型でグラフを精密化、最適化、検証します。 + +- 算術ノードは整数モードか浮動小数点モードかの選択を延期し、SCCP 後の再帰的グラフ成分を通じた解決も扱います。 +- Call は呼び出し先の種類が決まる前に、構文上のレシーバスロットを保持します。記号的なフィールド情報は配置依存の判断を延期します。 +- Load と通常の Phi は、パーサーが供給した下限型を持たなくなります。 +- `TypeScalar` は整数、浮動小数点、メモリポインタ、関数ポインタ値を、大域的な制御/メモリのボトムと区別して表します。 +- `TypeStruct._open` は不完全なフィールド集合を記述し、`_fref` は権威ある定義の欠如を記録します。フィールドを発見したことは、型を定義したことではありません。 +- ガードは入力の種類がわかる前に、分岐が証明したゼロ/非ゼロの事実を運びます。`FunPtrNode` は関数の戻りグラフへの明示的な辺を保持します。 + +ドライバはシリアライズとマシン選択の前に、解析、悲観的反復、SCCP、最終型検査を行います。単純でないインライン化は、常に即時のピープホール最適化として行うのではなく、反復と調整します。 + +## メモリと構築 + +パーサーは通常のバルクメモリを保持します。最適化器は `MemMerge`、`MemPhi`、`BulkMemPhi` で正確なエイリアスを復元します。バルク Phi は、並列の正確な Phi で表す除外分以外のすべてのエイリアスを覆います。各スライスはちょうど1回だけ覆われなければなりません。 + +ユーザーコンストラクタは次のように宣言し、呼び出します。 + +```java +struct Box { + i64[] values; + new Box = { i64[] initial -> values = initial; }; +}; +return new Box(new i64[1]); +``` + +宣言で割り当て補助関数を生成します。これは割り当てを行い、隠れた `<init>` とユーザーコンストラクタを順に呼び、`EscapeNode` を通じて初期化済みの非公開メモリを公開します。パーサーの `Var` メタデータは、早すぎるフィールド読み取りやコンストラクタ出口での未初期化を検出します。Store の幅は保存する値ではなく、対象の宣言から得ます。 + +ファイルレベルのコードは、インスタンス初期化とは異なるクラス初期化子(`<clinit>`)で表します。古い [モジュール設計ノート](module.ja.md) と [ロードマップ](ROADMAP.ja.md) には、特に初期化順序に関する提案や代替案があります。実装済み規則すべての仕様ではありません。 + +## システムライブラリと例 + +以前の単一の `sys.smp` ライブラリは、libc 束縛、I/O、文字、スキャナ、配列、伸長可能なバッファ、ビット集合の単位に整理されました。ネイティブドライバは実際のプログラム引数をサポートします。`docs/examples` の例には Bubble Sort、Capitalize、Dijkstra、FileIO があります。 + +`Chapter25Test` は、意図的にインライン化せず残す Hello World 呼び出しを含め、ライブラリ読み込みとネイティブリンクを検証します。そのテストは `sys.o` とリンクする前に、クライアントの外部シンボルを確認します。ほかのテストは前方コンストラクタ、呼び出し経由のフィールド更新、リテラルのエスケープ、縮小したコンパイラ失敗を検証します。独立した回帰テストは以前の章へ移す候補です。 + +## ビルドと確認 + +このディレクトリで `make lib` は Java テスト依存関係を用意し、`make tests` は章の生のスイート、独立スイート、システムテスト、ファザー回帰テストを実行します。`make release` はコンパイラ jar とネイティブライブラリ成果物を構築し、`make tags` はエディタ用タグを構築します。ネイティブツールと選択した CPU/ABI は環境と一致する必要があります。Makefile は現在、既定で x86-64/win64 を使います。 + +シリアライズ IR は現在 `C0D2` ヘッダを使います。ポインタ型は独立した読み取り専用アクセスビットを含みます。古いオブジェクトファイルは再ビルドが必要です。リーダは旧ヘッダを拒否し、新しい配置で旧型を解釈しません。 + +コンパイラ変更は `sys.o` のシリアライズ IR とネイティブコードの両方を無効にする場合があります。リンクしたプログラムのテスト結果を解釈する前に再ビルドしてください。ソースだけの部分集合は、この章の完全な `make tests` と同等ではありません。 + +## RegAlloc の改良: 生存区間の面積と分割コスト + +第24章はループ Phi の高頻度辺を分割する前に、低頻度のコピーを試しました。この章ではさらに、スピルがスケジュール済みコードのどれだけをレジスタ圧力から解放するかを推定し、新しいコピーの推定コストで割ります。callee-save 区間は関数全体を覆います。複製可能な定数は使用の近くで再構築できます。ループ内のコピーは高コストなので、`8^loopDepth` で重み付けします。特に多数の使用を持つ値については、低コストな近似です。 + +複数の固定レジスタ使用を直接分割する規則や、一部の複数定義区間をより深く分割する規則など、第25章のほかの衝突処理はここに残します。サイズのしきい値はヒューリスティックであり、最初の割り当て器へバックポートすべき一般的不変条件ではありません。共有される合法性修正は、引き続き最も早い該当章から始めます。最終監査は第20〜25章の固定隣接ノードの色バイアス、第20〜21章の狭いストアマスク、第21〜25章の ARM レジスタバンク/浮動小数点メモリエンコーディングを修正しました。 + +第21章の隣接コピー転送も引き継ぎます。次の命令だけで使うコピーは、オペランドがコピー元レジスタを受け入れ、命令の結果に結び付けられていなければ除去します。 + +以下は、必須の `cs` フィールドに明示的なコンストラクタを与えて保存済み String ソース3件を修正した後の、完全な2026-10-02再実行です。過去の212項目すべてが成功します。これはフィクスチャ調整と最近のコンパイラ修正後の新たな基準値を確立します。以前の監査との割り当て器だけの比較ではありません。[バックポート記録](../docs/chapter-backports.md) に古い部分比較を残しています。 + +`make spill-stats` を実行してください。各行は **第25章のコンパイラ** を使います。最初の5行は以前の212のプログラム/対象項目を、この章の構文とライブラリ構成に適応して保持しています。[フィクスチャノート](src/test/java/com/seaofnodes/simple/spill/README.ja.md) にその調整と測定の制限を記載しています。最適化器のシードは123です。最後の行には、既存シードで記録した `Chapter25Test` の13割り当てと、ドライバのシード456での新規システムライブラリコンパイルを含みます。`sys.o` を再利用して、この大きな処理対象を測定から消してはいけません。この結果は Windows x86-64 のものです。以前の群には RISC-V/ARM SystemV と x86 SystemV も含みます。報告器は対象ごとの合計も表示します。 + +| プログラム群 | コンパイル数 | 分割/移動数 | ループ重み付き数 | +|---|---:|---:|---:| +| 第20章 | 39 | 375 | 606 | +| 第21章 | 52 | 479 | 1,172 | +| 第22章 | 24 | 103 | 103 | +| 第23章 | 30 | 129 | 353 | +| 第24章 | 67 | 696 | 1,410 | +| 第25章 | 14 | 786 | 1,815 | +| **合計** | **226** | **2,568** | **5,459** | + +`_spills` はレジスタ間移動を含む残存 SplitNode を数え、`_spillScaled` はループ重みを適用します。どちらもメモリアクセスだけを測るものではありません。古い群はレジスタ合法性確認を伴う割り当ての再実行で、ネイティブハーネスの再実行ではありません。`Chapter25Test` の23テストすべてが、ネイティブ/結果確認を含め、スピル期待値の失敗なしに成功します。継承したエミュレータテストを含む章全体には `make -j 4 tests` を使ってください。 + +### 測定についてのコメント + +以前の割り当て器監査では、一様な改善は見つかりませんでした。そのコンパイラのほかの部分を変えず、第24章の順位付けに置き換えると、221のクライアントコンパイルで5移動減りました(2,211/4,724に対し2,206/4,719)。しかしシステムライブラリを8ラウンド以内に割り当てられませんでした。したがって不完全な比較であり、有効なスイート全体の低い合計ではありません。既存の面積/コスト順位付けを保持し、ラウンド上限と正しさの確認も変えません。その監査でシステムライブラリ自体は386移動/855重み付き移動で、現在の再実行は373/828です。 + +複数使用を持つすべての複製可能定数を優先する試行は、それらのクライアントで以前の順位付けより1移動減りましたが、やはり新規ライブラリビルドに失敗し、却下されました。これが、集計統計と完全なコンパイル・実行確認の両方が重要な理由です。有利な小計だけでは変更を正当化できません。 + +その以前の監査では、過去の群の合計が第24章の1,332/2,956から、第25章の1,814/3,753へ増えました。第25章はクラス初期化子、モジュール所有権、異なる脱出解析、構文/ライブラリの調整を追加します。これらは異なるマシングラフなので、増加を割り当て器の品質だけに帰することはできません。章の表から結論を出す前に、1つのコンパイラと固定した入力集合でヒューリスティックを比較してください。 diff --git a/chapter25/README.md b/chapter25/README.md index 7ab7c89e..8beee7b1 100644 --- a/chapter25/README.md +++ b/chapter25/README.md @@ -1,5 +1,7 @@ # Chapter 25: Modules, Separate Compilation, and SSA Construction +English | [日本語](README.ja.md) + [Previous: Chapter 24](../chapter24/README.md) | [Chapter index](../README.md#chapters) diff --git a/chapter25/ROADMAP.ja.md b/chapter25/ROADMAP.ja.md new file mode 100644 index 00000000..222f228a --- /dev/null +++ b/chapter25/ROADMAP.ja.md @@ -0,0 +1,451 @@ +# ロードマップ + +[English](ROADMAP.md) | 日本語 + +この文書は [ROADMAP.md](ROADMAP.md) の日本語訳です。原文の設計案と未解決の問いを、そのまま設計案として記しています。 + +## 分割コンパイル + +未知の前方参照をどう扱うか? H-M は使わないので、単一化も行わない。 +関数以外の前方参照を認めないことにする……関数なら、定義した時点ですぐにシグネチャを提供できる。 + +案 A:FRefNode を削除し、すべてのバグを直す。 +案 A の問題:相互再帰を使えるようにしたい。 +例:`val even = { int x -> x ? odd(x-1) : true; }; val odd = { int x -> x ? even(x-1) : false; };` + +案 B:型は順序どおりに定義する必要があるが、それ以外の関数には順序を要求しない。 +`new T` ならエラーにできるが、`fref.field`、`fref()`、`fref op fref`、`fref[]` ではできない。 + +あらゆる場所で Type.BOTTOM をパーサーの要件に合わせて「引き上げる」必要がある。 +FRef からフィールドをロードするには、FREF を FIELD のある TSTRUCT への TMP に引き上げる必要がある……。 +実際には、FREF が正しくなると局所的に仮定するだけだ。型は任意のコードから来るので、 +アクセスできず、引き上げられない。TMP は nullable かもしれない(後でエラーにすべき)。 +TSTRUCT は無名にでき、無名になるだろう。これは、すべての TSTRUCT を無名にすることを意味する。 + +問題は、未知・遠隔・外部・前方の参照にある。 + +FRef を*少しでも*認めると、すべての処理が FRef 型に遭遇することを考慮しなければならない。 +同じスコープの関数で認めると、上の odd/even の例が使える。 +同じスコープの構造体で認めると、相互再帰的な構造体が使える。 +スコープの外へ昇格させる際には外部の検索を試み、見つからなければ失敗する。 + +任意の使い方の例:`sys.fld`。`sys` の検索に失敗したら、次のどちらかを推測しなければならない。 +前方参照で問題なく、間もなく定義されるのか……それとも今すぐ外部を検索するのか。 +さらに、使い方によっては問題の発覚が遅れる。 +```java +// Extern or Future `sys` being returned. +val foo { -> return sys; } + +// foo() returns FRef, so Bottom, so the `.bar` has no type +// to do field lookups +val main { arg -> foo().bar } + +// HERE: Parser finds `sys` OR NOT?!?!?! +val sys??? = user_stuff....; + +``` + +行き着く先は、パーサーが一切エラーを出さず、盲目的な AST のように SSA を構築し、 +不適切な型だらけのグラフを結線するだけになることだ。 +必要なこと:パーサーを導く型をなくす! 構文だけを使う! + +(明らかな小さな問題:浮動小数点数へ拡張するかどうか。) +修正:たとえば整数演算を挿入する。両方の入力が isa-Int であり、isa-Float に引き上げられなくなるまでは、 +戻り値の型を Int と Float の MEET とする。 + +メソッド呼び出しで起こり得る問題: +`UNKNOWN_STRUCT_BOT.UNKNOWN_FIELD ( ARGS ); // Insert SELF OR NOT?` +— SELF を挿入するか? +修正案:常に SELF 引数があると仮定し、静的と証明できる場合は null にする。 +最適化:定数パラメータをシグネチャから削除するか? *定数*パラメータは、 +*パーサー*の時点で定数でなければならない。そうでなければ未知の呼び出し元がないことが必要になる +(したがって局所的な最適化になる)。 +修正案:SELF 引数を*最初*ではなく*最後*に置くと、不要になった場合に削除しやすいか? + +Load は ConFldOff を捨て、名前を保持する。 +Load はスケーリング前の添字を保持し、添字入力を持つ。構造体ではこの入力は null。 +Ptr Base が*確定*した後で基底の配置を行い、添字の計算を畳み込める。 + +- `Load(ctrl, base, null );` — フィールド名は内部に持つ。 +- `Load(ctrl, base, index_math );` — フィールド名は `[]`。 +- `Store(ctrl, base, null , val);` + +問題:エイリアス情報がない! + +案 C:直接呼び出し `odd()` と型では FRef を認める。それ以外はすべて*引き上げ*が必要。 + +## 構造体と配列の統合 + +構造体のフィールドは名前で索引付けされ、配列の要素は数値で索引付けされる。 +この2つの考え方は両立する、という発想です。 +C 言語では古くからこの考え方を使っています。通常の C の構造体の後に配列を続け、 +通常の配列構文で索引アクセスできます。 + +別の方法として、配列へのポインタをフィールドに保持すると、間接参照が1つ増える代わりに柔軟性が得られます。 +特に、配列を(通常はより大きな配列に)交換できます。 +これは、広く使われる汎用コンテナ `Vector` や `ArrayList` の基礎です。 + +```java +struct str { + int hash; // A cache of the strings hash code + u32 #; // The array length + u8 []; // Array of bytes +}; + +str X = "abc"; +print(X.hash); // Read the hash field +X[0]=='a'; // Read array element 0 +X#; // Length of the array + +``` + +ここでは、`#` というフィールドが配列の長さを保持し、`[]` というフィールドが実際の配列です。 +メモリ内では、この配列がほかのフィールドの直後に続きます。 +可変長配列の別の例を示します。 + +```java +struct vecInt { + u32 size; // Actual number of elements + u32 #; // The array length or capacity + int []; // Array of integers. +}; +``` + +明らかな制約として、配列のサイズを直接変更することはできません。 +先頭のフィールドと同じメモリブロックに実際に配置されているからです。 +このような `vecInt` を、より大きい新しい `vecInt` に*再割り当て*することはできますが、 +元の `vecInt` は解放されます。同じ `vecInt` の参照を永遠に使い続けることはできません。 + +名前付きフィールドはいくつあってもかまいません。 + +```java +struct Class { + !str !className; + !str !professor; + int time; + int credits; + !Class !prerequisite; + u16 #; // Limit of 65535 students per class + Student []; +}; +``` + +自己参照も使えます。 + +```java +struct NTree { + Some val; + u8 #; // Length is 0-255 + NTree []; // More NTrees +}; +``` + +構文規則は次のとおりです。 + +- 最後のフィールドの名前は `[]` で、どの型でもよい。 +- 最後から2番目のフィールドの名前は `#` で、配列の最大長を表す符号なし整数型でなければならない。 +- ` []` の前に空白が必要になるかもしれない。 + +### コンストラクタと配列 + +割り当てには配列の長さを含め、コンストラクタも含められます。 +`new NTree{val=17;}[2];` + +コンストラクタは省略できますが、配列の長さは省略できません。 + +配列を初期化するための任意の関数を使うと、一度だけ書き込む final 配列を作れます。 +`new u8[buffer#,{ u32 idx -> buffer[idx].toUpperCase() }]` + +### 配列の最大長の変更 + +範囲チェックのある小さな配列を大量に使う場合、配列の長さのために丸々4バイトを確保すると、 +配列のオーバーヘッドが大きく増えることが問題になります。 +Simple では最大長を小さくできます。ただし、最大長が異なる配列は別の型として扱います。 + +```java +struct x0 { u8 #; u8[]; }; // Array limited to 0-255 length +struct x1 { u16 #; u8[]; }; // Array limited to 0-65535 length +struct x2 { u32 #; u8[]; }; // +struct x3 { u64 #; u8[]; }; // + +x0 x = "abc"; // 4 bytes total: [3, 'a', 'b', 'c'] +x1 y = "def"; // 6 bytes total: [3, 0 , 'd', 'e', 'f', 0,]; 1 pad byte to acheieve 2-byte alignment +x = y; // ERROR, "x1 is not x0", no auto-widening +x += y; // OK, 7 bytes total: [6, 'a', 'b', 'c', 'd', 'e', 'f'] +y += x; // OK, 8 bytes total: [6, 0 , 'd', 'e', 'f', 'a', 'b', 'c'] + +x += "VeryLongString.... ....255_chars"; // Runtime RangeCheck + +// Explicit conversion to widen string +y = "" + x; +``` + +### 拡張可能な配列・文字列の2つの形:間接参照を増やすかどうか + +間接参照を1つ増やすと、Java の `ArrayList` や C++ の `Vector` のようになります。 + +増やさない場合、拡張可能な配列を伸ばす呼び出しは、新しいオブジェクトを返す可能性があります。 +拡張は必要な場合だけ行います。 + +```java +val vec1 = new vecInt[0].add(2); // len=1, capacity=1 +val vec2 = vec1.add(3); // NEW !vecInt !returned, only is deleted; len=2; capacity=2; +vec1[0]; // ERROR, vec1 has been freed by call to `vec1.add(3)` +vec2[0]; // OK +vec2[1]; // OK +vec2[2]; // Runtime error, AIOOBE +``` + +## 文字列の実装 + +既定の文字列は「str」「cstr」「ustr」などの名前を持ちます。 + +常に unicode8 を使い、u8 を明示的に操作します。 +Unicode の「文字」は、基礎となる実装の上に関数呼び出しとして提供します。 +おそらく別の文字列クラスで実装することになります。 + +## 既定の文字列 + +連結によって効率的に拡張でき、さまざまな IO/printf 関数をサポートします。 +明示的に要求すれば、基礎となる配列を可変な状態で共有できます。 +既定では常に、非共有時は可変、共有時は読み取り専用です。 + +```java +struct str { + + // Add a string to the string, possibly growing + val add = { str str -> + val self = this; + for( int i=0; i<str#; i++ ) + self = self.add(str[i]); + return self; + }; + + // Add an char to the string, possibly growing + val add = { u8 e -> + if( len >= # ) return copy(# ? #*2 : 1).add(e); + [size++] = e; + return this; + }; + + // Copy 'this' to a new larger size + val copy = { int sz2 -> + val v2 = new str[sz2]; + for( int i=0; i<len; i++ ) + v2[i] = [i]; + v2.len = len; + sys.libc.free(this); // Needs a better Mem Management solution + return v2; + }; + + val print { -> sys.libc.write(1/*stdout*/,[]/*array base as argument to C*/,len); return this; } + + u32 len; // In-use size + u32 #; // Max length is 4Gig, although variants can request smaller lengths + u8 []; // Character data +}; +``` + +### Final な文字列 + +**不変**な文字列を意図しており、**安全な**文字列の基礎にも使えるかもしれません。 +一度に全体を作成する必要があり、おそらく防御的コピーも必要になります。 + +```java +struct sstr { + // Possible static call syntax; no references to "this" + val make { str str -> new sstr[str#,str.at] }; + + // Array contents are immutable. + // Still figuring out good syntax for this. + u8 #; + u8 [~]; // Middlin '~'? Makes primitive array contents *immutable* and requires constructor syntax +}; + +// Construction requires a function that produces the array contents. +// Here we are defining a defensive copy over `buf`. +sstr safe = new sstr[buf.len,{ int idx -> buf[idx]; }]; +sstr safe = sstr.make("abc"); // Defensive copy is made +``` + +### 拡張可能な文字列(StringBuilder または StringBuffer) + +この API は `StringBuilder` スタイルの代替としてそのまま使えることを意図しており、 +使うたびに間接参照が1つ増えます。 + +```java +struct xstr { + !str !_str; // The extra indirection is here + val add = { str str -> _str = str.add2(str); }; + val write = { int fd -> _str.write(fd); }; +}; +``` + +### Unicode 文字列 + +この API は、通常の `str` の上で Unicode の文字を操作できるようにすることを意図しています。 + +```java +struct ustr { + // Assuming idx is at the start of a character, return the character + // as a 32b integer. Returns junk integer if not at a character start. + // For all ASCII strings this will amount to a simple byte load. + // For bytes with high order bits set, this may read 1-4 characters. + val at = { int idx -> ... }; +}; +// Unicode character iterator +struct ucharator { +// Return successive unicode character codes +}; +``` + +### 「C」文字列 + +ゼロ終端の文字列ですが、長さを直接取得できる必要があります。 + +```java +struct cstr { + u8 #; // length including the trailing zero + u8[]; // always ends in a zero byte; suitable for direct passing to C string functions +}; +``` + +### 「german string」:レジスタに格納できる非常に短い文字列 + +メモリ内では、長さを表す1バイト(3ビット?)に続いて0〜7文字を格納し、最大8文字です。 +64ビットのレジスタに収まります。 + +```java +struct gstr { // aka germanString, or blend string and register "streg" + u3 #; // Size is 0-7 bytes??? + u8 []; // Up to 7 bytes +} +// Using inlined object (see below). All these strings are short and are +// represented as packed integers in a 64b register. +*gstr prize1 = "gold"; +*gstr prize2 = "silver"; +*gstr prize3 = "bronze"; + +``` + +### Main は `val main = { str[] args -> ...}` になる + +## 構造体のインライン化 + +構造体は通常、参照として扱いますが、「内容(contents of)」と読む `*` 演算子を使って +値として扱う形へ変換できます(この名前は、たとえば `inline` に変えるかもしれません)。 + +これは名前空間の変更だけです。内部の構造体の寿命を完全に消費しない限り、そのアドレスは取得できません。 +「外部への流出」は認めません。 + +```java +// 2 64-bit words, no other overheads +struct Complex { f64 x,y; val len = { ->Math.sqrt(x*x+y*y); }; }; + +// Current Simple rules: always by-reference +struct ByRef { + !Complex !c; // 4-byte pointer to a Complex +}; +print(new ByRef.c.y); // Lookup requires 1 extra memory load from ref to c + +// New behavior: by value (inlining a struct) +// The '*' syntax indicator can be something else, e.g. a keyword "inline" +// I am pronouncing '*' as "contents of" +struct ByValue { + *!Complex !c; // c is inlined, full 16 bytes into ByValue +}; +print(new ByValue.c.y); // x,y inlined into ByValue, no extra memory load + +// Mixing Refs and Struct Values. Basically, a ref can be converted +// to a value by taking the "contents of" the ref, and values cannot +// be *converted* to a ref, although they can be copied over a ref. +ref. c = val.c; // Error, mixing refs and values +ref.*c = val.c; // Allowed, whole structure copy. Does not allocate, requires ref.c not-null +val. c = ref.c; // Error, mixing refs and values +val. c = *ref.c; // Allowed, whole structure copy +ref. c = ref.c; // Allowed, pointer copy +val. c = val.c; // Allowed, whole structure copy. +val *c = val.c; // Error, cannot take "contents of" a value + +// Alternative syntax, to avoid the "contents of" being on LHS and field applying to being on RHS: +ref.c = val.*c; // Assign into ref.c the "contents of" val.c + +// Arrays of inlined structures +var ary = new *Complex[99]; // Array of 99 !Complex !objects, inlined + +// Calling methods has the same syntax +ref.c.len(); +val.c.len(); + +// Function arguments use the same typing, so can pass-by-value +val math.sin = { *Complex c -> ... }; // Passes a complex by value +math.sin(ref.*c); // Requires de-reference to pass by value +math.sin(val. c); // Pass by value + +``` + +## ほかの章のロードマップ + +`&&` と `||`。 +`int` を `i32` に縮小する。 +`TypeMemPtr` を4バイトに縮小する。`mmap` を使い、ヒープを下位4Gの領域に置く。 +パーサーの既定の for ループ構築、または部分的なループ剥離。 +`for( init; test; next ) body` は、次のようになる。 +```java +{ init; // Normal scope entry to bound lifetime of index variable +if( !test ) { // Zero-trip count test + do { + body; + next; + } while( test ); // Exit test at loop bottom by default +} }; +``` + +## `&&` と `||` + +論理演算子 &&(AND)と ||(OR)は短絡評価を行います。 +最初の条件だけで結果が決まるときは、第2の条件の評価を省略できます。 + +- `&&` は最初のオペランドが偽なら評価を止めます。 +- `||` は最初のオペランドが真なら評価を止めます。 + +これらは制御フローを読みやすくする糖衣構文であり、実行モデルに新しいノードを作りません。 + +`&&` と `||` の構文は、制御フローだけを変える単純な糖衣構文であり、新しいノードを導入しません。 + +次の例を考えます。 +```java +int a = 1; +int b = 0; + +if(a++ || b++ ) { + if(b == 0 && a == 2) { + sys.io.p("Or"); + } +} else{ + sys.io.p("And"); +} +return 0; +``` + +- `a++` は1(真)と評価されるので、`b++` は評価されません。 +- その結果、`b` は0のままで、`a` は2になり、「Or」が表示されます。 + +```java +int a = 1; +int b = 1; + +int x=1; +int y=1; +int z=0; + +int g = x++ && y++ && z++; +... +``` + +この例では、次のようになります。 + +- `x++` は真(1)なので、`y++` を評価します。それも真です。 +- 続いて `z++` を評価します。これは偽(0)です。 +- `&&` はすべての条件が真であることを要求するため、最終結果 `g` は0になります。 +- 偽になるオペランドまでのすべてのオペランドを評価するため、`x`、`y`、`z` はすべてインクリメントされます。 diff --git a/chapter25/ROADMAP.md b/chapter25/ROADMAP.md index 30495a69..749a1614 100644 --- a/chapter25/ROADMAP.md +++ b/chapter25/ROADMAP.md @@ -1,5 +1,7 @@ ## Separate Compilation +English | [日本語](ROADMAP.ja.md) + How to handle unknown forward ref? No H-M, so no unification. diff --git a/chapter25/docs/18-grammar.ja.md b/chapter25/docs/18-grammar.ja.md new file mode 100644 index 00000000..758632be --- /dev/null +++ b/chapter25/docs/18-grammar.ja.md @@ -0,0 +1,137 @@ +# 第25章の文法 + +[English](18-grammar.md) | 日本語 + +この文書は [原文](18-grammar.md) の日本語訳です。 + +> 訳注: 原文の見出しは「Grammar for Chapter 16」です。この日本語版では、配置されている第25章に合わせた見出しにしています。 + +[第25章に戻る](../README.ja.md) + +```antlrv4 +grammar SimpleLanguage; + +program : block EOF ; + +block : statement+ ; + + +statement + : '{' block '}' + | returnStatement + | ifStatement + | whileStatement + | breakStatement + | continueStatement + | structDeclaration + | expressionStatement + ; + + +returnStatement : 'return' expression ';' ; + +ifStatement : 'if' '(' expression ')' statement ('else' statement)? ; + +whileStatement : 'while' '(' expression ')' statement ; + +breakStatement : 'break' ';' ; + +continueStatement : 'continue' ';' ; + +structDeclaration : 'struct' IDENTIFIER '{' block '}' ; + +expressionStatement + : type IDENTIFIER ';' + | type IDENTIFIER '=' expression ';' + | IDENTIFIER '=' expression ';' + | expression + ; + +PRIMTYPE + : 'int' + | 'i8' + | 'i16' + | 'i32' + | 'i64' + | 'u8' + | 'u16' + | 'u32' + | 'flt' + | 'f32' + | 'f64' + | 'bool' + ; + +type + : PRIMTYPE + | typeName '?'? + | typeName nestedArray+ + ; + +nestedArray : '?'? '[]'* ; + +typeName : IDENTIFIER ; + + +expression : bitWiseExpression ; + +bitWiseExpression + : comparisonExpression ( '&' | '|' | '^' ) comparisonExpression)* + ; + +comparisonExpression + : shiftExpression (('==' | '!='| '>'| '<'| '>='| '<=') shiftExpression)* + ; + +shiftExpression + : additiveExpression ( '<<' | '>>>' | '>>' ) additiveExpression)* + ; + +additiveExpression + : multiplicativeExpression (('+' | '-') multiplicativeExpression)* + ; + +multiplicativeExpression + : unaryExpression (('*' | '/') unaryExpression)* + ; + +unaryExpression + : ('-') unaryExpression + | '!' unaryExpression + | primaryExpression postAssign + ; + +primaryExpression + : INTEGER_LITERAL + | FLOAT_LITERAL + | '(' expression ')' + | 'true' + | 'false' + | 'null' + | newExpression + | IDENTIFIER + ; + +newExpression + : 'new' IDENTIFIER [ '{' block '}' ] + | 'new' IDENTIFIER '[' expression ']' + ; + +postAssign : postFix [ '=' expression ] | [ '#' ]; + +postFix + : '.' IDENTIFIER postFix + | '[' expression ']' postFix + ; + + +IDENTIFIER : NON_DIGIT (NON_DIGIT | DIGIT)* ; + +INTEGER_LITERAL + : [1-9]DIGIT* + | [0] + ; + +NON_DIGIT: [a-zA-Z_]; +DIGIT: [0-9]; +``` diff --git a/chapter25/docs/18-grammar.md b/chapter25/docs/18-grammar.md index 2c98f562..9176340a 100644 --- a/chapter25/docs/18-grammar.md +++ b/chapter25/docs/18-grammar.md @@ -1,5 +1,7 @@ # Grammar for Chapter 16 +English | [日本語](18-grammar.ja.md) + ```antlrv4 grammar SimpleLanguage; diff --git a/chapter25/module.ja.md b/chapter25/module.ja.md new file mode 100644 index 00000000..2746ad1e --- /dev/null +++ b/chapter25/module.ja.md @@ -0,0 +1,275 @@ +# Simple のモジュール + +[English](module.md) | 日本語 + +この文書は [Modules for Simple](module.md) の日本語訳です。原文はモジュールの設計案です。 + +目標: + +- 有効なプログラムを1行で書けるようにする(チュートリアルや初心者の導入を簡単にする)。 + - 最小限の手間でコンパイルして実行する。 +- 分割コンパイル、名前空間、外部ライブラリを扱う。 + - ELF など、リンク可能・実行可能な形式へコンパイルする。 +- フィールドやクラスを簡単に非公開にする。 +- 出力ファイルを、既存の .o/.exe/lib/dll の仕組みと互換にする。 +- `#include` や `import` 用のファイルを不要にする。 +- モジュールごとに構造・ファイルシステムの階層を持たせる。 + - 複数のファイルで1つのモジュールを構成することも考えられる。 + - モジュール内で簡単に名前空間を使う。 + - 親ファイルまたは入れ子のファイルに、入れ子の名前を置く。 +- クラス初期化子について次を保証する。 + - 正確に1回だけ実行する。 + - 最初にアクセスされた時点で遅延実行する。 + - 最初のインスタンスが作られる前、またはその時点までに完了する。 + - `<clinit>` 内の特別な原型インスタンス(golden instance)は例外として認める。 + +--- +### 目標:最小限の手間で有効な1行プログラムを書く + +現在の作業ディレクトリの名前から、モジュールとクラス `hello` を作ります。 +追加の引数なしでコンパイルすると、リンクと実行も行います。 + +ファイル:`hello.smp` +``` +sys.io.p("Hello World!"); +``` + +コンパイルして実行します。 +``` +> smp hello.smp +Hello World! +``` + +モジュール名とクラス名は、ファイル名から取り出した `hello` です。 +メモリ内でコンパイルし、`exec` で実行します。ファイルは作りません。 +これは、その場でコンパイルして実行するためだけのモードです。 + +--- +### 目標:分割コンパイル + +コンパイル時に出力先のビルドディレクトリを指定すると、`.o` ファイルを得られます。 +出力先ディレクトリの構造はソースと一致し、場所は自由に選べます。 + +前と同様に、コンパイルのコマンドでモジュール名を指定していないため、 +`Pet` がモジュールにもクラスにもなります。 + +ファイル:Pet.smp +``` +// Class initializer <clinit> is any code outside a struct declaration +// and is guaranteed to run-once on first touch. + +// static fields listed here: +int default_leggies = 4; + +// A default (no argument) <init> is code inside struct with same name as the +// file; this is the same behavior as field initializers for Java, although +// any code is allowed, not just expressions. + +struct Pet { + str name; + int leggies = default_leggies; +} + +float yet_another_static_field; +``` + +`build/` ディレクトリ内の `Pet.o` へコンパイルします。 +``` +> smp Pet.smp -o build/ +> ls build +Pet.o +``` + +--- +### 目標:入れ子の名前空間 + +`myRepo` 内のファイル名から、モジュールとクラス `Pet` を作ります。 +入れ子のクラス `Pet.Vet` も作ります。これらを別のモジュール `Owner` から使います。 + +ディレクトリ:`~/myRepo/` +``` +~/myRepo/ + Pet.smp + build/ + +~/otherRepo/ + Owner.smp +``` + +ファイル:`myRepo/Pet.smp` +``` +int default_leggies = 4; + +struct Pet { + str name; + int leggies = default_leggies; +} + +// Nested class Vet inside of file Pet +struct Vet { + str name; + str address; + long credit_card_num; +} +``` + +ファイル:`otherRepo/Owner.smp` +``` +Pet dog = Pet{name="Fido"}; +Pet snake = Pet{name="slither", leggies = 0; }; +Pet.Vet vet = Pet.Vet{name="Linda"; address="101 Main St"; credit_card_num=1234}; + +sys.io.p("I took "+dog.name+" to "+vet.name+"\n"); +``` + +`build/` ディレクトリ内の `Pet.o` へコンパイルします。 +コマンドで別のモジュールのルートを指定していないため、`Pet` がモジュールであり、クラスでもあります。 +`Pet` がモジュールなので、`Vet` はモジュール `Pet` 内のクラスです。 + +``` +> smp Pet.smp -o build/ +> ls build +Pet.o +``` + +`Pet` と `Pet.Vet` のコードは、同じ `Pet.o` ファイルに入ります。 + +自由変数 `Pet` と `Pet.Vet` を解決するために `Pet` のリポジトリを指定して、 +`Owner.smp` をコンパイルし、実行します。 + +``` +> smp Owner.smp -lib ~/myRepo/build +I took Fido to Linda +``` + +--- +### 目標:入れ子のファイル内の入れ子の名前空間 + +前の例と同じですが、ファイルを入れ子にします。 + +``` +~/myRepo + Pet.smp + Pet/ + Vet.smp + build/ + Pet.o + Pet/ + Vet.o + +~/otherRepo + Owner.smp +``` + +ファイル:Pet.smp +``` +int default_leggies = 4; + +struct Pet { + str name; + int leggies = default_leggies; +} +``` + +ファイル:Pet/Vet.smp +``` +// Nested class Vet inside of directory Pet +struct Vet { + str name; + str address; + long credit_card_num; +} +``` + +`Owner.smp` をコンパイルし、実行します。 +``` +> smp Owner.smp -lib ~/myRepo/build +I took Fido to Linda +``` + +--- +### 目標:クラス初期化子の遅延実行 + +``` +Parent.smp +Parent/ + ChildA.smp + ChildB.smp +``` + +ファイル:`Parent.smp` +``` +sys.io.p("In Parent <clinit>"); +sys.io.p(ChildB.str); +``` + +ファイル:`ChildA.smp` +``` +// <clinit> for ChildA +sys.io.p("In ChildA <clinit>"); +str str = "A"; +``` + +ファイル:`ChildB.smp` +``` +// <clinit> for ChildB +sys.io.p("In ChildB <clinit>"); +str str = "B"; +``` + +コンパイルして実行します。 +``` +> smp Parent.smp +In Parent <clinit> +In ChildB <clinit> +B + +``` + +### 目標:簡単な非公開フィールド + +名前の先頭にアンダースコアを付けると、フィールドやクラスが非公開になります。 + +### 目標:C コンパイラのドライバに似た構文 + +``` +Simple --cpu x86_64_v2 --abi Win64 --norun -o lib/lib/sys_x86_64_v2_win64.o src/main/smp/sys.smp +``` + +モジュールを指定していないため、最上位のファイルがモジュール `sys` になります。 +モジュールディレクトリも指定していないため、ファイル名のディレクトリ部分 `src/main/smp` になります。 +ビルドディレクトリを指定していないため、出力ファイルのディレクトリ部分 `lib/` になります。 +出力ファイル名の既定値は `smp.o` ですが、ここでは別名を指定しています。 + +ある `<clinit>` は別の `<clinit>` を参照でき、最初のアクセス時に実行されます。 +設計上、子より先に親へアクセスする必要があります。ただし、子 A は子 B を参照できます。 +Parent.B へのアクセスは、次を意味します。 + +- 最初に Parent にアクセスし、`<clinit>` を開始する。 +- そこからアクセスするものにも、A や B を含め、再帰的に `<clinit>` を実行する。 +- Parent の処理が完了する。 +- `<clinit>` 内では別の*モジュール*を参照できない。 +- 静的関数は別のモジュールを参照*できる*が、その場合、実行される可能性がある……。 + +目標:モジュール内の `<clinit>` を決定的にする。 + +- 背景:複雑な `<clinit>` の順序のせいで、子が使う時点で参照先の static final フィールドがまだ初期化されていないことが、何度もあった。 +- 副目標:親が子より先になるなど、`<clinit>` の*順序*を決定的にする。 + - 問題:Parent の `<clinit>` の途中で Child を参照すると、Parent が完了する前に Child の `<clinit>` が走る。 + - 部分的な回答:親が先で「親は子を知っている」ため、これはよい。 + - 追加の問題:モジュールをまたぐと成り立たない。あるモジュールは別のモジュールの親ではない。 + - 問題:`<clinit>` 中に呼び出す Parent の関数が、非決定的に子を初期化できる。 + - `fcn = { -> rand ? A.fld : B.fld }` +- 回答案:Parent の `<clinit>` は、直接にも間接にも子を参照しては*ならない*。 +- 例:`B b = new B; // Illegal ref to child in Parent.<clinit>` + — Parent の初期化子から子への不正な参照。 +- 例:`B? b = fcn(); fcn = { -> rand ? null : new B; } // Illegal call to fcn referring to B inside Parent <clinit>` + — Parent の初期化子内で B を参照する関数への不正な呼び出し。 +- 例:`B? b; fcn = { -> new B; } // OK, no B.<clinit> during Parent.<clinit>` + — Parent の初期化中に B の初期化子を実行しないため、これは許される。 + +目標:外部とのリンクでも、1回限りの初期化機能を使えるようにする。 + +回答案:外部から `Foo.foo()` を呼ぶときは、まず `Foo.<clinit>` が実行済みであることを確認してから、 +`Foo.foo()` を呼び出す。任意の最適化として、成功後は呼び出し元を変更し、 +`<clinit>` の確認を省略する。 diff --git a/chapter25/module.md b/chapter25/module.md index d7bed18c..4cd801e7 100644 --- a/chapter25/module.md +++ b/chapter25/module.md @@ -1,6 +1,8 @@ # Modules for Simple +English | [日本語](module.ja.md) + Goals: - 1-liner valid program (for easy tutorial/newbie starts) - - Compile and run with minimal effort diff --git a/chapter25/src/test/java/com/seaofnodes/simple/spill/README.ja.md b/chapter25/src/test/java/com/seaofnodes/simple/spill/README.ja.md new file mode 100644 index 00000000..5ae019dd --- /dev/null +++ b/chapter25/src/test/java/com/seaofnodes/simple/spill/README.ja.md @@ -0,0 +1,50 @@ +# 過去の割り当てコホート + +[English](README.md) | 日本語 + +この文書は [Historical allocation cohorts](README.md) の日本語訳です。 + +`cohorts.tsv` は、第24章の `SpillStats` で測定した212件のコンパイル項目を固定したものです。 +コホート20〜24の項目数は、それぞれ39、52、24、30、67です。 +ソースファイルは内容で重複を除き、最初に参照したテストの名前を付けています。 +マニフェストには、テストの識別情報、ターゲット、ABI、元の停止フェーズを保持しています。 +`Win64` は、第25章で使う ABI の表記 `win64` に変更しています。 +引数の型は i64、再実行のシードは全項目で123です。 + +これらの入力は、この最終監査で新たな正しさの回帰テストを追加する前に、 +第24章のテストから取得しました。比較のための安定したコーパスです。 +変更する場合は、比較可能性が失われる理由を説明する必要があります。 +人工的なアロケータの回帰テストや命令の回帰テストでの割り当ては、これらのコホートには含めません。 + +第25章では、以前のソースをすべて無変更で解析できるわけではありません。適応のための変更を明記します。 + +- String、Scan、連結された S のコンストラクタでは、`new S { field=...; }` を、 + 宣言されたコンストラクタとその引数に置き換えています。 + レシーバーを明示するスキャナ関数は、操作を保ったままインスタンスメソッドにしています。 +- 保存した `Chapter21Test-testStringExport`、`Chapter21AllocTest-testString`、 + `Chapter23AllocTest-testString` のソースは、必須の非 null フィールド `cs` を初期化するために、 + `new String = { u8[] data -> cs=data; };` を宣言しています。 + これによって10件の再実行項目(4件、3件、3件)を修正していますが、 + 等価性・ハッシュの本体やマニフェストの構成項目は変えていません。 + これらの適応済みソースが新しい比較基準です。以前のコンストラクタなしのフィクスチャと + 同一入力として、その集計値を比較することはできません。 +- 短絡評価の右辺で初期化する2つのループは、同等のガード付きブロックにしています。 + 割り当て、副作用、最終結果を保持しています。 +- 単純な I/O のケースには、以前の `write` の束縛と表示用の補助関数を直接含めています。 + Bubble Sort には、名前空間の接頭辞を平坦化し、コンストラクタを明示し、 + 置換用バッファを可変にした、以前のライブラリの本体を含めています。 + これにより、第25章で大幅に改訂した例に差し替えるのではなく、以前のワークロードを保持しています。 +- 元の Newton の浮動小数点数版2種類を含め、ほかのソース本文は保持しています。 + +すべての項目を **RegAlloc まで**再実行し、スケジューリング済みのレジスタ制約を確認します。 +過去のフェーズの列は来歴情報であり、第25章で以前のネイティブ実行ハーネスを動かすことを約束するものではありません。 +適応済み Bubble Sort は、割り当ての比較可能性のために、過去の `i32 errno="C"` 宣言を保持しています。 +直接のデータ束縛は現在、3つのターゲットすべてでエンコードできますが、 +errno がスレッドローカルまたはアクセサ経由の libc とは、この宣言をリンクできません。 +現在のシステムライブラリのコードは、代わりに `libc.errno()` を使います。 +このコーパスを、過去のランタイムの実行カバレッジとして数えてはいけません。 + +その後 `SpillStats` は、シード456で現在のシステムライブラリをエンコーディングまでコンパイルし、 +既存のシードとネイティブ検査を使って Chapter25Test を実行します。 +これらを合わせてコホート25とします。ライブラリまたはテストが失敗すると、レポートも失敗します。 +部分的な集計値では改善を示せません。現在のコンパイラの実行検証は、通常の章全体のテストスイートが担います。 diff --git a/chapter25/src/test/java/com/seaofnodes/simple/spill/README.md b/chapter25/src/test/java/com/seaofnodes/simple/spill/README.md index 57435402..3a064486 100644 --- a/chapter25/src/test/java/com/seaofnodes/simple/spill/README.md +++ b/chapter25/src/test/java/com/seaofnodes/simple/spill/README.md @@ -1,5 +1,7 @@ # Historical allocation cohorts +English | [日本語](README.ja.md) + `cohorts.tsv` freezes the 212 compilation entries measured by Chapter 24's `SpillStats`: 39, 52, 24, 30, and 67 entries for cohorts 20-24. The source files are deduplicated by contents and named for their first referring test. The |
