次の方法で共有


MemoryOwner<T>

MemoryOwner<T> は、IMemoryOwner<T> (組み込みの長さプロパティと一連のパフォーマンス指向 API) を実装するバッファー型です。 基本的に、いくつかの追加のヘルパー ユーティリティを備えた ArrayPool<T> 型の軽量なラッパーです。

プラットフォーム API: MemoryOwner<T>AllocationMode

しくみ

MemoryOwner<T> には、次の主な機能があります。

  • ArrayPool<T> API から返される配列と、MemoryPool<T> API から返される IMemoryOwner<T> インスタンスの主な問題の 1 つは、ユーザーが指定したサイズが単に "最小" サイズとして使われることです。返されるバッファーの実際のサイズは大きくなる可能性があります。 この問題を解決するには、MemoryOwner<T> を使って元の要求されたサイズも保存します。これにより、取得される Memory<T>Span<T> の各インスタンスを手動でスライスする必要がなくなります。
  • IMemoryOwner<T> を使う場合、基となるバッファーの Span<T> を取得するには、まず Memory<T> インスタンスを取得し、次に Span<T> を取得する必要があります。 これはかなりコストが高く、多くの場合は不要です。中間の Memory<T> が実際にはまったく必要ない可能性があるためです。 代わりに、MemoryOwner<T> には Span プロパティもあります。プールから借用されている内部 T[] 配列を直接ラップするため、非常に軽量です。
  • プールから借用されたバッファーは既定ではクリアされません。つまり、以前にプールに戻されたときにクリアされなかった場合、ガベージ データが含まれている可能性があります。 通常、これらの借用されたバッファーはユーザーが手動でクリアする必要がありますが、特に頻繁に行う場合は冗長になる可能性があります。 MemoryOwner<T> には、Allocate(int, AllocationMode) API を使った、より柔軟なアプローチがあります。 このメソッドは、要求されたサイズとまったく同じ新しいインスタンスを割り当てるだけでなく、使う割り当てモード (ArrayPool<T> と同じもの、または借用したバッファーを自動的にクリアするもの) を指定するために使うこともできます。
  • 実際に必要なサイズよりも大きいバッファーを借用し、後でサイズを変更することがあります。 通常、このような場合、ユーザーは新しいバッファーを借用し、古いバッファーから対象領域をコピーする必要があります。 代わりに、MemoryOwner<T> は、指定した対象領域をラップする新しいインスタンスを単に返す Slice(int, int) API を公開しています。 これを使うと、新しいバッファーの借用と項目全体のコピーを省略できます。

構文

バッファーを借用して Memory<T> インスタンスを取得する方法の例を次に示します。

// Be sure to include this using at the top of the file:
using Microsoft.Toolkit.HighPerformance.Buffers;

using (MemoryOwner<int> buffer = MemoryOwner<int>.Allocate(42))
{
    // Both memory and span have exactly 42 items
    Memory<int> memory = buffer.Memory;
    Span<int> span = buffer.Span;

    // Writing to the span modifies the underlying buffer
    span[0] = 42;
}

この例では、using ブロックを使って MemoryOwner<T> バッファーを宣言しました。これが特に便利なのは、基となる配列がブロックの最後でプールに自動的に返されることです。 MemoryOwner<T> インスタンスの有効期間を直接制御できない場合、オブジェクトがガベージ コレクターによって終了されると、バッファーは単にプールに返されます。 どちらの場合も、借用されたバッファーは常に適切に共有プールに返されます。

使うべき場合

MemoryOwner<T> は汎用のバッファー型として使用できます。これには、共有プールから同じ配列を内部的に再利用するため、時間の経過と共に行われる割り当て数を最小限に抑えるという利点があります。 一般的なユース ケースは、new T[] 配列の割り当てを置き換えることです。特に、作業に一時バッファーを必要とする操作や、結果としてバッファーを生成する操作を繰り返し実行する場合です。

たとえば、一連のバイナリ ファイルで構成されるデータセットがあり、これらすべてのファイルを読み取り、何らかの方法で処理する必要があるとします。 コードを適切に分離するには、次のような 1 つのバイナリ ファイルを単に読み取るメソッドを記述します。

public static byte[] GetBytesFromFile(string path)
{
    using Stream stream = File.OpenRead(path);

    byte[] buffer = new byte[(int)stream.Length];

    stream.Read(buffer, 0, buffer.Length);

    return buffer;
}

new byte[] 式に注意してください。 大量のファイルを読み取ると、多くの新しい配列を割り当てることになり、ガベージ コレクターに大きな負荷がかかります。 次のように、プールから借用したバッファーを使ってこのコードをリファクタリングすることができます。

public static (byte[] Buffer, int Length) GetBytesFromFile(string path)
{
    using Stream stream = File.OpenRead(path);

    byte[] buffer = ArrayPool<T>.Shared.Rent((int)stream.Length);

    stream.Read(buffer, 0, (int)stream.Length);

    return (buffer, (int)stream.Length);
}

このアプローチを使うと、バッファーはプールから借用されるようになります。つまり、ほとんどの場合、割り当てをスキップできます。 さらに、借用されたバッファーは既定ではクリアされないため、バッファーをゼロで埋めるのに必要な時間を節約することもできます。その結果、パフォーマンスがさらに少し向上します。 上の例では、1000 個のファイルを読み込むと、合計割り当てサイズが約 1 MB からわずか 1024 バイトに減り、1 つのバッファーだけが効果的に割り当てられ、自動的に再利用されます。

上記のコードには、主に 2 つの問題があります。

  • ArrayPool<T> は、要求されたサイズよりも大きいサイズのバッファーを返す可能性があります。 この問題を回避するには、実際に使われたサイズも示すタプルを、借用されたバッファーに返す必要があります。
  • 単に配列を返すことで、その有効期間を適切に追跡し、適切なプールに返すように細心の注意を払う必要があります。 代わりに MemoryPool<T> を使い、IMemoryOwner<T> インスタンスを返すことでこの問題を回避できる可能性がありますが、借用されたバッファーのサイズが必要以上に大きい問題は依然として残ります。 さらに、IMemoryOwner<T> には、作業対象の Span<T> を取得するときにオーバーヘッドが発生します。これはインターフェイスであり、最初に Memory<T> インスタンス、次に Span<T> の順に取得する必要があるためです。

これら両方の問題を解決するには、MemoryOwner<T> を使ってこのコードを再度リファクタリングします。

public static MemoryOwner<byte> GetBytesFromFile(string path)
{
    using Stream stream = File.OpenRead(path);

    MemoryOwner<byte> buffer = MemoryOwner<byte>.Allocate((int)stream.Length);

    stream.Read(buffer.Span);

    return buffer;
}

返された IMemoryOwner<byte> インスタンスは、その IDisposable.Dispose メソッドが呼び出されたときに、基となるバッファーを破棄し、それをプールに返す処理を行います。 これを使って、読み込まれたデータを操作する Memory<T> または Span<T> インスタンスを取得し、不要になったらインスタンスを破棄できます。 さらに、すべての MemoryOwner<T> プロパティ (MemoryOwner<T>.Span など) は、使っていた初期要求サイズを尊重するため、借用されたバッファー内の有効なサイズを手動で追跡する必要はなくなります。

単体テスト」では、さらに他の例を見つけることができます。