kagamihogeの日記

kagamihogeの日記です。

JEP 370: Foreign-Memory Access API (Incubator)をテキトーに訳した

https://openjdk.java.net/jeps/370

JEP 370: Foreign-Memory Access API (Incubator)

Owner    Maurizio Cimadamore
Type    Feature
Scope   JDK
Status  Proposed to Target
Release 14
Component   tools
Discussion  panama dash dev at openjdk dot java dot net
Created 2019/07/09 15:55
Updated 2019/12/05 01:19
Issue   8227446

Summary

javaヒープ外の外部メモリ(foreign memory)に安全かつ効率的なアクセスをするAPIを導入します。

Goals

外部メモリAPIは以下条件を満たす必要があります。

  • 汎用性(Generality): 同一APIで様々な外部メモリ(native memory, persistent memory, managed heap memoryなど)を操作可能であること。
  • 安全性(Safety): どのような種類のメモリであっても、JVMの安全性を損なわないAPIであること。
  • 決定論的(Determinism): メモリ割り当て解放操作はソースコード上から自明であること。

Success Metrics

外部メモリAPIは、今日使われているjava.nio.ByteBuffer, sun.misc.Unsafe、の代替を目指します。新規APIはそうした既存APIと比べてパフォーマンスの向上を目指します。

Motivation

現状、外部メモリにアクセスするライブラリは、Ignite, mapDB, memcached, Netty (ByteBuf)、など多数存在します。

ただ、Java APIは外部メモリアクセスに対しては不十分です。

Java 1.4で導入したByteBuffer APIは、off-heapに割り当てるダイレクトバイトバッファ(direct byte buffers)を生成可能で、Javaから直接off-heapメモリを操作出来ます。しかしダイレクトバッファには制限があります。たとえば、2GB以上のバッファを生成できず、これはByteBuffer APIがintベースのインデクシングスキーマなためです。また、ダイレクトバッファに関連するメモリの割り当て解除はGC任せなため、バッファ使用は面倒事になりがちです。これは、GCから到達不能と見なされたダイレクトバッファのみ解放可能なためです。長年、問題解決や制限解除のための機能拡張リクエストが多数提出されました(例:4496703, 6558368, 4837564, 5029431)。ByteBuffer APIは、off-heapメモリアクセスだけでなく、文字エンコーディング/デコーディングとパーシャルI/O操作に重要なバルクデータのプロデューサー/コンシューマ、のためにも設計されたもので、各種制限はここに由来しています。

もう一つのJavaから外部メモリアクセスする主要な方法はsun.misc.Unsafe APiです。Unsafeは多数のメモリアクセス操作(例:Unsafe::getIntputInt)を公開し、このAPIは良く出来た汎用アドレスモデルによりon-heap同様にoff-heapでも動作します。メモリアクセスにUnsafeを使用するのは極めて有効です。すべてのメモリアクセス操作はJVM intrinsicsとして定義してあるため、メモリアクセス操作は基本的にJITが最適化します。しかし、Unsafe APIはその定義上unsafeで、任意のメモリにアクセスできます(例:Unsafe::getIntlongアドレスを取る)。これにより、例えば解放済みメモリにアクセスするなどで、JavaプログラムでJVMをクラッシュ可能です。加えて、Unsafe APIはサポートされるJava APIでは無いので、その使用は強く非推奨です。

メモリアクセスにJNIを用いることも可能ですが、固有のコストがあるのであまり使う機会はありません。開発パイプライン全体が複雑で、これはJNIがCコードのメンテナンスを開発者に強いるためです。またJNIは本質的に低速で、これはJava-ネイティブ変換がアクセスのたびに発生するためです。

要約すると、外部メモリでは開発者はジレンマとして、安全だが制限(プラス基本的に非効率)のある方法を取るか、安全保障が無く非サポートで危険なUnsafe APIを使うべきか、に直面します。

本JEPでは、サポートが有る・安全・効率的な外部メモリアクセスAPI、を導入します。外部メモリアクセスの問題を解決することで、既存APIの制限や危険性から開発者を解放します。また、新規APIJIT最適化を念頭に作り直すため、パフォーマンスも向上します。

Description

外部メモリアクセスAPIは三つの抽象化、MemorySegment, MemoryAddress, MemoryLayout、を提供します。

MemorySegmentは指定の空間的・一時的境界で隣接メモリ空間のモデルを扱います。MemoryAddressはセグメント内のオフセットと見なせるものです。MemoryLayoutはメモリセグメントの中身のプログラム上の表現です。

メモリセグメントは各種ソース、ネイティブメモリバッファ・Java配列・バイトバッファ(ダイレクトかヒープベース)、から生成します。たとえば、ネイティブメモリセグメントは以下のように生成します。

try (MemorySegment segment = MemorySegment.allocateNative(100)) {
   ...
}

これは100 byteのネイティブメモリバッファに関連付けられたメモリセグメントを生成します。メモリセグメントは空間的な境界(spatially bounded)で、上限・下限があります。この境界外へのメモリアクセスにセグメントを使用しようとすると例外をスローします。try-with-resourceと共に使用する場合、セグメントは一時的な境界(temporally bounded)となり、生成・使用・クローズするとアクセス出来なくなります。セグメントのクローズは常に明示的な操作で、セグメントに関連付けられたメモリ解放など、副作用を伴います。クローズ済みメモリセグメントへのアクセスは例外をスローします。メモリアクセスAPIの安全性を保障するのに空間的・一時的メモリの安全性チェックは、JVMクラッシュを防止などで、重要です。

セグメントとメモリ間のデリファレンスmemory-access var handleで行います。この特殊なvar handleは少なくとも1つの必須アドレス、MemoryAddress型、を持ちます。ここでのMemoryAddressデリファレンス対象のアドレスです。これはMemoryHandlesのファクトリメソッドで取得します。例として、ネイティブセグメントのsetにはメモリアクセスのvar handleを以下のように使用します。

VarHandle intHandle = MemoryHandles.varHandle(int.class);

try (MemorySegment segment = MemorySegment.allocateNative(100)) {
   MemoryAddress base = segment.baseAddress();
   for (int i = 0 ; i < 25 ; i++) {
        intHandle.set(base.offset(i * 4), i);
   }
}

メモリアクセスvar handleは、longの、追加パラメータアドレスを使用可能で、多次元インデックスアクセスなど、複雑なアドレッシングスキーマのために使います。こうしたメモリアクセスvar handleは通常MemoryHandlesコンビネータメソッド呼び出しで取得します。たとえば、インデックスメモリアクセスハンドル経由でネイティブセグメントに直接setするには以下のようにします。

VarHandle intHandle = MemoryHandles.varHandle(int.class);
VarHandle intElemHandle = MemoryHandles.withStride(intHandle, 4);

try (MemorySegment segment = MemorySegment.allocateNative(100)) {
   MemoryAddress base = segment.baseAddress();
   for (int i = 0 ; i < 25 ; i++) {
        intElemHandle.set(base, (long)i);
   }
}

This effectively allows rich, multi-dimensional addressing of an otherwise flat memory buffer.

APIの表現力強化と、上記サンプルのような明示的な数値計算の必要を省略するために、MemoryLayoutはメモリセグメントをプログラム的に記述するのに使います。たとえば、上記サンプルで使用するネイティブメモリセグメントのレイアウトは以下のように記述します。

MemoryLayout intArrayLayout
    = MemoryLayout.ofSequence(25,
                              MemoryLayout.ofValueBits(32,
                                                       ByteOrder.nativeOrder()));

上記は指定(32-bit)のシーケンスメモリレイアウトを25回繰り返したものを生成します。メモリレイアウトを作ると、コードから手計算のコードを追い出して、必要なメモリアクセスvar handleを単に生成するだけになります。

MemoryLayout intArrayLayout
    = MemoryLayout.ofSequence(25,
                              MemoryLayout.ofValueBits(32,
                                                       ByteOrder.nativeOrder()));

VarHandle intElemHandle
    = intArrayLayout.varHandle(int.class,
                               PathElement.sequenceElement());

try (MemorySegment segment = MemorySegment.allocateNative(intArrayLayout)) {
   MemoryAddress base = segment.baseAddress();
   for (int i = 0 ; i < intArrayLayout.elementCount() ; i++) {
        intElemHandle.set(base, (long)i);
   }
}

上の例では、レイアウトパス(layout path)の生成経由でメモリアクセスvar handleを生成してレイアウトインスタンスを作成しています。複雑なレイアウト式からネストしたレイアウトを構築するのにレイアウトパスを使います。レイアウトインスタンスはネイティブメモリセグメントのアロケーションを、レイアウトのサイズとアライメントに基づいて行います。前述サンプルのループ定数はシーケンスレイアウトの要素数で置き換えています。

外部メモリアクセスAPIはまずインキュベートモジュールjdk.incubator.foreignとして提供します。

Alternatives

既存APIByteBuffer, Unsafe, 最悪JNIの使用を継続する。

Risks and Assumptions

安全性と有効性を兼ね備える外部メモリアクセスAPIの構築は困難なタスクです。前述セクションのような空間的・一時的チェックはアクセスのたびに実行の必要があり、そうしたチェックでJITが最適化可能なことは重要です e.g., hoisting them outside of hot loops. メモリアクセスAPIが有効かつ既存のByteBufferやUnsafeのような最適化可能なことを保障する機能がJIT実装に必要になると思われます。

Dependencies

本JEPのAPIProject Panamaの目的であるネイティブ相互運用機能の構築を補助するかもしれません。また、本APIJEP 352 (Non-Volatile Mapped Byte Buffers)のnon-volatileメモリアクセスにおいてより汎用で効果的な方法で使えるかもしれません。