Bagikan melalui


MemoryOwner<T>

MemoryOwner<T> adalah jenis buffer yang mengimplementasikan IMemoryOwner<T>, properti panjang yang disematkan dan serangkaian API berorientasi performa. Ini pada dasarnya adalah pembungkus ringan di sekitar jenis, ArrayPool<T> dengan beberapa utilitas pembantu tambahan.

API Platform: MemoryOwner<T>, AllocationMode

Cara kerjanya

MemoryOwner<T> memiliki fitur utama berikut:

  • Salah satu masalah utama array yang dikembalikan oleh ArrayPool<T> API dan IMemoryOwner<T> instans yang dikembalikan oleh MemoryPool<T> API adalah bahwa ukuran yang ditentukan oleh pengguna hanya digunakan sebagai ukuran minimum : ukuran aktual buffer yang dikembalikan mungkin benar-benar lebih besar. MemoryOwner<T>menyelesaikan ini dengan juga menyimpan ukuran asli yang diminta, sehingga dan Span<T> instans yang Memory<T> diambil darinya tidak perlu diiris secara manual.
  • Saat menggunakan IMemoryOwner<T>, mendapatkan Span<T> untuk buffer yang mendasar memerlukan terlebih dahulu untuk mendapatkan Memory<T> instans, lalu Span<T>. Ini cukup mahal, dan seringkali tidak perlu, karena perantara Memory<T> mungkin sebenarnya tidak diperlukan sama sekali. MemoryOwner<T> sebaliknya memiliki properti tambahan Span yang sangat ringan, karena secara langsung membungkus array internal T[] yang disewa dari kumpulan.
  • Buffer yang disewa dari kumpulan tidak dibersihkan secara default, yang berarti bahwa jika mereka tidak dihapus ketika sebelumnya dikembalikan ke kumpulan, mereka mungkin berisi data sampah. Biasanya, pengguna diharuskan untuk menghapus buffer yang disewa ini secara manual, yang dapat verbose terutama ketika sering dilakukan. MemoryOwner<T> memiliki pendekatan yang lebih fleksibel untuk ini, melalui Allocate(int, AllocationMode) API. Metode ini tidak hanya mengalokasikan instans baru dengan ukuran yang diminta, tetapi juga dapat digunakan untuk menentukan mode alokasi mana yang akan digunakan: baik yang sama dengan ArrayPool<T>, atau yang secara otomatis menghapus buffer yang disewa.
  • Ada kasus di mana buffer mungkin disewa dengan ukuran yang lebih besar dari apa yang sebenarnya diperlukan, dan kemudian diubah ukurannya setelahnya. Ini biasanya akan mengharuskan pengguna untuk menyewa buffer baru dan menyalin wilayah yang menarik dari buffer lama. Sebagai gantinya, MemoryOwner<T> memaparkan Slice(int, int) API yang hanya mengembalikan instans baru yang membungkus area minat yang ditentukan. Ini memungkinkan untuk melewati penyewaan buffer baru dan menyalin item sepenuhnya.

Sintaks

Berikut adalah contoh cara menyewa buffer dan mengambil Memory<T> instans:

// 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;
}

Dalam contoh ini, kami menggunakan using blok untuk mendeklarasikan MemoryOwner<T> buffer: ini sangat berguna karena array yang mendasar akan secara otomatis dikembalikan ke kumpulan di akhir blok. Jika sebaliknya kita tidak memiliki kontrol langsung atas masa MemoryOwner<T> pakai instans, buffer hanya akan dikembalikan ke kumpulan ketika objek diselesaikan oleh pengumpul sampah. Dalam kedua kasus, buffer yang disewa akan selalu dikembalikan dengan benar ke kumpulan bersama.

Kapan ini harus digunakan?

MemoryOwner<T> dapat digunakan sebagai jenis buffer tujuan umum, yang memiliki keuntungan meminimalkan jumlah alokasi yang dilakukan dari waktu ke waktu, karena secara internal menggunakan kembali array yang sama dari kumpulan bersama. Kasus penggunaan umum adalah mengganti new T[] alokasi array, terutama ketika melakukan operasi berulang yang memerlukan buffer sementara untuk dikerjakan, atau yang menghasilkan buffer sebagai hasilnya.

Misalkan kita memiliki himpunan data yang terdiri dari serangkaian file biner, dan bahwa kita perlu membaca semua file ini dan memprosesnya dalam beberapa cara. Untuk memisahkan kode dengan benar, kita mungkin akhirnya menulis metode yang hanya membaca satu file biner, yang mungkin terlihat seperti ini:

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;
}

Perhatikan bahwa new byte[] ekspresi. Jika kita membaca sejumlah besar file, kita akhirnya akan mengalokasikan banyak array baru, yang akan menempatkan banyak tekanan atas pengumpul sampah. Kami mungkin ingin merefaktor kode ini menggunakan buffer yang disewa dari kumpulan, seperti:

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);
}

Dengan menggunakan pendekatan ini, buffer sekarang disewa dari kumpulan, yang berarti bahwa dalam banyak kasus kita dapat melewati alokasi. Selain itu, karena buffer yang disewakan tidak dibersihkan secara default, kami juga dapat menghemat waktu yang diperlukan untuk mengisinya dengan nol, yang memberi kami peningkatan performa kecil lainnya. Dalam contoh di atas, memuat 1000 file akan membawa ukuran alokasi total dari sekitar 1MB menjadi hanya 1024 byte - hanya satu buffer yang akan secara efektif dialokasikan, dan kemudian digunakan kembali secara otomatis.

Ada dua masalah utama dengan kode di atas:

  • ArrayPool<T> mungkin mengembalikan buffer yang memiliki ukuran lebih besar dari yang diminta. Untuk mengatasi masalah ini, kita perlu mengembalikan tuple yang juga menunjukkan ukuran aktual yang digunakan ke dalam buffer sewaan kami.
  • Dengan hanya mengembalikan array, kita harus ekstra hati-hati untuk melacak masa pakainya dengan benar dan mengembalikannya ke kumpulan yang sesuai. Kita mungkin mengatasi masalah ini dengan menggunakan MemoryPool<T> sebagai gantinya dan dengan mengembalikan IMemoryOwner<T> instans, tetapi kita masih memiliki masalah buffer sewaan yang memiliki ukuran yang lebih besar dari apa yang kita butuhkan. Selain itu, IMemoryOwner<T> memiliki beberapa overhead saat mengambil Span<T> untuk dikerjakan, karena itu menjadi antarmuka, dan fakta bahwa kita selalu perlu mendapatkan Memory<T> instans terlebih dahulu, dan kemudian Span<T>.

Untuk mengatasi kedua masalah ini, kita dapat merefaktor kode ini lagi dengan menggunakan 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;
}

Instans yang dikembalikan IMemoryOwner<byte> akan mengurus pembuangan buffer yang mendasar dan mengembalikannya ke kumpulan ketika metodenya IDisposable.Dispose dipanggil. Kita dapat menggunakannya untuk mendapatkan Memory<T> instans atau Span<T> untuk berinteraksi dengan data yang dimuat, lalu membuang instans ketika kita tidak lagi membutuhkannya. Selain itu, semua MemoryOwner<T> properti (seperti MemoryOwner<T>.Span) menghormati ukuran awal yang diminta yang kami gunakan, jadi kami tidak perlu lagi melacak ukuran efektif secara manual dalam buffer yang disewa.

Contoh

Anda dapat menemukan lebih banyak contoh dalam pengujian unit.