[DotNetBook] Rentang, Memori, dan ReadOnlyMemory

Dengan artikel ini, saya terus menerbitkan serangkaian artikel, yang hasilnya akan menjadi buku tentang karya .NET CLR, dan .NET secara umum. Untuk tautan - selamat datang ke kucing.


Memori <T> dan ReadOnlyMemory <T>


Ada dua perbedaan visual antara Memory<T> dan Span<T> . Yang pertama adalah bahwa tipe Memory<T> tidak mengandung batasan ref pada header tipe. Artinya, dengan kata lain, tipe Memory<T> memiliki hak untuk tidak hanya pada stack, baik variabel lokal atau parameter metode atau nilai kembalinya, tetapi juga pada heap, merujuk dari sana ke beberapa data dalam memori. Namun, perbedaan kecil ini membuat perbedaan besar dalam perilaku dan kemampuan Memory<T> dibandingkan dengan Span<T> . Tidak seperti Span<T> , yang merupakan sarana untuk menggunakan buffer data tertentu untuk beberapa metode, tipe Memory<T> dirancang untuk menyimpan informasi tentang buffer, dan tidak bekerja dengannya.


Catatan


Bab yang diterbitkan di Habré tidak diperbarui dan, mungkin, sudah agak ketinggalan zaman. Dan karena itu, silakan buka teks asli untuk teks yang lebih baru:



Dari sinilah perbedaan dalam API:


  • Memory<T> tidak mengandung metode akses data yang dikelola. Sebagai gantinya, ia memiliki properti Span dan metode Slice , yang mengembalikan pekerja keras - turunan dari tipe Span .
  • Memory<T> juga mengandung metode Pin() , yang dirancang untuk skrip ketika buffer yang disimpan harus diteruskan ke kode yang unsafe . Ketika dipanggil untuk kasus-kasus ketika memori dialokasikan di .NET, buffer akan disematkan dan tidak akan bergerak ketika GC dipicu, kembali kepada pengguna contoh dari struktur MemoryHandle , yang merangkum konsep GCHandle hidup GCHandle yang telah memperbaiki buffer dalam memori:

 public unsafe struct MemoryHandle : IDisposable { private void* _pointer; private GCHandle _handle; private IPinnable _pinnable; /// <summary> ///  MemoryHandle    /// </summary> public MemoryHandle(void* pointer, GCHandle handle = default, IPinnable pinnable = default) { _pointer = pointer; _handle = handle; _pinnable = pinnable; } /// <summary> ///     ,   ,       /// </summary> [CLSCompliant(false)] public void* Pointer => _pointer; /// <summary> ///  _handle  _pinnable,      /// </summary> public void Dispose() { if (_handle.IsAllocated) { _handle.Free(); } if (_pinnable != null) { _pinnable.Unpin(); _pinnable = null; } _pointer = null; } } 

Namun, untuk memulainya, saya mengusulkan untuk berkenalan dengan seluruh rangkaian kelas. Dan sebagai yang pertama dari mereka, lihat struktur Memory<T> (tidak semua anggota tipe ditampilkan, tetapi yang tampaknya paling penting):


  public readonly struct Memory<T> { private readonly object _object; private readonly int _index, _length; public Memory(T[] array) { ... } public Memory(T[] array, int start, int length) { ... } internal Memory(MemoryManager<T> manager, int length) { ... } internal Memory(MemoryManager<T> manager, int start, int length) { ... } public int Length => _length & RemoveFlagsBitMask; public bool IsEmpty => (_length & RemoveFlagsBitMask) == 0; public Memory<T> Slice(int start, int length); public void CopyTo(Memory<T> destination) => Span.CopyTo(destination.Span); public bool TryCopyTo(Memory<T> destination) => Span.TryCopyTo(destination.Span); } 

Selain menentukan bidang struktur, saya memutuskan untuk juga menunjukkan bahwa ada dua konstruktor tipe internal yang bekerja berdasarkan entitas lain - MemoryManager , yang akan dibahas sedikit lebih jauh dan itu bukan sesuatu yang mungkin baru saja Anda bicarakan. berpikir: manajer memori dalam arti klasik. Namun, seperti Span , Memory juga berisi referensi ke objek yang akan dinavigasi, serta offset dan ukuran buffer internal. Juga, perlu dicatat bahwa Memory dapat dibuat dengan operator new hanya berdasarkan array ditambah metode ekstensi - berdasarkan string, array dan ArraySegment . Yaitu penciptaannya berdasarkan memori yang tidak dikelola secara manual tidak tersirat. Namun, seperti yang kita lihat, ada beberapa metode internal untuk membuat struktur ini berdasarkan MemoryManager :


File MemoryManager.cs


 public abstract class MemoryManager<T> : IMemoryOwner<T>, IPinnable { public abstract MemoryHandle Pin(int elementIndex = 0); public abstract void Unpin(); public virtual Memory<T> Memory => new Memory<T>(this, GetSpan().Length); public abstract Span<T> GetSpan(); protected Memory<T> CreateMemory(int length) => new Memory<T>(this, length); protected Memory<T> CreateMemory(int start, int length) => new Memory<T>(this, start, length); void IDisposable.Dispose() protected abstract void Dispose(bool disposing); } 

Saya akan membiarkan diri saya sedikit berdebat dengan terminologi yang diperkenalkan dalam perintah CLR, menamai jenis dengan nama MemoryManager. Ketika saya melihatnya, saya pertama kali memutuskan bahwa itu akan menjadi sesuatu seperti manajemen memori, tetapi manual, selain LOH / SOH. Tapi dia sangat kecewa melihat kenyataan. Mungkin Anda harus menyebutnya dengan analogi dengan antarmuka: MemoryOwner.

Yang merangkum konsep pemilik sepotong memori. Dengan kata lain, jika Span adalah sarana untuk bekerja dengan memori, Memory adalah sarana untuk menyimpan informasi tentang situs tertentu, maka MemoryManager adalah sarana untuk mengendalikan hidupnya, pemiliknya. Misalnya, Anda dapat mengambil tipe NativeMemoryManager<T> , yang, meskipun ditulis untuk pengujian, tidak mencerminkan dengan buruk esensi konsep "kepemilikan":


File NativeMemoryManager.cs


 internal sealed class NativeMemoryManager : MemoryManager<byte> { private readonly int _length; private IntPtr _ptr; private int _retainedCount; private bool _disposed; public NativeMemoryManager(int length) { _length = length; _ptr = Marshal.AllocHGlobal(length); } public override void Pin() { ... } public override void Unpin() { lock (this) { if (_retainedCount > 0) { _retainedCount--; if (_retainedCount == 0) { if (_disposed) { Marshal.FreeHGlobal(_ptr); _ptr = IntPtr.Zero; } } } } } //   } 

Dengan kata lain, kelas menyediakan kemungkinan panggilan bersarang ke metode Pin() , sehingga menghitung tautan yang dihasilkan dari dunia yang unsafe .


Entitas lain yang terkait erat dengan Memory adalah MemoryPool , yang menyediakan MemoryManager instance MemoryManager (dan pada kenyataannya, IMemoryOwner ):


File MemoryPool.cs


 public abstract class MemoryPool<T> : IDisposable { public static MemoryPool<T> Shared => s_shared; public abstract IMemoryOwner<T> Rent(int minBufferSize = -1); public void Dispose() { ... } } 

Yang dirancang untuk mengeluarkan buffer dengan ukuran yang diperlukan untuk penggunaan sementara. Contoh sewaan yang mengimplementasikan IMemoryOwner<T> memiliki metode Dispose() yang mengembalikan array sewaan kembali ke kumpulan array. Dan secara default, Anda dapat menggunakan kumpulan buffer bersama, yang dibangun berdasarkan ArrayMemoryPool :


File ArrayMemoryPool.cs


 internal sealed partial class ArrayMemoryPool<T> : MemoryPool<T> { private const int MaximumBufferSize = int.MaxValue; public sealed override int MaxBufferSize => MaximumBufferSize; public sealed override IMemoryOwner<T> Rent(int minimumBufferSize = -1) { if (minimumBufferSize == -1) minimumBufferSize = 1 + (4095 / Unsafe.SizeOf<T>()); else if (((uint)minimumBufferSize) > MaximumBufferSize) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.minimumBufferSize); return new ArrayMemoryPoolBuffer(minimumBufferSize); } protected sealed override void Dispose(bool disposing) { } } 

Dan atas dasar apa yang dilihatnya, gambar dunia berikut ini tampak:


  • Tipe data Span harus digunakan dalam parameter metode jika Anda bermaksud membaca data ( ReadOnlySpan ) atau menulis ( Span ). Tetapi bukan tugas menyimpannya di bidang kelas untuk digunakan di masa depan
  • Jika Anda perlu menyimpan tautan ke buffer data dari bidang kelas, Anda harus menggunakan Memory<T> atau ReadOnlyMemory<T> - tergantung pada tujuannya
  • MemoryManager<T> adalah pemilik buffer data (Anda tidak dapat menggunakannya: jika perlu). Ini diperlukan ketika, misalnya, ada kebutuhan untuk menghitung panggilan ke Pin() . Atau ketika Anda perlu memiliki pengetahuan tentang cara membebaskan memori
  • Jika Memory dibangun di sekitar area memori yang tidak dikelola, Pin() tidak Pin() melakukan apa-apa. Namun, ini menyatukan pekerjaan dengan berbagai jenis buffer: baik dalam kasus yang dikelola dan dalam kasus kode yang tidak dikelola, antarmuka interaksi akan sama
  • Masing-masing tipe memiliki konstruktor publik. Ini berarti bahwa Anda dapat menggunakan kedua Span secara langsung dan mendapatkan salinannya dari Memory . Anda dapat membuat Memory itu sendiri baik secara terpisah atau mengatur jenis IMemoryOwner yang akan memiliki bagian memori yang akan direferensikan Memory . MemoryManager khusus dapat berupa jenis apa saja yang didasarkan pada MemoryManager : kepemilikan lokal atas sebagian memori (misalnya, dengan penghitungan referensi dari dunia yang unsafe ). Jika pada saat yang sama Anda perlu menarik buffer semacam itu (mengharapkan lalu lintas buffer yang sering dengan ukuran kurang lebih sama), Anda dapat menggunakan tipe MemoryPool .
  • Jika tersirat bahwa Anda perlu bekerja dengan kode yang unsafe , melewati buffer data tertentu di sana, Anda harus menggunakan tipe Memory : ia memiliki metode Pin yang secara otomatis memperbaiki penyangga dalam tumpukan .NET, jika ada yang dibuat di sana.
  • Jika Anda memiliki beberapa lalu lintas buffer (misalnya, Anda memecahkan masalah parsing teks program atau beberapa DSL), ada baiknya menggunakan tipe MemoryPool , yang dapat diatur dengan cara yang sangat benar, menghasilkan buffer dengan ukuran yang sesuai dari kumpulan (misalnya, sedikit lebih besar jika tidak cocok) tetapi dengan memangkas originalMemory.Slice(requiredSize) agar tidak memecah kolam)

Tautan ke seluruh buku



Source: https://habr.com/ru/post/id420051/


All Articles