Foreign Memory Access – Pulling all the threads

原文はこちら。
The original article was written by Maurizio Cimadamore (compiler architect at Oracle).
https://inside.java/2021/01/25/memory-access-pulling-all-the-threads/

TL;DR

このドキュメントで説明されているrestacking(積み直し)は、さまざまな方法でForeign Memory Access APIを強化し、クライアントが(ニーズに応じて)複雑さを増しながらAPIにアプローチできるようにします。

  • ByteBuffer APIからのよりスムーズな移行のために、ByteBuffer::allocateDirectMemorySegment::allocateNativeと交換するだけです。それ以上の変更は必要なく、ライフサイクル(とResourceScope)を考慮する必要はありません。GCは引き続きdeallocationを担います。
  • リソースをより厳密に管理したい場合、セグメント(およびその他のリソース)がリソーススコープ(必要に応じて安全にクローズできます)にどのように接続されるかをより深掘りし理解できます。
  • ネイティブの相互運用のために、NativeScopeの抽象化をResourceScopeNativeAllocatorの両方であるように後付けで設定しました。これにより、新しく作成されたリソースをどのように割り当てるか、どのライフサイクルを使用するかをAPIが知る必要がある場合に使用できるようになりました。
  • スコープをロックできるようになりました。これにより、クライアントはクローズを恐れずにセグメントの操作が必要なクリティカルセクションを記述できます。
  • ここで説明した方法を使ってByteBuffer APIの機能強化をしたり、クローズ機能を追加したりするために使用できます。

以上のことから、メモリアクセスAPIのクライアントの変更はほとんど必要ありません。最大の変更点は、MemorySegmentAutoCloseableインターフェースをサポートしなくなることで、代わりにResourceScopeに移動します。単一のセグメントが必要な場合には少し冗長になりますが、複数のセグメントやリソースが必要な場合には、コードのスケーリングが大幅に改善されます。一方、jextractで生成されたAPIを使用している既存のクライアントは、主にNativeScope APIに依存しているためあまり影響を受けませんが、この提案はそれを変更するものではありません(ただし、NativeScopeの役割はアロケータ+スコープに変更されます)。

In detail…

ご存知のように、私はメモリアクセスAPIの使用に関する社内外のフィードバックを見て、APIの問題点と今後の進め方を理解しようとしています。メーリングリストで議論したように、構造化されたアクセスや、最近追加された共有セグメントのサポートなど、うまく機能しているものもあります(後者はさまざまな実験を可能にするため、より多くのフィードバックを集めることができるでしょう)。

[foreign-memaccess] musing on the memory access API
https://mail.openjdk.java.net/pipermail/panama-dev/2021-January/011700.html

しかし、まだ解決すべき問題がいくつかあります。それは、「MemorySegmentの抽象化は、一度に多くのことをやろうとしすぎている」ということです(この問題の詳細な説明については、先ほどのメーリングリストの議論を参照してください)。

前述のメーリングリストでは、ある可能性のあるアプローチを説明しました。それは、全てのアロケーションメソッド(MemorySegment::allocateNativeMemorySegment::mapFile)がセグメントを直接返すのではなく、”allocation handle”(アロケーション・ハンドル)を返す、というものです。アロケーション・ハンドルはクローズ可能なエンティティであり、セグメントは単なるビューです。このアプローチは実行可能ですが(実際によく似たものが以下で検討されています)、ある部分を実装後、このアプローチが外部リンカのサポートに関して、どのように統合されているかについては満足できませんでした。

Memory Package (Apache DataSketches)
https://datasketches.apache.org/docs/Memory/MemoryPackage.html

例えば、CLinker::toCStringのようなメソッドの動作を定義することは、非常に複雑になります。返ってきた文字列に関連付けられているアロケーション・ハンドルはどこに由来するのでしょうか。セグメントがハンドルへのポインタを持たない場合、文字列に関連付けられたメモリはどのようにクローズできるのでしょうか?アロケーション・ハンドルとNativeScopeの関係はどうなっているのでしょうか?これらの疑問から、提案されたアプローチでは不十分であり、もっと努力する必要があると結論づけました。

上記のアプローチは、メモリセグメントをメモリリソースの割り当て/閉鎖を管理するエンティティから分離することで、メモリセグメントをダムビューに変えるという点では正しいのですが、この方向性では十分ではありません。結局のところ、ここで本当に必要なのは、1つ以上の(論理的に関連する)リソースに関連するライフサイクルの概念を捉える方法ですが、これは当然のことながら、NativeScopeが行うことの一部でもあります。そこで、この抽象的な概念をモデル化してみましょう。

interface ResourceScope extends AutoCloseable {
   void addOnClose(Runnable) // adds a new cleanup action to this scope
   void close() // closes the scope

   static ResourceScope ofConfined() // creates a confined resource scope
   static ResourceScope ofShared() // creates a shared resource scope
   static ResourceScope ofConfined(Cleaner) // creates a confined resource scope - managed by cleaner
   static ResourceScope ofShared(Cleaner) // creates a shared resource scope - managed by cleaner
}

これは非常にシンプルなインターフェイスで、基本的にはスコープが閉じられたときに呼び出される新しいクリーンアップ・アクションを追加できます。ResourceScopeは、(Cleanerを使った)暗黙的なクローズまたは(closeメソッドを使った)明示的なクローズをサポートしていることに注意してください。(ここでは示しませんが)両方をサポートできます。

この新しい抽象化を利用して、既存のAPIメソッドや抽象化に新たな光を当てられるかどうか試してみましょう。

まず、ヒープセグメントについて説明します。ヒープセグメントはMemorySegment::ofArray()ファクトリのいずれかを使用して割り当てられます。ヒープセグメントの問題点のひとつは、それを閉じることにあまり意味がないということです。提案されているアプローチでは、これをうまく処理できます。ヒープセグメントは、閉じることのできないグローバルスコープ(常に生存するスコープ)に関連付けられています。これにより、ヒープセグメント (およびバッファセグメント) の役割が明確になります。

MemorySegment::allocateNative/mapFileに移ります。これらのファクトリは何をすべきでしょうか?新しい提案では、これらのメソッドはResourceScopeパラメータを受け入れることになっています。これは、新しく作成されたセグメントがアタッチされるべきライフサイクルを定義します。ResourceScopeのないオーバーロードを提供したい場合(現在のAPIのように)、便利なデフォルトを選択できます。この選択により、基本的にバイトバッファと同じセマンティクスになるので、ByteBuffer APIから来た開発者が新しいメモリアクセスAPIに慣れるための理想的な出発点となるでしょう。このようなコンパクトなファクトリを使用する場合、スコープはほとんどクライアントから隠されているので、(例えばByteBuffer APIと比較して)余分な複雑さは追加されないことに注意してください。

結論から言うと、ResourceScopeはセグメントだけでなく、以下のようなライフサイクルに接続する必要のある多くのエンティティにも役立ちます。

  • upcall stubs
  • va lists
  • loaded libraries

upcall stubのケースは特に重要です。このケースでは、upcall stubをMemorySegmentとしてモデル化することを決定しました。これは、upcall stubを参照解除することに意味があるからではなく、単に、upcall stubを使い終わった後に解放する方法が必要だからです。この新しい提案では、新しい強力なオプションがあります。upcall stubのAPIポイントは、upcall stubエンティティのライフサイクルを管理する責任を負う、ユーザーが提供するResourceScopeを受け入れることができます。つまり、upcall stubの呼び出し結果を、機能を損なわずにMemorySegment以外のもの(FunctionPointerなど)に自由に変更できるようになったのです。

リソーススコープは、リソースのグループを管理するのに非常に便利です。実際には、1つまたは複数のセグメントが同じライフサイクルを共有する場合、つまり、すべてのセグメントが同時に存続する必要がある場合があります。このようなユースケースを処理するために、現状ではNativeScope抽象化を追加し、外部メモリセグメントの登録を(MemorySegment::handoff APIを使って)受け入れることができるようになっています。このユースケースは、当然ながらResourceScope APIで処理されます。

try (ResourceScope scope : ResourceScope.ofConfined()) {
    MemorySegment.allocateNative(layout, scope):
    MemorySegment.mapFile(… , scope);
    CLinker.upcallStub(… , scope);
} // release all resources

これでNativeScopeの必要性がなくなるのでしょうか?そうはいきません。NativeScopeは、論理的に関連するリソースをグループ化するために使用されますが、より高速なアリーナベースのアロケータとしても使用されます。これは、より大きなメモリブロックを割り当て、スライスをクライアントに引き渡すことで、システムコール(mallocなど)の数を最小限に抑えようとするものです。以下のように、NativeScopeのアロケーションの性質を別のインターフェースでモデル化してみましょう。

@FunctionalInterface
interface NativeAllocator {
    MemorySegment allocate(long size, long align);
    default allocateInt(MemoryLayout intLayout, int value) { … }
    default allocateLong(MemoryLayout intLayout, long value) { … }
    … // all allocation helpers in NativeScope
}

最初は、このインターフェースはあまり機能が追加されていないように見えますが、これは非常に強力です。例えば、クライアントは次のように、シンプルでmallocのようなアロケータを作成できます。

NativeAllocator malloc = (size, align) -> 
     MemorySegment.allocateNative(size, align, ResourceScope.ofConfined());

このアロケータは、新しいconfinedスコープ(これは独立してクローズできます)が背後にある、各割り当てリクエストでメモリの新しい領域を割り当てます。この方法は非常に一般的なので、APIではクライアントがよりコンパクトな方法でアロケータを作成できるようになっています。

NativeAllocator confinedMalloc = NativeAllocator.ofMalloc(ResourceScope::ofConfined);
NativeAllocator sharedMalloc = NativeAllocator.ofMalloc(ResourceScope::ofConfined);

しかし、他の戦略も可能です。

  • arena allocation(アリーナ・アロケーション)
    例:NativeScopeが現在使用しているアロケーション戦略
  • recycling allocation(リサイクリング・アロケーション)
    指定されたレイアウトの1個のセグメントが割り当てられ、割り当てリクエストはそのセグメントだけを繰り返しスライスすることで処理されます) – これは、ループなどでは重要な最適化です。
  • カスタムアロケータとの相互運用

では、APIのどこでNativeAllocatorを受け付けるのでしょうか?APIポイントがネイティブなメモリを割り当てる必要があるときには、アロケータの受け入れが便利であることがわかりました。それゆえ、

MemorySegment toCString(String)

ではなく、こちらのほうがよいです。

MemorySegment toCString(String, NativeAllocator)

もちろん、外部リンカを調整する必要があります。構造体を値で返すすべての外部呼び出し(何らかの割り当てを必要とするもの)では、NativeAllocatorプレフィックス引数をメソッドハンドルに追加し、ユーザーが呼び出しで使用すべきアロケータを指定できるようになります。これは簡単な変更ですが、リンカAPIの表現力を大幅に向上させます。

つまり、あるメソッド(リソースを作成するファクトリなど)は追加のResourceScope引数を取り、他のメソッド(ネイティブセグメントを割り当てる必要があるメソッドなど)は追加のNativeAllocator引数を取るという状況になっています。現時点では、少なくともシンプルなユースケースにおいては、ResourceScopeとNativeAllocatorの両方を実装するユーザーにとっては都合がよろしくありません。しかしこれらはインターフェースなので、ResourceScopeとNativeAllocatorの両方を実装する新しい抽象化を作ることを妨げるものではありません。実はこれこそが、すでに存在するNativeScopeの役割なのです。

interface NativeScope extends NativeAllocator, ResourceScope { … }

言い換えれば、よりプリミティブな抽象化(スコープとアロケータ)の観点からその動作を説明することで、既存のNativeScopeの抽象化をつなぎ直しました。このことは、クライアントはほとんどの場合、NativeScopeを作成し、ResourceScopeやNativeAllocatorが必要なときはいつでもそれを渡すことができることを意味します(これはすべてのjextractの例ですでに起こっていることです)。

このアプローチには、いくつかのボーナスポイントがあります。

まず、ResourceScopeはいくつかのロック機能を備えており、例えば以下のようなことができます。

try (ResourceScope.Lock lock = segment.scope().lock()) {
    <critical operation on segment>
}

これにより、クライアントはセグメントに対するクリティカルな操作を、操作中にセグメントのメモリを再利用されることを気にせずに行うことができます。これにより、共有セグメントから派生したバイトバッファに対する非同期操作の問題が解決されます。

Interactions between memory segments and byte buffers
https://mail.openjdk.java.net/pipermail/panama-dev/2021-January/011810.html

もう一つのボーナスポイントは、ResourceScope インターフェースがセグメントに完全にとらわれないことです。実際、ユーザー(または暗黙のうちにGC)がクリーニングしなければならないリソースを返す API を記述する方法ができました。例えば、ある日、与えられた(クローズ可能な)スコープに接続されたダイレクトバッファを提供する、追加のファクトリ(例えば、allocateDirect(int size, ResourceScope scope))を提供するByteBuffer APIを想像するのは、全く極端な話ではありません。パフォーマンスや安全性の観点から暗黙のクリーンアップが好まれる他のAPIでも同様の手法を使用できるでしょう。

Resources

(外部リンカAPIへの変更を除く)上記の変更の一部を実装するブランチは以下にあります。

panama-foreign: resourceScopeブランチ
https://github.com/mcimadamore/panama-foreign/tree/resourceScope

オリジナルの提案で説明したAPIの初期javadocはこちらにあります。

Original Proposal (memory access – pulling all the threads)
https://mail.openjdk.java.net/pipermail/panama-dev/2021-January/011894.html
Package jdk.incubator.foreign
http://cr.openjdk.java.net/~mcimadamore/panama/resourceScope-javadoc_v2/javadoc/jdk/incubator/foreign/package-summary.html

コメントを残す

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

WordPress.com ロゴ

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

Facebook の写真

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

%s と連携中