State of Valhalla – Section 4: Translation

原文はこちら。
The original article was written by Brian Goetz (Java Architect, Oracle).
http://cr.openjdk.java.net/~briangoetz/valhalla/sov/04-translation.html

この文書では、インラインクラス、インライン型の操作をJavaソースコードからJavaクラスファイルへどのように変換されるかを説明します。

Section 1: The Road to Valhalla英語日本語
Section 2: Language Model英語日本語
Section 3: JVM Model英語日本語
Section 4: Translation英語日本語

Class declarations

インラインクラスの宣言では、スーパークラス(Objectもしくは適切な抽象クラスのいずれか)とスーパーインターフェースを持つことができます。

class C extends A implements I { }

コンパイル時に、2個のクラスファイル(ソースレベルの型C.refとC.valに対応するC$refとC$val)が生成されます。

[ PermittedSubtypes(C$val.class) ]
[ NestMembers(C$val.class) ]
abstract sealed class C$ref
    extends A implements I
    permits C$val {
}

[ NestHost(C$ref.class) ]
ACC_INLINE class C$val extends C$ref {
}

さらに、型Cを生成します。これは上記の型の一つへのエイリアスです(Cがref-defaultもしくはval-defaultなのかによって変わります)。

ここでシーリングの利用とは、クラスC$refの任意の値がC$valの値への参照かnull参照であることを意味します(事実上、C$refは識別値nullを伴うC$valの随伴(adjunction)です)。さらに、生成された2個のクラスはnestmateゆえ、お互いのプライベートメンバーにアクセスできます。

Members

Cで宣言されたメンバーは、C.refとC.valの両方のメンバーです。クラス宣言のメンバーを射影クラスのメンバーに変換するいくつかの方法が考えられますが、今回は、重複やブリッジメソッドのようなアーティファクトの変換の必要性を最小化し、将来の互換性問題を最小限に抑える、VMが既に実施していることを受け入れるアプローチを選択します。このアプローチは状態関連のメンバー(フィールドやコンストラクタ)を値射影に、動作関連のメンバー(メソッド)を参照射影に分類します。フィールドは暗黙のうちにfinalで、ソースレベルでのコンストラクタはクラスファイルのファクトリメソッドにマップされます。

inline class C {
    private int x;

    public C(int x) { this.x = x; }

    public int x() { return x; }
}

これを変換すると、メンバーは以下のように分類されます。メソッド本体の変換にて種々の調整が入っています。

abstract sealed class C$ref permits C$val {
    public int x() { return ((C$val) this).x; }
}

ACC_INLINE class C$val extends C$ref {
    private final int x;

    public static void <new>(int x) {
        defaultvalue C$val
        iload_0
        withfield C$val.x:int
        areturn
    }
}

静的メンバーは参照射影に変換されています。

Type uses and member access

ほとんどの場合、フィールドディスクリプタやメソッドディスクリプタなどでは、C.refの利用はLV$refとして変換され、C.valの利用はQC$valとして変換されます。そしてCの利用はCがref-defaultもしくはval-defaultのインラインクラスとして宣言されているかに基づいて、そのどちらか(LV$refもしくはQC$val)に変換されます。

C$valの全てのインスタンスメンバーへのアクセスと、C$refのメソッドアクセスは通常通り変換されます(getfield、invokevirtualなど)。C$refを使ったフィールドアクセスは、まずC$valにキャストし(C$refとC$valの2つは同じアクセシビリティを有します)、続いて値射影を使ったフィールドアクセスにより変換されます。C$refのインスタンス化は、C$valのインスタンス化に続いてC$refへのキャストが行われるものとして変換されます。C$valを使った静的メンバーへのアクセスは、C$refへの展開してそこのメンバーにアクセスすることにより変換されます(これは静的フィールドも取り扱います)。プライベートメソッドの場合も(継承されないため)同様です。これらの変換のアーティファクトはユーザーに対して透過的です。ユーザーは両方の射影を、Cで宣言された全てのメンバーを持っているものとして認識できます。

以下の2つのゴールを達成するために、この変換スキームを選択しました。

  • クラスファイルでの(抽象メソッドやブリッジメソッドなどの)メンバーの重複を最小化すること
  • val-default 対 ref-default(非修飾名の意味を除く)や、新たなバイナリ互換の制約を生み出す可能性がある別の特性をの変換決定の必要条件にしないこと

Conversions and subtyping

値射影と参照射影間の関係は、言語モデルとVMモデルへの変換との間では異なります。言語モデルでは、値射影は参照射影のサブタイプではありませんが、両者(値射影と参照射影)はインライン縮小・拡大変換が関係していますが、VMモデルでは、実際のサブタイプ化が関係しています。

意図的にVM内のように言語内で異なるモデルを選択したのは奇妙に思えるかもしれませんが、見ての通り、この選択によって言語の制約が言語内に残り、VMに対し、効率的な翻訳ターゲットであることを主眼にさせることができます。

インライン縮小・展開変換は意味論上、よく知られているアンボクシング・ボクシング変換に類似 していますが、間接参照(indirection)、割り当て(allocation)、偶発的な同一性(accidental identity)といった、ボクシングに関連するネガティブなパフォーマンスのコストの多くを共有しません。C$valからC$refに変換するとき、これは言語レベルでは変換とみなされますが、この変換の実際のバイトコードを発行する必要はありません。それはVMが既にC$valをC$refのサブタイプとして認識しているからです。逆に、C$refをC$valに縮小すると、言語コンパイラはcheckcastバイトコードを発行します。しかしVMはC$valのみを許容するようC$refがシールされていることを知っているため、これをnullチェックに弱める、もしくは最適化により完全に除去できます。それゆえ、言語レベルでボクシング・アンボクシングのように見えているものは、VMでの何もやらない、もしくは何もやらないに近い状態になってしまいます。

それでもなお、言語レベルでC$valとC$refの間のサブタイプを単純に公開することを選ばなかったのは、言語レベルでプリミティブとインラインを統一する道筋を残したかったからです。現在のところこの言語では、アドホックなボクシング・アンボクシングによってintとIntegerが関連しています。言語モデルで概説したように、プリミティブを通常のインラインクラスに移行し、レガシーなボックス型がこうしたクラスへの参照投影としたいと考えています。プリミティブの異なる2種類のボックス型(レガシーかつヘビーなボックス型と、新しい軽量なボックス型、前者はボクシングによって関連するもの、後者はサブタイプによって関連するもの)を有する点は永久的な欠点になるとともに、新たな2番目の(サブタイプという)変換セットを有することは、既存のオーバーロードの選択決定の変更は相容れない可能性があります。この欠点を取り除くために、intとInteger間の変換セマンティクスを残し、レガシーのボクシング変換が単なる通常のインライン展開・縮小となるよう、この変換セマンティクスを全てのインライン展開・縮小変換に拡張します。

State of Valhalla – Section 2: Language Model
http://cr.openjdk.java.net/~briangoetz/valhalla/sov/02-object-model.html
https://logico-jp.io/2020/02/27/state-of-valhalla-section-2-language-model/

さらに、ボクシングの関係性は型推論とオーバーロードの選択の役目を果たしています。ボクシングのセマンティクスに一致するインライン展開・縮小セマンティクスを選択することにより、これらの複雑で浸透した言語機能は、プリミティブを特別に扱うことなく、インラインクラスに拡張しても、ユーザーが期待する通りに機能し続けることができます。

単にボクシングからインライン展開に名前を変換するという修辞的なトリックを使っているように思われるかもしれません。しかし、インラインクラスとその参照投影のレイアウト、インスタンス化、呼び出し規約を日常的に最適化することができるという、新たな変換対象を有するようになった、という違いがあります。(パフォーマンスを除き)何も変更していない、という錯覚に陥っていますが、慣れ親しんだ言語セマンティクスを効率的な変換対象上に移植しただけです。

Primitives

言語モデルで詳説したように、この話の主要なゴールは、プリミティブの性能特性を保持しつつ、プリミティブが「ほとんどただの」インラインクラスになるよう、プリミティブを移行することです。プリミティブと参照型の現在の分断は、インラインクラスとアイデンティティクラスの分断に移行します。ここで、各ドメイン(と両者間の関係)のルールは、多くの場合現在の分断の対応する側のルールと同じです。この移行は段階的に行われることでしょう。

まず最初は、この言語(Java)が、暗黙のうちに宣言されたインラインクラスの値の射影であるかのようにプリミティブを扱い(例:java.lang.int)、そしてそのラッパークラスを、そのインラインクラスの参照投影であるかのように扱います(int.refがIntegerのエイリアスになり、Integer.valがintのエイリアスになります)。これにより、型推論やオーバーロード選択、型結合の適合性(type bound conformance)、オートボクシングなどのルールが、現在のプリミティブの扱い方と完全に一致した方法で、プリミティブとインラインを統一的に扱うことができるようになります。

後に、intをインターフェースやメソッドを持つ(特別な)クラスとしてjava.langで実際に宣言できるかもしれません。最終的に、ジェネリクスの話が完結すると、intはComparableを実装する可能性があります。

当然ながら、JVMは引き続き直接プリミティブをサポートします(例:Iキャリアやi*命令など)。言語コンパイラには(例えばフィールドやメソッド記述子で)有益な場合や移行互換性上必要な場合に、生成されたコードの中で I キャリアやi命令を使用し、その他の場所ではQjava/lang/intを使用するという自由度を有します。

このスキームには1個の振る舞い上の非互換な変更があります。プリミティブのラッパークラスの1個のインスタンス上での同期によって、IMSE(IllegalMonitorStateException)がスローされる、というものです。これを軽視しているわけではありませんが、ほとんどの場合、この同期化はエラーであり、これが完全なエラーではないごく少数のケースにおいては、簡単な回避策が存在します。

Reflection

Q-Worldでは、JVMが参照射影を機械的にインラインクラスファイルから導出していました。そのため、JVMは参照射影の合成ミラーも作成する必要がありました。値射影と参照射影が実際のクラスファイルになったので、実際のクラスミラーを使ってほとんどのリフレクション関連の作業を実施できます。

リフレクションAPIを拡張し、インラインクラスのより多くの情報(このクラスはインラインクラスなのか、とか、参照射影とは何か、など)を提供したいと思うかもしれません。また、ミラーの1個もしくは両方のリフレクションの挙動を調整し、より言語に近い挙動をするようにしたい、と思うケースがあるかもしれませんし、あるいは、単純にリフレクションに変換モデルを公開させるだけかもしれません。

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Google フォト

Google アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中