OracleのOpenJDKチームは、Project ValhallaのLW2プロトタイプ (別名"インラインクラス"、以前は"値型(value type)"と呼ばれていた)のアーリーアクセス(EA)ビルドのリリースを発表した。
プロトタイプはこちらからダウンロードできる。公開されているバイナリは、今後数週間にわたって、バグ修正とパフォーマンス向上を目的として随時更新される予定である。
同チームでは、ユーザーモデルへのフィードバックを積極的に求めている。ただし実装上、多くの面において、精査の準備がまだできていない部分が残っている点に注意が必要だ。
LW2という呼称は、インラインクラス機能の実装が、いわゆる"L-World"デザインの2番目のマイルストーンに到達したことを示している。
現在のプロトタイプでは、既存のオブジェクトやインターフェース、つまりL-Typeシステムないし"L-World"にできる限り近いものにすることによって、既存の型システムにインライン型を統合している。
このプロトタイプは、以前のLW1マイルストーンに置き換わるものとして、2019年7月5日にリリースされた。Valhallaとインラインクラスの初期試験を、より多くの開発者の手が届くものにするが、ただし、まだ極めて実験的なレベルである。
このプロトタイプには、ジェネリクスを見直すための最初のステップという意味もあり、インラインクラスのnull許容型の参照プロジェクションを、ジェネリックな型引数として使用可能な構文が採用されている。
初期のプロトタイプであることから、次のような多くの制限がある。
- x64 Linux、x64 Mac OS X、およびx64 Windowsでのみ使用可能である
- 間接型を含むアトミックフィールドがサポートされていない
- @Contendedインライン型フィールドがサポートされていない
- インタープリタとC2に限定したサポートであるC1、階層化コンパイル、Graalに対応しない
- インライン型用の非セーフフィールド(unsafe field)や配列アクセサAPIがない
- C2 JITの最適化が優先されているため、インタプリタはまだ最適化されていない
Dan Heidinga氏(IBMのEclipse OpenJ9プロジェクトリーダ)に、LW2リリースに関して話を聞いた。
InfoQ: LW2の新機能の中で、最も重要なものは何でしょうか?
Heidinga: LW2アーリーアクセスビルドには、たくさんの新機能がありますが、中でも重要なのは、インライン型用のユーザモデルです。これまでのプロトタイプは、MethodHandlesの観点で定義されていたため、参入障壁が高かったのです。
これまでのプロトタイプで本格的なコードを書くのは、MethodHandleの専門家でさえ難しかったため、フィードバックの提供が困難でした。LW2は違います。インライン型の動作方法について、開発者がモデルをテストして、専門家グループにフィードバックを提供できるように、ユーザーフレンドリであることを目指しています。
特に重要な機能のひとつがインライン型用の"?"演算子の導入です。これにより、インライン型がインライン化の候補か、あるいはインダイレクションを使用するかを、ユーザが明示的に選択することが可能になります。
この演算子は、Javaのコレクションライブラリでインライン型を使用するためのイネーブラでもあり、この点でもプロトタイプの開発者エクスペリエンスを向上しています。ジェネリクスで、LW2のタイプの"?"、あるいはnull許容バージョンを使用できるようにするだけで、インライン型用にリファインされたジェネリクスのような、将来的な改善への余地を残しておけるのです。
InfoQ: インラインクラスがエスケープ分析(Escape Analysis)とどのように関係するのか、なぜこれがパフォーマンスにとって重要なのか、説明して頂けますか?
Heidinga: エスケープ分析はJIT最適化のひとつで、オブジェクトの存続期間が現在のコンパイル単位に完全に含まれていて、ヒープや別スレッド、さらにはインラインメソッドの現在のセットのスコープ外に"エスケープ"しないことを証明しようとするものです。
オブジェクトがエスケープしないことを証明できれば、そのオブジェクトを独立したフィールドのセットに分割して、最適化の余地を増やすためレジスタに配置したり、オブジェクトをヒープではなくスタックに割り当てることが可能になります。これらはいずれも、最適化の機会の向上や、オブジェクトをヒープに格納しないことによるガベージコレクションの負荷軽減に寄与します。
素晴らしいものに思えますが、保証はされません。最適化は、さまざまな原因から失敗することがあります。メソッド呼び出しのインライン化が十分でないために、オブジェクトが本当にエスケープしないことを確認できない場合や、あるいは、特定のコンパイル操作では実行されないことがあります。と言うのは、低いティアのコンパイルにおいては、コンパイル結果のコードをより速く得るため、コストの高い最適化処理がスキップされる可能性があるからです。それだけではありません。コードを少し変更したことによってオブジェクトがエスケープされて、最適化が成功しなくなる可能性があるため、明確な原因なしにパフォーマンスが低下する場合もあるのです。
インラインクラスは不変(immutable)であると同時に、IDを持ちません。これによってJITがエスケープしないことを証明する必要がなくなるため、エスケープ分析の理想的な候補になります。分割したり、レジスタに配置したり、最適化することは自由ですが、インライン型はエスケープされる可能性のある任意の時点で常に再構成できるため、そうする必要があります。ここで重要なのは、インライン型はIDを持たないため、再生成されたかどうかを判断する手段がない、ということです。これによって、従来のエスケープ分析にあった不安定さの多くが取り除かれます。
ほとんどのプログラムには、この保証されたエスケープ分析の恩恵を受けるデータのラッパとして機能するような、小さなクラスがたくさんあります。intやlong、あるいはStringをラップして、新たにセマンティックな意味を持たせるようなコードを想像してください。JITがこれらのインスタンスをすべてスタックに割り当てられたら、素晴らしいと思いませんか?それがインライン型の成功のひとつです。
InfoQ: インラインクラスのインスタンスは不変だと思うのですが、なぜこれが更新の原子性に関する問題を引き起こす可能性があるのか、説明して頂けますか?単純な楽観的コピー(optimistic copy)とCAS(compare-and-swap)を使用して、ポインタをスワップできないのはなぜでしょう?
Heidinga: このプロジェクトのスローガンが"クラスのようなコード、intのような動作"であることを思い出してください。intプリミティブをローカル変数やフィールドや配列に書き込んでも、そのintは変更しません。代わりに、コンテンツ全体が上書きされます。LW2のインラインクラスは、このプリミティブ型のように動作します。
概念的にはクリーンなモデルですが、64ビットシステムが標準になってからはJava開発者がほとんど無視できた問題であるティアリング(tearing)、すなわち非アトミックな更新という問題を再燃させているのです。
初期の32ビットJava実装がlongあるいはdoubleの値を処理した方法を思い出せば、問題が理解できるでしょう。CPUは、ネイティブワードサイズの書き込みがアトミックに発生することを保証します。32ビットシステムで32ビットを書き込んでも、分離することはありません。しかし、longは64ビットであるため、ハードウェアに対する特別な働きかけがなければ、32ビットシステムではアトミックに書き込むことはできないのです。
Java言語仕様では、"17.7. Non-atomic Treatment of double and long"で、この問題を認識している。
Javaプログラミング言語のメモリモデルでは、不揮発のlong値またはdouble値への単一の書き込みは、2つの別々の書き込みとして扱われます。
32ビット各半分にひとつです。これにより、ひとつの書き込みから64ビット値の最初の32ビットが、別の書き込みから次の32ビットが参照される場合があり得ます。
インライン型の読み取り時と書き込み時には型の内容全体をコピーする必要があるため、インラインクラスはユーザに対して、このティアリングの問題を再び検討課題のひとつとして突き付けるのだ。
インラインクラスのティアリングが開発者にとって新たな問題になる理由を理解する上で役立つ、ひとつの例を次に示す。
次のCustomerのようなインラインクラスを考えてみよう。
inline class Customer { String firstName; String lastName; long customerID; }
次に、上位3顧客を保持する配列を定義する。
Customer topCustomers[3];
この配列を、2つのスレッドが同時にアクセスする。最初のスレッドが新しいCustomerを配列に書き込み、
Customer c = getTopCustomer(); topCustomers[0] = c;
2番目のスレッドが配列からの読み取りを行う。
Customer b = topCustomers[0];
読み取りと書き込みが同時に発生すると、更新前のCustomerと更新後のCustomerが混在したCustomerを読み取って、不正なCustomerオブジェクトになる可能性がある。これはデータ競合だが、これまでは配列内のポインタを別のポインタに置き換えるだけであったため、(ほとんど)問題にはなっていなかった。
インライン型が、プロセッサが提供する最大のアトミックアップデート(通常、64ビットシステムでワードサイズの2倍なので、128ビット)よりも大きくなると、データ競合がある場合には、ティアリングが潜在的な問題になる。インライン型では、ポインタではなくコンテンツ全体を更新する必要があるため、CASが成功するには大きすぎる。型のコンテンツ全体がコンテナ内でインライン化されているため、都合よく更新するポインタは存在しない。
Valhallaの専門家グループは、これらの懸念の一部に対処するため、インライン型を"アトミック"とマークすることで、ティアリングの起きない方法でのみ記述可能とする方法を検討中だが、現在のLW2は現時点では対象としていない。
ティアリングは、インライン型のデザインはデータの小さな集合体にすべきだ、ということを示唆する、もうひとつの根拠になっている。ここでのキーワードは、"小さい"ということだ
InfoQ: 現在、開発者コミュニティによって最も誤解されているのは、インラインクラスのどのような部分だと思いますか?
Heidinga: ここでは2つを答えたいと思います。最初に多いのは、Cの構造体がそうであるように、インライン型が可変(mutable)である、という誤解です。IBMのPackedObjectsやAzulのObjectLayoutなど、以前の提案では可変型をサポートしていましたが、LW2のインライン型は厳密に不変です。これは最近のJava機能では一般的な傾向で、不変性を優先することで、正しい並行アプリケーションの開発を容易にします。
2番目の一般的な誤解は、インライン型によって、ユーザがオブジェクトのレイアウトを明示的に制御できるというものです。インライン型を使用すると、ユーザがJVMに対して、データをコンテナ(オブジェクトまたは配列)に直接インライン化するように要求できますが、型内のフィールドのレイアウトまでは制御できません。レイアウトのアルゴリズムは引き続き完全にJVMの制御下にあるので、ガベージコレクションをより効率的にするように、フィールドを並べ替えてグループ化することもあります。
InfoQ: 他に何か、読者に伝えたいことはありますか?
Heidinga: LW2アーリーアクセスバイナリと、アップデートされたJVM仕様には、多くの変更が行われています。ぜひチェックして、フィードバックをお寄せください。設計上、未解決の疑問がたくさんあります。そのため、ユースケースに対して設計がどのように機能するかについて、経験レポートを求めています。
インライン型の==での動作、Object []とインライン型配列との間への配列共分散の導入、自分のコードベースでのインライン型の実験方法に関して、フィードバックを募集しています。
Project ValhallaのLW2バイナリは現在提供中で、(ユーザモデルなどの準備されている分野で開発を行っている)一般のJava開発者からのフィードバックが積極的に求められている。