Memory and Span pt. 3


Memori <T> dan ReadOnlyMemory <T>


Ada dua perbedaan visual antara Memory<T> dan Span<T> . Yang pertama adalah tipe Memory<T> tidak mengandung pengubah ref di header tipe. Dengan kata lain, tipe Memory<T> dapat dialokasikan baik pada stack sambil menjadi variabel lokal, atau parameter metode, atau nilai yang dikembalikan dan pada heap, merujuk beberapa data dalam memori dari sana. Namun, perbedaan kecil ini menciptakan perbedaan besar dalam perilaku dan kemampuan Memory<T> dibandingkan dengan Span<T> . Tidak seperti Span<T> yang merupakan instrumen untuk beberapa metode untuk menggunakan beberapa buffer data, tipe Memory<T> dirancang untuk menyimpan informasi tentang buffer, tetapi tidak untuk menanganinya. Jadi, ada perbedaan dalam API.


  • Memory<T> tidak memiliki metode untuk mengakses data yang menjadi tanggung jawabnya. Sebagai gantinya, ia memiliki properti Span dan metode Slice yang mengembalikan turunan tipe Span .
  • Selain itu, Memory<T> berisi metode Pin() yang digunakan untuk skenario ketika data buffer yang disimpan harus diteruskan ke kode yang unsafe . Jika metode ini dipanggil saat memori dialokasikan dalam .NET, buffer akan disematkan dan tidak akan bergerak ketika GC aktif. Metode ini akan mengembalikan instance dari struktur MemoryHandle , yang merangkum GCHandle untuk menunjukkan segmen seumur hidup dan untuk pin buffer array dalam memori.

Bab ini diterjemahkan dari bahasa Rusia bersama oleh penulis dan penerjemah profesional . Anda dapat membantu kami dengan terjemahan dari bahasa Rusia atau Inggris ke bahasa lain, terutama ke bahasa Cina atau Jerman.

Juga, jika Anda ingin berterima kasih kepada kami, cara terbaik yang dapat Anda lakukan adalah memberi kami bintang di github atau untuk repositori garpu github / sidristij / dotnetbook .

Namun, saya sarankan agar kita terbiasa dengan seluruh rangkaian kelas. Pertama, mari kita lihat struktur Memory<T> itu sendiri (di sini saya hanya menunjukkan anggota tipe yang saya anggap 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); public Span<T> Span { get; } public unsafe MemoryHandle Pin(); } 

Seperti yang kita lihat struktur berisi konstruktor berdasarkan array, tetapi menyimpan data dalam objek. Ini adalah tambahan string referensi yang tidak memiliki konstruktor yang dirancang untuk mereka, tetapi dapat digunakan dengan metode string AsMemory() , mengembalikan string ReadOnlyMemory . Namun, karena kedua jenis harus sama biner, Object adalah jenis bidang _object .


Selanjutnya, kita melihat dua konstruktor berdasarkan MemoryManager . Kami akan membicarakannya nanti. Properti untuk memperoleh Length (ukuran) dan IsEmpty memeriksa set kosong. Juga, ada metode Slice untuk mendapatkan subset serta metode penyalinan CopyTo dan TryCopyTo .


Berbicara tentang Memory Saya ingin menjelaskan dua metode jenis ini secara terperinci: properti Span dan metode Pin .


Memori <T> .Span


 public Span<T> Span { get { if (_index < 0) { return ((MemoryManager<T>)_object).GetSpan().Slice(_index & RemoveFlagsBitMask, _length); } else if (typeof(T) == typeof(char) && _object is string s) { // This is dangerous, returning a writable span for a string that should be immutable. // However, we need to handle the case where a ReadOnlyMemory<char> was created from a string // and then cast to a Memory<T>. Such a cast can only be done with unsafe or marshaling code, // in which case that's the dangerous operation performed by the dev, and we're just following // suit here to make it work as best as possible. return new Span<T>(ref Unsafe.As<char, T>(ref s.GetRawStringData()), s.Length).Slice(_index, _length); } else if (_object != null) { return new Span<T>((T[])_object, _index, _length & RemoveFlagsBitMask); } else { return default; } } } 

Yakni, garis yang menangani manajemen string. Mereka mengatakan bahwa jika kita mengonversi ReadOnlyMemory<T> ke Memory<T> (hal-hal ini sama dalam representasi biner dan bahkan ada komentar jenis ini harus bertepatan dengan cara biner karena satu dihasilkan dari yang lain dengan memanggil Tidak Unsafe.As ) kita akan mendapatkan ~ akses ke ruang rahasia ~ dengan kesempatan untuk mengubah string. Ini adalah mekanisme yang sangat berbahaya:


 unsafe void Main() { var str = "Hello!"; ReadOnlyMemory<char> ronly = str.AsMemory(); Memory<char> mem = (Memory<char>)Unsafe.As<ReadOnlyMemory<char>, Memory<char>>(ref ronly); mem.Span[5] = '?'; Console.WriteLine(str); } --- Hello? 

Mekanisme ini dikombinasikan dengan string interning dapat menghasilkan konsekuensi yang mengerikan.


Memori <T> .Pin


Metode kedua yang menarik perhatian kuat adalah Pin :


 public unsafe MemoryHandle Pin() { if (_index < 0) { return ((MemoryManager<T>)_object).Pin((_index & RemoveFlagsBitMask)); } else if (typeof(T) == typeof(char) && _object is string s) { // This case can only happen if a ReadOnlyMemory<char> was created around a string // and then that was cast to a Memory<char> using unsafe / marshaling code. This needs // to work, however, so that code that uses a single Memory<char> field to store either // a readable ReadOnlyMemory<char> or a writable Memory<char> can still be pinned and // used for interop purposes. GCHandle handle = GCHandle.Alloc(s, GCHandleType.Pinned); void* pointer = Unsafe.Add<T>(Unsafe.AsPointer(ref s.GetRawStringData()), _index); return new MemoryHandle(pointer, handle); } else if (_object is T[] array) { // Array is already pre-pinned if (_length < 0) { void* pointer = Unsafe.Add<T>(Unsafe.AsPointer(ref array.GetRawSzArrayData()), _index); return new MemoryHandle(pointer); } else { GCHandle handle = GCHandle.Alloc(array, GCHandleType.Pinned); void* pointer = Unsafe.Add<T>(Unsafe.AsPointer(ref array.GetRawSzArrayData()), _index); return new MemoryHandle(pointer, handle); } } return default; } 

Ini juga merupakan instrumen penting untuk penyatuan karena jika kita ingin meneruskan buffer ke kode yang tidak dikelola, kita hanya perlu memanggil metode Pin() dan meneruskan pointer ke kode ini tidak peduli apa pun tipe data yang merujuk pada Memory<T> . Pointer ini akan disimpan di properti dari struktur yang dihasilkan.


 void PinSample(Memory<byte> memory) { using(var handle = memory.Pin()) { WinApi.SomeApiMethod(handle.Pointer); } } 

Tidak masalah apa yang dipanggil Pin() dalam kode ini: bisa berupa Memory yang mewakili T[] , atau string atau buffer memori yang tidak dikelola. Hanya array dan string akan mendapatkan GCHandle.Alloc(array, GCHandleType.Pinned) nyata GCHandle.Alloc(array, GCHandleType.Pinned) dan dalam hal memori yang tidak dikelola tidak akan terjadi apa-apa.


MemoryManager, IMemoryOwner, MemoryPool


Selain menunjukkan bidang struktur, saya ingin mencatat bahwa ada dua konstruktor tipe internal lainnya berdasarkan entitas lain - MemoryManager . Ini bukan manajer memori klasik yang mungkin Anda pikirkan dan kami akan membicarakannya nanti. manajer memori klasik yang mungkin Anda pikirkan dan kami akan membicarakannya nanti. Seperti Span , Memory memiliki referensi ke objek yang dinavigasi, offset, dan ukuran buffer internal. Perhatikan bahwa Anda dapat menggunakan operator new untuk membuat Memory dari array saja. Atau, Anda dapat menggunakan metode ekstensi untuk membuat Memory dari string, array, atau ArraySegment . Maksud saya itu tidak dirancang untuk dibuat dari memori yang tidak dikelola secara manual. Namun, kita dapat melihat bahwa ada metode internal untuk membuat struktur ini menggunakan 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); } 

Struktur ini menunjukkan pemilik rentang memori. Dengan kata lain, Span adalah instrumen untuk bekerja dengan memori, Memory adalah alat untuk menyimpan informasi tentang rentang memori tertentu dan MemoryManager adalah alat untuk mengontrol masa pakai rentang ini, yaitu pemiliknya. Sebagai contoh, kita dapat melihat NativeMemoryManager<T> . Meskipun digunakan untuk tes, tipe ini jelas mewakili 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; } } } } } // Other methods } 

Itu berarti kelas memungkinkan untuk panggilan bersarang metode Pin() , sehingga menghitung referensi yang dihasilkan dari dunia yang unsafe .


Entitas lain yang terkait erat dengan Memory adalah MemoryPool yang MemoryManager instance IMemoryOwner ( IMemoryOwner sebenarnya):


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() { ... } } 

Ini digunakan untuk menyewa buffer dengan ukuran yang diperlukan untuk penggunaan sementara. Instance yang disewa dengan IMemoryOwner<T> diimplementasikan memiliki metode Dispose() untuk mengembalikan array yang disewa kembali ke kumpulan array. Secara default, Anda dapat menggunakan kumpulan buffer yang dapat dibagikan yang dibangun di 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) { } } 

Berdasarkan arsitektur ini, kami memiliki gambar berikut:


  • Jenis data Span harus digunakan sebagai parameter metode jika Anda ingin membaca data ( ReadOnlySpan ) atau membaca dan menulis data ( Span ). Namun, itu tidak seharusnya disimpan dalam bidang kelas untuk penggunaan di masa depan.
  • Jika Anda perlu menyimpan referensi dari bidang kelas ke buffer data, Anda perlu menggunakan Memory<T> atau ReadOnlyMemory<T> tergantung pada tujuan Anda.
  • MemoryManager<T> adalah pemilik buffer data (opsional). Mungkin diperlukan jika Anda perlu menghitung panggilan Pin() misalnya. Atau, jika Anda perlu tahu cara melepaskan memori.
  • Jika Memory dibangun di sekitar rentang memori yang tidak dikelola, Pin() tidak dapat melakukan apa-apa. Namun, seragam ini bekerja dengan berbagai jenis buffer: untuk kode yang dikelola dan tidak dikelola antarmuka interaksi akan sama.
  • Setiap jenis memiliki konstruktor publik. Itu berarti Anda dapat menggunakan Span secara langsung atau mendapatkan instance dari Memory . Untuk Memory , Anda dapat membuatnya secara individual atau Anda dapat membuat rentang memori yang dimiliki oleh IMemoryOwner dan dirujuk oleh Memory . Jenis apa pun yang didasarkan pada MemoryManger dapat dianggap sebagai kasus khusus yang memiliki beberapa rentang memori lokal (misalnya disertai dengan penghitungan referensi dari dunia yang unsafe ). Selain itu, jika Anda perlu MemoryPool buffer tersebut (lalu lintas yang sering diharapkan dari buffer berukuran hampir sama) Anda dapat menggunakan tipe MemoryPool .
  • Jika Anda bermaksud untuk bekerja dengan kode yang unsafe dengan mengirimkan buffer data di sana, Anda harus menggunakan tipe Memory yang memiliki metode Pin() yang secara otomatis pin buffer pada tumpukan .NET jika itu dibuat di sana.
  • Jika Anda memiliki lalu lintas buffer (misalnya Anda menguraikan teks suatu program atau DSL), lebih baik menggunakan tipe MemoryPool . Anda dapat mengimplementasikannya dengan benar untuk menghasilkan buffer dengan ukuran yang diperlukan dari sebuah pool (misalnya buffer yang sedikit lebih besar jika tidak ada yang sesuai, tetapi menggunakan originalMemory.Slice(requiredSize) untuk menghindari fragmentasi pool).

Performa


Untuk mengukur kinerja tipe data baru, saya memutuskan untuk menggunakan perpustakaan yang sudah menjadi standar BenchmarkDotNet :


 [Config(typeof(MultipleRuntimesConfig))] public class SpanIndexer { private const int Count = 100; private char[] arrayField; private ArraySegment<char> segment; private string str; [GlobalSetup] public void Setup() { str = new string(Enumerable.Repeat('a', Count).ToArray()); arrayField = str.ToArray(); segment = new ArraySegment<char>(arrayField); } [Benchmark(Baseline = true, OperationsPerInvoke = Count)] public int ArrayIndexer_Get() { var tmp = 0; for (int index = 0, len = arrayField.Length; index < len; index++) { tmp = arrayField[index]; } return tmp; } [Benchmark(OperationsPerInvoke = Count)] public void ArrayIndexer_Set() { for (int index = 0, len = arrayField.Length; index < len; index++) { arrayField[index] = '0'; } } [Benchmark(OperationsPerInvoke = Count)] public int ArraySegmentIndexer_Get() { var tmp = 0; var accessor = (IList<char>)segment; for (int index = 0, len = accessor.Count; index < len; index++) { tmp = accessor[index]; } return tmp; } [Benchmark(OperationsPerInvoke = Count)] public void ArraySegmentIndexer_Set() { var accessor = (IList<char>)segment; for (int index = 0, len = accessor.Count; index < len; index++) { accessor[index] = '0'; } } [Benchmark(OperationsPerInvoke = Count)] public int StringIndexer_Get() { var tmp = 0; for (int index = 0, len = str.Length; index < len; index++) { tmp = str[index]; } return tmp; } [Benchmark(OperationsPerInvoke = Count)] public int SpanArrayIndexer_Get() { var span = arrayField.AsSpan(); var tmp = 0; for (int index = 0, len = span.Length; index < len; index++) { tmp = span[index]; } return tmp; } [Benchmark(OperationsPerInvoke = Count)] public int SpanArraySegmentIndexer_Get() { var span = segment.AsSpan(); var tmp = 0; for (int index = 0, len = span.Length; index < len; index++) { tmp = span[index]; } return tmp; } [Benchmark(OperationsPerInvoke = Count)] public int SpanStringIndexer_Get() { var span = str.AsSpan(); var tmp = 0; for (int index = 0, len = span.Length; index < len; index++) { tmp = span[index]; } return tmp; } [Benchmark(OperationsPerInvoke = Count)] public void SpanArrayIndexer_Set() { var span = arrayField.AsSpan(); for (int index = 0, len = span.Length; index < len; index++) { span[index] = '0'; } } [Benchmark(OperationsPerInvoke = Count)] public void SpanArraySegmentIndexer_Set() { var span = segment.AsSpan(); for (int index = 0, len = span.Length; index < len; index++) { span[index] = '0'; } } } public class MultipleRuntimesConfig : ManualConfig { public MultipleRuntimesConfig() { Add(Job.Default .With(CsProjClassicNetToolchain.Net471) // Span not supported by CLR .WithId(".NET 4.7.1")); Add(Job.Default .With(CsProjCoreToolchain.NetCoreApp20) // Span supported by CLR .WithId(".NET Core 2.0")); Add(Job.Default .With(CsProjCoreToolchain.NetCoreApp21) // Span supported by CLR .WithId(".NET Core 2.1")); Add(Job.Default .With(CsProjCoreToolchain.NetCoreApp22) // Span supported by CLR .WithId(".NET Core 2.2")); } } 

Sekarang, mari kita lihat hasilnya.


Bagan kinerja


Melihat mereka, kita bisa mendapatkan informasi berikut:


  • ArraySegment mengerikan. Tetapi jika Anda membungkusnya dengan Span Anda bisa membuatnya lebih enak. Dalam hal ini, kinerja akan meningkat 7 kali lipat.
  • Jika kami mempertimbangkan .NET Framework 4.7.1 (hal yang sama untuk 4.5), penggunaan Span akan secara signifikan menurunkan kinerja saat bekerja dengan buffer data. Ini akan berkurang sekitar 30-35%.
  • Namun, jika kita melihat .NET Core 2.1+ kinerjanya tetap sama atau bahkan meningkat mengingat Span dapat menggunakan bagian dari buffer data, menciptakan konteks. Fungsionalitas yang sama dapat ditemukan di ArraySegment , tetapi berfungsi sangat lambat.

Dengan demikian, kita dapat menarik kesimpulan sederhana tentang penggunaan tipe data ini:


  • untuk .NET Framework 4.5+ dan .NET Core mereka memiliki satu-satunya keunggulan: mereka lebih cepat daripada ArraySegment ketika berhadapan dengan subset array asli;
  • dalam .NET Core 2.1+ penggunaannya memberikan keuntungan yang tidak dapat disangkal atas ArraySegment dan implementasi Slice manual;
  • ketiga cara seproduktif mungkin dan itu tidak dapat dicapai dengan alat apa pun untuk menyatukan array.
    Bab ini diterjemahkan dari bahasa Rusia bersama oleh penulis dan penerjemah profesional . Anda dapat membantu kami dengan terjemahan dari bahasa Rusia atau Inggris ke bahasa lain, terutama ke bahasa Cina atau Jerman.


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


All Articles