原文はこちら。なお、2023/08/17現在の情報に基づきます。
The original article was written by Brian Goetz.
https://openjdk.org/projects/leyden/notes/04-condensing-bootstraps
condensation(凝縮)のターゲットの1つはindy(とcondy)のbootstrapで、これらのbootstrapの作業を最初の使用時からビルド時にシフトします。(bootstrapはすでにビルド時から実行時にシフトされた計算を表していることに注意してください。したがって、condensationは効果的にこの作業をシフトバックします)。すべてのbootstrapがビルド時にシフトバックできるわけではなく、いくつかは実行時にのみ利用可能な情報や操作に依存する可能性があります。しかし、javacが使用するbootstrapは、一般的に、メソッドハンドルの最も動的な特徴である実行時依存性を回避します。そしてこれにより、このような再シフトの候補になりそうです。
これまでに、コンデンサーがbootstrapをビルド時にシフトさせる方法について、以下の2つの方向性を探ってきました。
- よく知られたbootstrapごとに手作業でビルドしたコンデンサーを用意する
- bootstrapをビルド時に評価する
この文書では、次の第3の選択肢を提示します。
- プログラミングモデルにbootstrapのシフトをより直接的に組み込む
Approach #1: Bespoke condensers
最も明白なアプローチは、シフトしたいbootstrapごとにコンデンサーを記述するというもので、これには、bootstrapが仕様を持つという利点があり、コンデンサは意味保存しながら変換を確実にするためにその仕様に依拠できます。そのようなコンデンサーの記述は通常は簡単で、これを行うbootstrapはそれほど多くありません。
これは「仕事を終わらせる」ための合理的な作業量のように思えますが、代償もあります。それぞれのbootstrapのために別々のコンデンサーを書く作業は別として、それは将来のメンテナンスのための技術的負債を作ることになります。別々のbootstrapとコンデンサーを持つということは、それぞれのbootstrapの2つの異なるプログラミング・モデルを使って、オンライン用とオフライン用の2つの別々の実装を持つことを意味します。そして今後も同期を保つ必要がありますが、残念ながらこれは難しいものです。一方を更新し、もう一方を更新するのを忘れるのは簡単であるだけでなく、同時に更新する場合でも、それぞれが使用するプログラミング・モデルの間の概念的なギャップは、微妙なバグが入り込むことを事実上保証しています。bootstrapをメンテナンスするにつれて、2つの実装が誤って分岐してしまうリスクとともに、コストが増大します。
Approach #2: Build-time evaluation of bootstraps
一方で、実際にビルド時にbootstrapを実行し、そして、リンクをバイトコードに戻すリバースエンジニアリングを試したくなるものです(これはビルド時評価の制約のある形式)。これは2つの問題にぶつかります。1つは意味的な問題、もう1つは実用的な問題です。
意味的な問題とは、すべてのbootstrapがこの種の変換に対応できるわけではないということです。リンケージはランタイム環境に依存する可能性があり、時として微妙な方法の場合があります。少なくとも、我々はbootstrapにはこのような扱いをオプトインしてもらいたいと思っていますが、開発者はbootstrapがオプトインしていることを理解している必要があり、ここが微妙なところで、やってはいけないことが多数あります。さらに悪いことに、このやってはいけないことの詳細に飛び込むと、事態はすぐに「ぐにゃぐにゃ」になります。シフト可能なbootstrapがどのような操作を実行できるかを指定することは、重要にして精度の低い試みであり、bootstrap開発者が簡単に誤って線の外側にはみ出してしまうでしょう。例えば、bootstrapは副作用なしであるべきと言いたくなりますが、ほとんどのbootstrapは、デバッグのためにロギングしたり、生成されたコードをディスクにダンプしたりするような、いくつかの良性の副作用があります。同じことが、想像できるほとんど全ての制約に当てはまりますが、それぞれに例外と例外の中の例外があります。これゆえに、bootstrap開発者が、所与の副作用(あるいは、不純物、mutability、アイデンティティ依存性、…)が良性であるかどうかを判断する必要があります。
意味論が素直なものと仮定すると、まだ技術的な課題が残っています。ツールは、CallSiteとそのMethodHandleターゲットを検査し、それらをバイトコードに戻すリバースエンジニアリングを行う必要があります。これにはさらなる課題があります。単なる「作業」の問題もあれば、より微妙な課題もあります。リンケージ・ターゲットが常に DirectMethodHandle であるとは限らないので、メソッドハンドルに反映するメカニズムが必要になります。bootstrapはHidden Classをロードすることが多々あるため、bootstrapでそのようなクラスのロードをインターセプトしたり、それらのHidden Classをリフレクションしたりして、それらをキャプチャしてアーカイブに保存できるようにする必要があります。しかし、これらのメカニズムがあったとしても、まだ課題があります。例えば、insertArgumentsコンビネーターは、メソッドハンドルにライブオブジェクトをカリー化できます。時として、これは無害なことがあります。オブジェクトは文字列のような定数で、バイトコードに戻すのは簡単だからです。重要なアイデンティティを持つオブジェクトや、(パターン・スイッチのディスパッチ・キャッシュのように)変更可能なオブジェクトであっても、それが唯一の未解決の参照であり、エスケープできないのであれば、これはまだ無害かもしれません。しかし、悪魔は細部に潜んでいます。
基本的な問題は、Indyのリンケージが非常に柔軟で表現力豊かであるため、結果として得られるメソッド・ハンドル・チェーンを見て、環境依存性を理解していると確信するのが非常に難しいということです。このやり方は不可能ではないものの、多くの複雑さともろさがあります。これは「Unscrambling the egg(卵を元に戻す)」ことに等しく、せいぜい厄介で不正確なものにしかならないでしょう。
A third option
もしオフラインの評価で卵を元に戻す必要があるのなら、そもそも卵を解きほぐすべきではないでしょう。あるbootstrapは、実行時の状態と密接に結びついているセマンティクスを有していて、他のbootstrapはシフトでき、「オンライン」でも「オフライン」でも機能するセマンティクスを有しています。我々は、この「シフト可能なbootstrap」という概念をプログラミングモデルに持ち込むことによって、特注コンデンサ・アプローチとビルド時評価アプローチの両方のもろさを回避できます。
indy bootstrapは、理論的には任意の引数で直接呼び出し可能な普通のstaticメソッドですが、 indyサイトをリンクするためにJVMから呼び出される場合は、常に、String、Class、MethodHandleなどの定数プールから定数をロードした結果であるオブジェクトつきで呼び出されます(定数プールでの未解決のシンボリック形式と区別するために、これらをライブオブジェクトと呼びます(JVMS 5.4.3.6))。
5.4.3.6. Dynamically-Computed Constant and Call Site Resolution
https://docs.oracle.com/javase/specs/jvms/se20/html/jvms-5.html#jvms-5.4.3.6
同様に、bootstrapは(CallSiteでラップされた)MethodHandleの形で ライブの結果を生成し、アクセス制御はLookup(アクセス制御コンテキストを表すライブオブジェクト)を通してなされます。
bootstrapのオフライン評価を表現したい場合、オンライン評価で使用されるライブオブジェクトが利用可能とは限りません。利用可能なのは、定数プールに存在するシンボリックな情報です。JDK12で、ConstantDesc階層を使用して、ロード可能な定数の全ての種類をシンボリックに記述できるjava.lang.constant APIを導入しました(Stringとその仲間は、ConstantDescを直接実装するように改修されました。新しい実装はClassDescとその仲間のために作成されました)。オフラインbootstrapは、Classのようなライブオブジェクトではなく、ClassDescのようなシンボリック記述子を扱います。
ロード可能な定数のそれぞれの種類には、ライブとシンボリックの両方の形式(例えば、ClassとClassDesc)をがあり、両者間で変換する手段がありますが、双方向で制限があります。例えば、hidden classにはClassがありますが、ClassDescで記述できません。そのため、すべてのClassオブジェクトがシンボリック記述を持つわけではありません。一方、ClassDescからClassへの変換にはLookupの助けが必要です。そのため、対応するClassはそのLookupにアクセス可能で、適切なクラス・ローダーがロード出来る必要があります。シフト可能なbootstrapは、両ドメインの制限の中で動作できる必要があります。
2つのエントリーポイントを持つシフト可能なbootstrapを表現できます。1つは伝統的なライブオブジェクトを受け入れ、生成するもの、もう1つはシンボリック記述子を受け入れ、生成するものです。現実には、これはつまり、このようなbootstrapは通常クラスファイル生成によって動作し、2つのエントリーポイントは、引数を共通の形に展開することにほぼほぼ関わっていて、2つのエントリーポイントが主要なコード生成ロジックを共有できる、ということです。コード生成の反対側では、2つのエントリーポイントは、クラス生成の結果を展開するための異なる経路を持つかもしれません(展開したものをロードするか、アーカイブに保存するか)。アクセス制御の決定は、生成されたバイトコードを実行する通常のメカニズムを通じて行われます。
Paired bootstraps
indy bootstrapは、3つの標準メタデータ引数(ルックアップ、名前、型)と追加のbootstrap固有引数を持つ静的メソッドです。bootstrapは可変長引数を取るメソッドであってもよく、 JDKは静的引数リストの形をbootstrapの形に適切に適応させます。bootstrapは一般的に、それらの引数がロード可能な定数であることを期待しています。それは、bootstrapのための静的引数リストは、一般的にクラスファイルのBootstrapMethod属性に由来するためです(bootstrapは普通のメソッドで、直接呼び出すこともできますが)。
bootstrapはABIを形成するので、既存bootstrapのシグネチャを変更できませんが、(オンライン使用目的で設計されている)既存のbootstrapをオフラインbootstrapとペアにできます。ClassをClassDescに、MethodTypeをMethodTypeDescに、MethodHandleをMethodHandleDescに変換し、String、Integer、Long、Float、Doubleはそのままにして、他の全てをConstantDescに変換することで、オンラインbootstrapの引数リストを対応するオフラインbootstrapに機械的に変換できます。そして、オンラインbootstrapに注釈を付けて、indyのcondense(凝縮)に使用できるオフラインのペアがあることを示すことができます。例えば、LambdaMetafactoryからのbootstrapは以下の通りです。
@OnlineBootstrap
public static CallSite metafactory(MethodHandles.Lookup caller,
String interfaceMethodName,
MethodType factoryType,
MethodType interfaceMethodType,
MethodHandle implementation,
MethodType dynamicMethodType)
throws LambdaConversionException { ... }
@OnlineBootstrap
public static CallSite altMetafactory(MethodHandles.Lookup caller,
String interfaceMethodName,
MethodType factoryType,
Object... args)
throws LambdaConversionException { ... }
@OnlineBootstrapタグを付けて、これらはbootstrapとして利用されることを意図したメソッドであること、そしてオフライン用のペアが存在することを識別できるようにします。オフラインのペアは、オンラインbootstrapのシグネチャから機械的に導出されたシグネチャを持ちます。Lookup引数をホストクラスを記述するClassDescで置き換えます。bootstrapは、CallSiteではなく、望ましい(定数)連結のためのシンボリック記述子を返します。
@OfflineBootstrap
public static OfflineIndyLinkage metafactory(ClassDesc lookupClass,
String interfaceMethodName,
MethodTypeDesc factoryType,
MethodTypeDesc interfaceMethodType,
DirectMethodHandleDesc implementation,
MethodTypeDesc dynamicMethodType)
throws LambdaConversionException { ... }
@OfflineBootstrap
public static OfflineIndyLinkage altMetafactory(ClassDesc lookupClass,
String interfaceMethodName,
MethodTypeDesc factoryType,
ConstantDesc... args)
throws LambdaConversionException { ... }
Condenser operation
凝縮可能なindy bootstrap用の単一のコンデンサーがあるとしましょう。これはindyサイトを検索し、bootstrapの@OnlineBootstrapアノテーションを検査します。もし見つかれば、(機械的なシグネチャ・マッピングによって)オフラインのbootstrapを探し、bootstrapクラスをロードし、オフラインbootstrapを起動します。その結果は、リンケージの結果をエンコードするOfflineIndyLinkageをシンボリックに記述します。これには、どのhidden classがリンケージの結果として生成されたか、そしてターゲット・メソッドの所有者、名前、型を含むリンケージの結果が含まれています。コンデンサーは、indyを保持するクラスのnestmateとしてアプリケーション構成に新しいクラスを追加し、indyの呼び出しをバイトコード化されたターゲットメソッドの呼び出しに置き換えます。
What about condy?
Constant_Dynamic_info (“condy“) の凝縮は、indyの凝縮よりもさらに複雑です。それは、condyがより多くの場所に出現し、任意の型を取ることができるためです。condyは、LDCのオペランド、indyまたはcondy bootstrapのstatic引数リスト、あるいは、定数プールインデックスを使用する任意の属性に現れます。
ここでindyについて概説したアプローチは、制限付きながらcondyに拡張できそうです。これらは後のイテレーションで検討されることでしょう。
Was using indy in translation a mistake?
これらのことから、そもそもindyを使ってリンケージをビルド時からランタイムにシフトしたのは間違いだったと結論づけたくなるかもしれませんが、indyを使ってLambdaを翻訳することを選んだのにはそれなりの理由がありましたし、その理由は現在でも変わっていません。
Lambdaやメソッド参照をキャプチャするindyサイトは、必要なセマンティクスを明示的かつ最小限にキャプチャします。staticに生成されたlabmdaのプロキシ・クラスは、本質的ではない詳細に満ちていてプログラマーの意図がわかりづらく、結果最適化が難しくなります。リンケージを実行時に延期することで、再コンパイルせずに、JDKの更新だけでコード生成を改善でき、実行時に選択できる(文字列連結bootstrapで行われるような)多様なコード生成戦略を可能にします。すべてのプログラムが起動時間だけを最適化したいわけではありませんからね。
Writing shiftable bootstraps
結果として、bootstrap用のプログラミング・モデルを調整すると、均衡点がシフトして、オンラインでもオフラインでも実行できるbootstrapに見返りがあるようになります。これは、そのようなbootstrapがバイトコード生成によって実行される、ということとほぼ同義です。このとき、ライブ入力またはシンボリック入力をコード生成用共通フォーマットにマップする2つのshallow prefixと、生成済みコードを受け取り、コンデンサーが利用するためにロードするかパッケージ化する2つのshallow exit pointを持ちます。LambdaMetafactoryのようなJDKの既存のbootstrapは、この「2つの頭を持つ」フォームに比較的簡単にリファクタリングできます。
bootstrapの中には、定数リンケージを持っている可能性がありますが、そのリンケージにはミュータブルな状態が含まれています。パターンスイッチ分類のためのbootstrapデフォルトでは、(ディスパッチ決定をキャッシュして後で再利用できるように)このテクニックを使いたいかもしれません。伝統的なオンラインbootstrapの場合、通常、insertArgumentsでメソッド・ハンドルにミュータブル・オブジェクトをカリー化して実現しますが、これはコード生成でも同じように簡単にできます。indyを置き換えるstaticメソッドは、必要なステート・キャリアの新しいインスタンスを生成し、それをファクトリーやコンストラクタに内部的に渡すことができるからです。