Selectively Shifting and Constraining Computation

原文はこちら。
The original article was written by Mark Reinhold (Chief Architect, Java Platform Group, Oracle).
https://openjdk.org/projects/leyden/notes/02-shift-and-constrain

The goal of Project Leyden is to improve the startup time, time to peak performance, and footprint of Java programs. In this note we propose to work toward that goal by extending the Java programming model with features for selectively shifting and constraining computation by means of condensing code. We also propose an initial research and development roadmap.

(Project Leydenのゴールは、起動時間、ピークパフォーマンス到達までの時間、そしてJavaプログラムのフットプリント、これらを改善することにあります。このノートでは、Javaプログラミングモデルを拡張し、コードを凝縮することによって計算を選択的にシフトしたり制約したりする機能を持たせることによって、この目標に取り組むことを提案します。また、最初の研究開発ロードマップも提案します。)

Call for Discussion: New Project: Leyden
https://mail.openjdk.org/pipermail/discuss/2020-April/005429.html

プログラムの起動時間、ウォームアップ時間、フットプリントは、計算の一部を時間的にずらすことで改善されることが多々あります。例えば、実行時の後の時点に先送りする(例:遅延初期化)とか、実行時間より前の時点に後戻りする(例:AOTコンパイル)とかです。また、Javaの動的機能(クラスローディング、クラスの再定義、リフレクションなど)に関連する計算の一部を制約することで、さらに性能を向上させることができ、より優れたコード解析、ひいてはさらなる最適化を可能にします。

Project Leydenでは、これらのシフト、制約、最適化変換をコンデンサ(condenser) として実装します。コンデンサとは、コンパイル時と実行時の間のフェーズで動作するプログラム変換器です。コンデンサは、Javaプラットフォーム仕様が元のプログラムに与える意味を維持したまま、プログラムを新しく、より高速で、潜在的に小さなプログラムに変換します。私たちは、このような変換を認め、サポートするために、必要に応じてJavaプラットフォーム仕様を進化させる予定です。また、開発者自身が計算をシフトできるような新しい言語機能を調査していきまうs。これによりさらなる凝縮を可能にできると考えています。

凝縮モデルは、開発者に大きな柔軟性を与えます。開発者は、どのコンデンサ(=凝縮モデル)を適用するかを選択し、そうすることで、Javaの自然なダイナミズムを制限する制約を受け入れるかどうか、どのように受け入れるかを選択します。起動、ウォームアップ、フットプリントの最適化について、画一的なモデルを許容する必要はないのです。

また、凝縮モデルは、Javaの実装にかなりの自由を与えます。コンデンサがプログラムの意味を保持し、開発者が受け入れた制約以外の制約を課さない限り、実装は結果を最適化するための大きな自由を得られます。

凝縮されたプログラムの性能特性は、プログラムに適用されたコンデンサの創発的な特性です。もし開発者が、十分な計算を実行時から初期の段階に移すコンデンサを選択した場合(多くの制約を受け入れる必要があるかもしれませんが)、適合するJava実装は、完全に静的なプラットフォーム固有の実行ファイルを作成することさえ可能でしょう。

Shifting computation

起動時間、ウォームアップ時間、フットプリントを改善する最良の方法は、単純に排除できる計算を特定することです。それができなければ、時間的に前方または後方に計算を移動させることができます。計算が常に必要とは限らないことを期待して、計算を実行時間の後半にシフトさせることもできます。また、実行時よりも前の時点、例えばコンパイル時などにずらすこともできます。プログラムによって直接表現される計算(例:forループの実行)をシフトしたり、プログラムに代わってランタイムシステムが間接的に実行する計算(例:メソッドをネイティブコードにコンパイル)をシフトしたりできます。

時間的に計算をずらすという概念は新しいものではありません。実際Javaの実装にはすでに計算のシフト機能が多くあります.これらの機能の中には、自動的に動作するものもあります。以下はその例です。

  • コンパイル時の定数折りたたみ
    • 直接計算、つまり単純な式の評価を、実行時からコンパイル時まで時間的に前倒しします。
  • ガベージコレクション:間接的な計算
    • つまりメモリの再利用を実行時以降にシフトします.

その他の計算シフト機能はオプションであり,リクエストが必要です。時によっては設定が必要です。以下はその例です。

  • Ahead-of-timeコンパイル
    • Java コードからネイティブコードへのコンパイルという間接的な計算を、実行時からコンパイル時に前倒しする。
  • クラスデータ共有 (Class-data sharing: CDS)
    • いくつかのクラスファイルの解析と検証、およびいくつかのランタイムデータ構造の初期化という間接的な計算を、実行時からアーカイブ生成時に前倒しする。

さらに、計算シフト機能はJavaプログラミング言語にもともと備わっており、開発者自身がその機能を使い計算をシフトできます。例えば、遅延クラスローディングと初期化は、開発者がInitialization On Demand Holderイディオムなどの技術を使って、直接的な計算を時間的に後回しにできます。

Initialization-on-demand holder idiom
https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom

Java実装が計算シフトをする場合、Javaプラットフォーム仕様、特にJava言語仕様とJava仮想マシン仕様の範囲内で行う必要があります。

The Java® Language Specification – Java SE 19 Edition
https://docs.oracle.com/javase/specs/jls/se19/html/index.html
The Java® Virtual Machine Specification – Java SE 19 Edition
https://docs.oracle.com/javase/specs/jvms/se19/html/index.html

これらの仕様では、計算を任意に時間的にシフトさせることはできません。したがって、上記のものを含め、適合するJava実装で利用可能なシフト機能は、互換性を確保するために、これらの仕様の範囲内で動作するように注意深く設計されています。

Project Leydenでは、計算シフトのための新たな手法を模索し、必要に応じてプラットフォーム仕様を進化させ、計算をシフトしてもプログラムの意味が保たれるように対応する予定です。たとえば、動的プロキシのコールサイトを実行時間前に通常のバイトコードに拡張するような手法です。また、実行前にクラスを解決するなど、仕様の変更を必要とするものもあるでしょう。さらに、遅延 static finalフィールド (Lazy Static Final Fields) のように、開発者がソースコードで直接時間的変化を表現できるような新しいプラットフォーム機能の形をとる可能性もあるでしょう。

JEP draft: Lazy Static Final Fields
https://openjdk.org/jeps/8209964

Constraining Java’s natural dynamism

計算シフトには、コード解析が必要になることが多々あります。どの計算をシフトさせるかを正確に判断し,そのシフトがプラットフォーム仕様に沿ったプログラムの意味を維持することを確認しなければならないからです。

Java の場合、プラットフォームの動的な機能のせいで、従来の言語よりもコード解析が難しくなっています。実行中のプログラムは、クラスのロード、クラスの再定義、フィールドへのリフレクションによるアクセス、予測不可能な方法でのメソッドの呼び出しなどが可能です。

この問題に対するソリューションの一つは、closed-world制約の採用です。これには、実行時に実行されるすべてのコードが、ビルド時に既知で解析に利用できる必要があります。このため、任意の動的なクラスローディングやリフレクションが禁止されています。しかし、多くのライブラリやフレームワークがクラスの読み込みとリフレクションに大きく依存しているため、すべてのアプリケーションがこの制約に適しているわけではありません。さらに、すべての開発者がclosed-world制約に耐えられるわけではありません。なぜなら、この制約を利用するツールは通常、開発者に脆弱な設定ファイルを構築し維持することを要求し、異常に長いビルド時間に耐えることを要求するからです。

そのため、Project Leydenでは、より弱い制約についても検討します。より多くの、より良い計算シフトを可能にするために、プラットフォームの動的機能を選択的かつ明示的に制限する方法を検討します。開発者は、アプリケーションの必要性に応じて、機能と性能をどのように交換するかを自ら選択できるようにします。

例えば、開発者が特定したいくつかのクラスは再定義できないという弱い制約を選ぶことができるように仕様を改訂したとしましょう(つまり、そのようなクラスを再定義しようとすると、例外が発生するような状況にします)。この制約の下で適合するJava実装は、実行時に先んじてこのようなクラスをロード、検証、準備、および解決し、結果のメタデータをCDSアーカイブに保存できることでしょう。 この制約の下で、適合するJava実装は、実行時間の前にそのようなクラスをロード、検証、準備、および解決し、結果のメタデータをCDSアーカイブに保存することができます。これらの計算をより早い時期に行うことで、CDSやAOTコンパイルされたネイティブコードが必要とする有効性チェックのいくつかを排除するなど、さらなる最適化が可能になります。これらにより、起動時間の大幅な短縮が期待されます。

Phases of computation

実行時の計算を後ろ倒しすると、(本当に実行されるのであれば)JVM呼び出しの通常のライフタイムの間、依然として実行時に実行されます。しかし、JVMの呼び出しの前に計算を前倒す場合、実行時の前に、そしてほぼ確実に実行時とは異なる環境で、計算の異なるフェーズで実行される必要があります。

もともとJavaには、コンパイル時(つまりjavacを実行したとき)と実行時の2つの計算フェーズがあるだけでした。JDK 9では、リンク時を導入しました。

JEP 261: Module System
https://openjdk.org/jeps/261#Phases

これは、2つの段階の間の第3の段階(オプション)で、モジュールのセットを組み立て、jlinkツールでカスタムランタイムイメージに最適化できる段階です。JDK 10では、アプリケーションのクラスとデータの共有(Application Class Data Sharing、AppCDS)を実装し、CDSアーカイブ生成のために、コンパイル時またはリンク時のいずれかに続く4番目のオプションのフェーズを導入しました。

Application Class Data Sharing
https://docs.oracle.com/en/java/javase/19/docs/specs/man/java.html#application-class-data-sharing

程度の差はありますが、実行時の計算を前の3つのフェーズのいずれかに移行できます。

コンパイル時局所的に計算を時間的に前倒しするのに適したフェーズ(例:動的プロキシの拡張など)。しかし、すべての計算シフトに適したフェーズではない。
Java言語のコンパイラは、プログラム全体にアクセスできることが保証されていないため、そのような計算を常に実行できるとは限らず、さらに、コンパイラはソースファイルをクラスファイルに変換するものであり、コンパイルしていないコード(例えば、JDK自体のコード)の計算をシフトする立場にはないためである。
CDSアーカイブ生成時アーカイブ生成にはJDKが利用できる必要があるため、JDK自体の計算を含むいくつかのタイムシフトされた計算に適している可能性がある。
ただし、アーカイブ生成はプログラム全体へのアクセスを必要としないので、コンパイル時と同様に、すべての計算シフトに適したフェーズではない。
リンク時前の2個とは異なり、任意の計算シフトに適したフェーズである。
jlinkツールはカスタムランタイムイメージ作成のためにJDK本体とプログラム全体の両方にアクセスできる必要があるため、常にこのような計算を実行できる。

しかし、実行時からの前倒し計算シフトを、コンパイル時、アーカイブ生成時、リンク時の3つのフェーズだけに限定しなければならないのはなぜでしょうか。異なるタイミングで異なる計算のシフトを適用することが最適であるのだから、もっと多くの段階があってもいいはずです。

たとえば、設定がXMLファイルに保存されているが、それ以外にJDKのXMLパーサーを必要としないアプリケーションを考えてみましょう。模式的に説明します。

class Configuration {
    private static final XML conf = XML.parse(CONFIG_FILE);
    private static final String serverName = conf.xpath("/conf/server");
    static String serverName() { return serverName; }
}

public class AppMain {
    public static void main(String ... args) {
        Server.start(Configuration.serverName(), ...);
    }
}

実行時の前段階でConfigurationクラスを初期化し、後のフェーズで使用するためにserverNameフィールドの値を保存して時間を節約し、さらに、不要になったXMLパーサーを削除してスペースを節約したいと考えています。

しかし、serverNameフィールドを初期化するには、アプリケーションのXML構成ファイルを読み込む必要があり、実際には、そのファイルは既存の3つのフェーズのいずれでも使用できない可能性があります。例えば、アプリケーションをカスタムランタイムイメージにリンクして、複数の異なるサーバー環境に配備し、それぞれが独自のXML構成ファイルを持っている場合、そのようなことが起こり得ます。イメージのデプロイまでにすでにリンク済みであり、この変換を適用するには遅すぎるのです。

そこで、一般化して、タイムシフト変換や関連する最適化を適用するフェーズを任意に増やしてみましょう。

Condensing code

完全で実行可能なプログラムのビルドプロセスは、コード成果物を変換する一連の操作です。例えば、コンパイルはソースファイルの集合をクラスファイルの集合に変換します。関係する成果物は、たとえば、次のように他の多くの形態をとり得ます。

  • アプリケーションのコードを含む JAR ファイル。
  • アプリケーションのコードとそのすべての依存関係を含むJARファイルのセット、またはいわゆるfatもしくはuber JARファイル。
  • アプリケーションのコード、その依存関係、およびJVMを含むJDKのあらゆるモジュールを含むカスタムJDKランタイムイメージ、あるいは
  • アプリケーションコード、依存関係コード、JDKコードをネイティブ形式で含む、JVMを含まない完全に静的なプラットフォーム固有の実行ファイル。

The Skinny on Fat, Thin, Hollow, and Uber
https://dzone.com/articles/the-skinny-on-fat-thin-hollow-and-uber

コンデンサーは、オプションの変換フェーズで、ここでは以下のことを実施します。

  1. コード成果物で表現された計算の一部を実行し、その計算を後のフェーズから現在のフェーズにシフトさせる。
  2. そのシフトによって可能になる最適化を適用して、アーティファクトを新しい、より高速で、より小さいアーティファクトに変換する。結果として得られる成果物には、新しいコード(AOTコンパイルされたメソッドなど)、新しいデータ(シリアライズされたヒープオブジェクトなど)、および新しいメタデータ(プリロードされたクラスなど)を包含できる。また、元の成果物のうち、もはや必要とされないコンポーネント(XMLパーサーなど)を省くこともできる。
  3. 後のフェーズに新しい制約を課すこともできる(たとえば、特定のクラスを再定義できない)。

凝縮には、3つの重要な特性があります。

意味の保持
(meaning preserving)
出来上がった成果物は、元の成果物がそうであったように、Javaプラットフォーム仕様に従ってアプリケーションを実行する。
組み立て可能
(Composable)
あるコンデンサが出力する成果物は、別のコンデンサの入力にできるため、パフォーマンスの向上はコンデンサの連鎖に渡って蓄積される。帰納的に、コンデンサの連鎖は意味も保存する。
選択可能
(Selectable)
開発者は、どのように凝縮するか、何を凝縮するか、そしていつ凝縮するかを選択できる。

この3つの特性は、開発者がプログラムを構築する際に大きな柔軟性を与えてくれます。例えば、テストやデバッグであれば、本番環境では通常使用するコンデンサを省略しても、プログラムの意味は変わらないという保証が得られます。さらに重要なことは、時間的に計算をずらすことで新たな制約を受け入れる必要がある場合、アプリケーションに適したコンデンサーを選ぶことで、機能と性能をトレードオフする方法を選択できます。スタートアップ、ウォームアップ、フットプリントの最適化について、画一的なモデルを我慢する必要はありません。

上記の例に戻ると、デプロイプロセスの一環で、リンク済みランタイムイメージを各サーバー環境用に最適化できます。デプロイ時に、ビルド時に選択したクラスを初期化するコンデンサ(例えば、Configuration)を適用し、その後、使用しないコンポーネント(例えば、XMLパーサー)を削除するコンデンサを適用できます。

Specifying condensers

コンデンサはプログラムコードを実行し、プログラムコードを変換し、時には後のフェーズでプログラムコードができることに制約を加えます。コンデンサがコードを正しく実行し、意味を保持する方法でのみコードを変換および制約することを確実にするために、Javaプラットフォーム仕様にコンデンサの概念を追加する必要があります。また、コンデンサで活用したい新しいオプトイン制約(限定的なクラス再定義など)も追加しなければなりません。最後に、Java実装で利用可能なコンデンサをテストするために、Java Compatibility Kit (Java互換性キット、JCK) を拡張する必要があります。

Gaining Access to the JCK
https://openjdk.org/groups/conformance/JckAccess/

しかし、特定のコンデンサを指定するためにJavaプラットフォーム仕様を拡張する必要はありませんし、特定のコンデンサが何を行うかを正確にテストするためにJCKを拡張する必要もありません。Javaプラットフォーム仕様では、Javaプログラムの意味、すなわちプログラムで直接表現された計算の結果がどうあるべきかを定義しており、JCKはそれを検証するものです。Javaプラットフォーム仕様は、プログラムの意味をどのように実装すればよいのか、あるいは、どのように実装しなければならないのかについては、意図的に触れていません。したがって、Javaの実装は、仕様の範囲内で自由に最適化することができます。

Performance is an emergent property

Javaプラットフォーム仕様が特定のコンデンサを指定しないことで、Javaの実装にはかなりの自由が与えられています。コンデンサがプログラムの意味を保持し、開発者が受け入れた制約以外の制約を課さない限り、その結果を最適化する自由度は大きくなります。

したがって、完全かつ実行可能なプログラムの性能特性は、仕様書のどの語句の直接的な結果でもなく、むしろプログラムに適用されるコンデンサの創発的な特性なのです。開発者が、十分な計算を実行時から初期の段階に移すコンデンサを選択した場合、多くの制約を受け入れる必要があるかもしれませんが、適合するJava実装は、完全に静的なプラットフォーム固有の実行ファイルを作成することさえ可能です。

Project Leyden自体で、完全に静的な実行ファイルを実装するところまで行くかどうかは、まだわかりません。しかし、他のJava実装(例えば、GraalVMネイティブイメージツールの将来のバージョン)がそうすることを可能にすることはできます。

Getting Started
https://graalvm.org/dev/reference-manual/native-image/

Roadmap

作業は2つのカテゴリーに分けられます。コンデンサの概念を規定し実装することと、具体的なコンデンサとそれに関連する新しい言語機能を研究開発することです。

Introduce condensers

前述の通り、コンデンサの概念をJavaプラットフォーム仕様に追加し、コンデンサをテストするためにJCKを拡張する必要があります。

実装の面では、コンデンサをサポートするためにJDKのツール(たとえばjlink)を拡張し、新しいコード、データ、メタデータに対応するためにさまざまなコード成果物(たとえばJARファイルやランタイムイメージ)の形式を拡張する必要があります。その過程で、新しいツールや、おそらくは新しい凝縮可能な成果物の形式の作成が最善であることがわかる可能性があります。

Research and develop new condensers and new language features

多くの可能性がありますが、ここではそのうちのいくつかを挙げています。これらのアイデアのいくつかは、Project Leydenの貢献者がすでに検討、プロトタイプ化しています。これらのほとんどは並行して作業を進めることができ、実りあることが証明されたものは、メインラインのJDKリリースに段階的に提供できます。

新しいオプトイン制約を必要としないため、仕様変更を必要としない(と思われる)コンデンサ– 動的プロキシcall siteを通常のバイトコードに拡張。
– invokedynamicの内部使用(lambda、文字列連結、switch分岐)を通常のバイトコードに拡張する。
– lambda形式クラスの事前生成
– CDSのさらなる改善
– 投機的AOTコンパイル
新しいオプトイン制約を必要とするために、仕様変更を必要とするコンデンサ– クラス、フィールドアクセス、メソッド呼び出しの事前解決(選択したクラスは再定義できないという制約が必要)
– 非特定AOTコンパイル(プリコンパイルされたクラスが実行時に新たにサブクラス化されないという制約が必要)
– 未使用と思われるクラスやクラスメンバの削除(例えばストリッピング。選択したクラスやメンバは反映されないという制約が必要)。
さらなる凝縮を可能にする新しい言語機能
(これらは言語機能であるため、かなりの研究とプロトタイピングが必要)
– Lazy static final fields
JEP draft: Lazy Static Final Fields
https://openjdk.org/jeps/8209964

– ビルド時の明示的なクラス初期化

コメントを残す

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

WordPress.com ロゴ

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

Twitter 画像

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

Facebook の写真

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

%s と連携中