メモリー・レイアウトおよび構造化アクセス

基本の操作のみを使用して構造化データにアクセスすると、保守が困難で理解しにくいコードになる可能性があります。かわりに、メモリー・レイアウトを使用すると、より複雑なネイティブ・データ型(C構造体など)を効率よく初期化してアクセスすることができます。

たとえば、次のC宣言について考えてみます。これはPoint構造体の配列を定義し、各Point構造体にはPoint.xPoint.yという2つのメンバーが含まれます:

struct Point {
   int x;
   int y;
} pts[10];

このようなネイティブ配列は、次のように初期化およびアクセスできます:

        try (Arena arena = Arena.ofConfined()) {

            MemorySegment segment =
                arena.allocate((long)(2 * 4 * 10), 1);

            for (int i = 0; i < 10; i++) {
                int xValue = i;
                int yValue = i * 10;
                segment.setAtIndex(ValueLayout.JAVA_INT, (i * 2),     xValue);
                segment.setAtIndex(ValueLayout.JAVA_INT, (i * 2) + 1, yValue);
            }
        
            for (int i = 0; i < 10; i++) {
                int xVal = segment.getAtIndex(ValueLayout.JAVA_INT, (i * 2));
                int yVal = segment.getAtIndex(ValueLayout.JAVA_INT, (i * 2) + 1);
                System.out.println("(" + xVal + ", " + yVal + ")");
            }
        }

Arena::allocateメソッドのコールの最初の引数によって、配列に必要なバイト数が計算されます。MemorySegment::setAtIndexメソッドのコールの引数によって、Point構造体の各メンバーに書き込むためのメモリー・アドレスのオフセットが計算されます。同様の引数は、MemorySegment::getAtIndexメソッドに対して同じ計算を実行します。これらの計算を回避するには、メモリー・レイアウトを使用できます。

Point構造体の配列を表すために、次の例ではシーケンス・メモリー・レイアウトを使用します:

        try (Arena arena = Arena.ofConfined()) {
            
            SequenceLayout ptsLayout
                = MemoryLayout.sequenceLayout(10,
                    MemoryLayout.structLayout(
                        ValueLayout.JAVA_INT.withName("x"),
                        ValueLayout.JAVA_INT.withName("y")));

            VarHandle xHandle
                = ptsLayout.varHandle(PathElement.sequenceElement(),
                    PathElement.groupElement("x"));
            VarHandle yHandle
                = ptsLayout.varHandle(PathElement.sequenceElement(),
                    PathElement.groupElement("y"));            

            MemorySegment segment = arena.allocate(ptsLayout);
            
            for (int i = 0; i < ptsLayout.elementCount(); i++) {
                int xValue = i;
                int yValue = i * 10;
                xHandle.set(segment, 0L, (long) i, xValue);
                yHandle.set(segment, 0L, (long) i, yValue);
            }
            
            for (int i = 0; i < ptsLayout.elementCount(); i++) {
                int xVal = (int) xHandle.get(segment, 0L, (long) i);
                int yVal = (int) yHandle.get(segment, 0L, (long) i);
                System.out.println("(" + xVal + ", " + yVal + ")");
            }
        }

最初の文によって、SequenceLayoutオブジェクトで表されるシーケンス・メモリー・レイアウトが作成されます。これには、StructLayoutオブジェクトで表される10個の構造体レイアウトのシーケンスが含まれます。メソッドMemoryLayout::structLayoutは、StructLayoutオブジェクトを返します。各構造体レイアウトには、xyという名前の2つのJAVA_INT値レイアウトが含まれます:

            SequenceLayout ptsLayout
                = MemoryLayout.sequenceLayout(10,
                    MemoryLayout.structLayout(
                        ValueLayout.JAVA_INT.withName("x"),
                        ValueLayout.JAVA_INT.withName("y")));

事前定義値ValueLayout.JAVA_INTには、Java int値に必要なバイト数に関する情報が含まれます。

次の文によって、メモリー・アドレス・オフセットを取得する2つのメモリー・アクセスVarHandleが作成されます。VarHandleは、動的な強い型指定の参照であり、変数を参照するか、パラメータによって定義された変数ファミリ(静的フィールド、非静的フィールド、配列要素、オフヒープ・データ構造体のコンポーネント)を参照します。

            VarHandle xHandle
                = ptsLayout.varHandle(PathElement.sequenceElement(),
                    PathElement.groupElement("x"));
            VarHandle yHandle
                = ptsLayout.varHandle(PathElement.sequenceElement(),
                    PathElement.groupElement("y")); 

メソッドPathElement.sequenceElement()は、シーケンス・レイアウトからメモリー・レイアウトを取得します。この例では、構造体レイアウトの1つをptsLayoutから取得します。メソッド・コールPathElement.groupElement("x")は、xという名前のメモリー・レイアウトを取得します。名前が付いたメモリー・レイアウトを作成するにはwithName(String)メソッドを使用します。

for文によって、VarHandle::setおよびVarHandle::getがコールされ、MemorySegment::setAtIndexおよびMemorySegment::getAtIndexのようにメモリーにアクセスします。

            MemorySegment segment = arena.allocate(ptsLayout);
            
            for (int i = 0; i < ptsLayout.elementCount(); i++) {
                int xValue = i;
                int yValue = i * 10;
                xHandle.set(segment, 0L, (long) i, xValue);
                yHandle.set(segment, 0L, (long) i, yValue);
            }
            
            for (int i = 0; i < ptsLayout.elementCount(); i++) {
                int xVal = (int) xHandle.get(segment, 0L, (long) i);
                int yVal = (int) yHandle.get(segment, 0L, (long) i);
                System.out.println("(" + xVal + ", " + yVal + ")");
            }

この例では、setメソッドは4つの引数を使用します:

  1. segment: 値を設定するメモリー・セグメント
  2. 0L: ベース・オフセット。配列の先頭を指すlong座標です。
  3. (long) i: 値を設定する配列索引を示す2番目のlong座標
  4. xValueおよびyValue: 設定する実際の値

VarHandle xHandleおよびyHandleは、Point構造体のサイズ(8バイト)とそのintメンバーのサイズ(4バイト)を認識しています。つまり、setAtIndexメソッドのように、配列の要素またはメモリー・アドレス・オフセットに必要なバイト数を計算する必要はありません。

ヒント:

ベース・オフセットを使用すると、VarHandleに追加のオフセット計算を挿入することで、複雑なアクセス操作を表現できます。特に、メモリー・セグメントとベース・オフセットを使用して、可変長配列をモデル化できます。これらは、サイズが静的に認識されず、シーケンス・レイアウトを使用して表すことができない配列です。このようなメモリー・セグメントには、MemoryLayout::arrayElementVarHandleメソッドを使用してアクセスできます。例については、MemoryLayoutインタフェースのJavaDoc APIドキュメントの可変長配列の操作に関する項を参照してください。