Memory and Span pt. 2


Rentang contoh penggunaan <T>


Manusia pada dasarnya tidak dapat sepenuhnya memahami tujuan instrumen tertentu sampai ia mendapatkan pengalaman. Jadi, mari kita beralih ke beberapa contoh.


ValueStringBuilder


Salah satu contoh paling menarik sehubungan dengan algoritma adalah tipe ValueStringBuilder . Namun, itu terkubur jauh di dalam mscorlib dan ditandai dengan pengubah internal karena banyak tipe data lain yang sangat menarik. Ini berarti kami tidak akan menemukan instrumen yang luar biasa ini untuk optimasi jika kami belum meneliti kode sumber mscorlib.


Apa kerugian utama dari tipe sistem StringBuilder ? Kelemahan utamanya adalah tipe dan dasarnya - ini adalah tipe referensi dan didasarkan pada char[] , yaitu array karakter. Paling tidak, ini berarti dua hal: kita menggunakan heap (walaupun tidak banyak) dan meningkatkan peluang untuk melewatkan uang CPU.


Masalah lain dengan StringBuilder yang saya hadapi adalah konstruksi string kecil, yaitu ketika string yang dihasilkan harus pendek misalnya kurang dari 100 karakter. Pemformatan singkat menimbulkan masalah kinerja.


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 .

  $"{x} is in range [{min};{max}]" 

Sejauh mana varian ini lebih buruk daripada konstruksi manual melalui StringBuilder ? Jawabannya tidak selalu jelas. Itu tergantung pada tempat konstruksi dan frekuensi memanggil metode ini. Awalnya, string.Format mengalokasikan memori untuk StringBuilder internal yang akan membuat array karakter (SourceString.Length + args.Length * 8). Jika selama konstruksi array ternyata panjangnya tidak ditentukan dengan benar, StringBuilder lain akan dibuat untuk membangun sisanya. Ini akan mengarah pada pembuatan daftar tertaut tunggal. Akibatnya, ia harus mengembalikan string yang dikonstruksi yang berarti penyalinan lain. Itu sia-sia. Akan lebih bagus jika kita bisa menghilangkan mengalokasikan array dari string yang terbentuk di heap: ini akan menyelesaikan salah satu masalah kita.


Mari kita lihat jenis ini dari kedalaman mscorlib :


Kelas ValueStringBuilder
/ src / mscorlib / shared / System / Text / ValueStringBuilder


  internal ref struct ValueStringBuilder { // this field will be active if we have too many characters private char[] _arrayToReturnToPool; // this field will be the main private Span<char> _chars; private int _pos; // the type accepts the buffer from the outside, delegating the choice of its size to a calling party public ValueStringBuilder(Span<char> initialBuffer) { _arrayToReturnToPool = null; _chars = initialBuffer; _pos = 0; } public int Length { get => _pos; set { int delta = value - _pos; if (delta > 0) { Append('\0', delta); } else { _pos = value; } } } // Here we get the string by copying characters from the array into another array public override string ToString() { var s = new string(_chars.Slice(0, _pos)); Clear(); return s; } // To insert a required character into the middle of the string //you should add space into the characters of that string and then copy that character public void Insert(int index, char value, int count) { if (_pos > _chars.Length - count) { Grow(count); } int remaining = _pos - index; _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); _chars.Slice(index, count).Fill(value); _pos += count; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(char c) { int pos = _pos; if (pos < _chars.Length) { _chars[pos] = c; _pos = pos + 1; } else { GrowAndAppend(c); } } [MethodImpl(MethodImplOptions.NoInlining)] private void GrowAndAppend(char c) { Grow(1); Append(c); } // If the original array passed by the constructor wasn't enough // we allocate an array of a necessary size from the pool of free arrays // It would be ideal if the algorithm considered // discreteness of array size to avoid pool fragmentation. [MethodImpl(MethodImplOptions.NoInlining)] private void Grow(int requiredAdditionalCapacity) { Debug.Assert(requiredAdditionalCapacity > _chars.Length - _pos); char[] poolArray = ArrayPool<char>.Shared.Rent(Math.Max(_pos + requiredAdditionalCapacity, _chars.Length * 2)); _chars.CopyTo(poolArray); char[] toReturn = _arrayToReturnToPool; _chars = _arrayToReturnToPool = poolArray; if (toReturn != null) { ArrayPool<char>.Shared.Return(toReturn); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Clear() { char[] toReturn = _arrayToReturnToPool; this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again if (toReturn != null) { ArrayPool<char>.Shared.Return(toReturn); } } // Missing methods: the situation is crystal clear private void AppendSlow(string s); public bool TryCopyTo(Span<char> destination, out int charsWritten); public void Append(string s); public void Append(char c, int count); public unsafe void Append(char* value, int length); public Span<char> AppendSpan(int length); } 

Kelas ini secara fungsional mirip dengan rekan seniornya StringBuilder , walaupun memiliki satu fitur yang menarik dan sangat penting: ini adalah tipe nilai. Itu berarti disimpan dan dilewati sepenuhnya oleh nilai. Juga, pengubah tipe ref baru, yang merupakan bagian dari tanda tangan deklarasi tipe, menunjukkan bahwa tipe ini memiliki kendala tambahan: ia hanya dapat dialokasikan pada stack. Maksud saya menyampaikan instance-nya ke bidang kelas akan menghasilkan kesalahan. Untuk apa semua ini? Untuk menjawab pertanyaan ini, Anda hanya perlu melihat kelas StringBuilder , esensi yang baru saja kami jelaskan:


Kelas StringBuilder /src/mscorlib/src/System/Text/StringBuilder.cs


 public sealed class StringBuilder : ISerializable { // A StringBuilder is internally represented as a linked list of blocks each of which holds // a chunk of the string. It turns out string as a whole can also be represented as just a chunk, // so that is what we do. internal char[] m_ChunkChars; // The characters in this block internal StringBuilder m_ChunkPrevious; // Link to the block logically before this block internal int m_ChunkLength; // The index in m_ChunkChars that represent the end of the block internal int m_ChunkOffset; // The logical offset (sum of all characters in previous blocks) internal int m_MaxCapacity = 0; // ... internal const int DefaultCapacity = 16; 

StringBuilder adalah kelas yang berisi referensi ke array karakter. Jadi, ketika Anda membuatnya, tampak ada dua objek sebenarnya: StringBuilder dan array karakter yang setidaknya berukuran 16 karakter. Inilah sebabnya mengapa sangat penting untuk mengatur panjang string yang diharapkan: itu akan dibangun dengan membuat daftar array yang terhubung dengan masing-masing 16 karakter. Akui, itu sia-sia. Dalam hal tipe ValueStringBuilder , itu berarti tidak ada capacity default, karena meminjam memori eksternal. Juga, ini adalah tipe nilai, dan itu membuat pengguna mengalokasikan buffer untuk karakter pada stack. Dengan demikian, seluruh instance dari suatu jenis diletakkan di tumpukan bersama dengan isinya dan masalah optimasi diselesaikan. Karena tidak perlu mengalokasikan memori pada heap, tidak ada masalah dengan penurunan kinerja ketika berhadapan dengan heap. Jadi, Anda mungkin memiliki pertanyaan: mengapa kita tidak selalu menggunakan ValueStringBuilder (atau analog khusus karena kami tidak dapat menggunakan yang asli karena ini internal)? Jawabannya adalah: itu tergantung pada tugas. Apakah string yang dihasilkan memiliki ukuran yang pasti? Apakah akan diketahui panjang maksimalnya? Jika Anda menjawab "ya" dan jika string tidak melebihi batas yang masuk akal, Anda dapat menggunakan versi nilai dari StringBuilder . Namun, jika Anda mengharapkan string yang panjang, gunakan versi yang biasa.


ValueListBuilder


 internal ref partial struct ValueListBuilder<T> { private Span<T> _span; private T[] _arrayFromPool; private int _pos; public ValueListBuilder(Span<T> initialSpan) { _span = initialSpan; _arrayFromPool = null; _pos = 0; } public int Length { get; set; } public ref T this[int index] { get; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(T item); public ReadOnlySpan<T> AsSpan(); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Dispose(); private void Grow(); } 

Tipe data kedua yang ingin saya perhatikan adalah tipe ValueListBuilder . Ini digunakan ketika Anda perlu membuat kumpulan elemen untuk waktu yang singkat dan meneruskannya ke algoritma untuk diproses.


Akui, tugas ini terlihat sangat mirip dengan tugas ValueStringBuilder . Dan itu diselesaikan dengan cara yang serupa:


File ValueListBuilder.cs coreclr / src /../ Generic / ValueListBuilder.cs


Untuk lebih jelasnya, situasi ini sering terjadi. Namun, sebelumnya kami memecahkan masalah dengan cara lain. Kami dulu membuat List , mengisinya dengan data dan kehilangan referensi untuk itu. Jika metode ini sering dipanggil, ini akan menyebabkan situasi yang menyedihkan: banyak instance List (dan array terkait) ditangguhkan di heap. Sekarang masalah ini terpecahkan: tidak ada objek tambahan yang akan dibuat. Namun, seperti dalam kasus ValueStringBuilder diselesaikan hanya untuk programmer Microsoft: kelas ini memiliki pengubah internal .


Aturan dan praktik penggunaan


Untuk sepenuhnya memahami tipe data baru yang Anda butuhkan untuk bermain dengannya dengan menulis dua atau tiga metode yang lebih baik yang menggunakannya. Namun, dimungkinkan untuk mempelajari peraturan utama saat ini:


  • Jika metode Anda memproses beberapa dataset input tanpa mengubah ukurannya, Anda dapat mencoba menggunakan tipe Span . Jika Anda tidak akan memodifikasi buffer, pilih tipe ReadOnlySpan ;
  • Jika metode Anda menangani string yang menghitung beberapa statistik atau mem-parsing string ini, ia harus menerima ReadOnlySpan<char> . Harus adalah aturan baru. Karena ketika Anda menerima string, Anda membuat seseorang membuat substring untuk Anda;
  • Jika Anda perlu membuat array data pendek (tidak lebih dari 10 kB) untuk suatu metode, Anda dapat dengan mudah mengaturnya menggunakan Span<TType> buf = stackalloc TType[size] . Perhatikan bahwa TType harus menjadi tipe nilai karena stackalloc berfungsi dengan tipe nilai.

Dalam kasus lain, Anda sebaiknya melihat lebih dekat pada Memory atau menggunakan tipe data klasik.


Bagaimana cara kerja span?


Saya ingin mengatakan beberapa kata tambahan tentang bagaimana Span berfungsi dan mengapa itu penting. Dan ada sesuatu untuk dibicarakan. Jenis data ini memiliki dua versi: satu untuk .NET Core 2.0+ dan yang lainnya untuk sisanya.


File Span.Fast.cs, .NET Core 2.0 coreclr /.../ System / Span.Fast.cs **


 public readonly ref partial struct Span<T> { /// A reference to a .NET object or a pure pointer internal readonly ByReference<T> _pointer; /// The length of the buffer based on the pointer private readonly int _length; // ... } 

File ??? [didekompilasi]


 public ref readonly struct Span<T> { private readonly System.Pinnable<T> _pinnable; private readonly IntPtr _byteOffset; private readonly int _length; // ... } 

Masalahnya adalah .NET Framework dan .NET Core 1. * yang besar itu tidak memiliki pengumpul sampah yang diperbarui dengan cara khusus (tidak seperti .NET Core 2.0+) dan mereka harus menggunakan pointer tambahan ke awal buffer di gunakan. Itu berarti, bahwa secara internal Span menangani objek .NET yang dikelola seolah-olah tidak dikelola. Lihat saja varian kedua dari struktur: ia memiliki tiga bidang. Yang pertama adalah referensi ke objek manged. Yang kedua adalah offset dalam byte dari awal objek ini, yang digunakan untuk mendefinisikan awal buffer data (dalam string buffer ini berisi karakter char sedangkan di array berisi data dari array). Terakhir, bidang ketiga berisi jumlah elemen dalam buffer yang diletakkan dalam satu baris.


Mari kita lihat bagaimana Span menangani string, misalnya:


File MemoryExtensions.Fast.cs
coreclr /../ MemoryExtensions.Fast.cs


 public static ReadOnlySpan<char> AsSpan(this string text) { if (text == null) return default; return new ReadOnlySpan<char>(ref text.GetRawStringData(), text.Length); } 

Di mana string.GetRawStringData() terlihat seperti berikut:


File dengan definisi bidang coreclr /../ System / String.CoreCLR.cs


File dengan definisi GetRawStringData coreclr /../ System / String.cs


 public sealed partial class String : IComparable, IEnumerable, IConvertible, IEnumerable<char>, IComparable<string>, IEquatable<string>, ICloneable { // // These fields map directly onto the fields in an EE StringObject. See object.h for the layout. // [NonSerialized] private int _stringLength; // For empty strings, this will be '\0' since // strings are both null-terminated and length prefixed [NonSerialized] private char _firstChar; internal ref char GetRawStringData() => ref _firstChar; } 

Ternyata metode tersebut secara langsung mengakses bagian dalam string, sementara spesifikasi referensi memungkinkan GC untuk melacak referensi yang tidak dikelola ke bagian dalam string dengan memindahkannya bersama-sama dengan string ketika GC aktif.


Hal yang sama dengan array: ketika Span dibuat, beberapa kode JIT internal menghitung offset untuk awal array data dan menginisialisasi Span dengan offset ini. Cara Anda dapat menghitung offset untuk string dan array dibahas dalam bab tentang struktur objek dalam memori (. \ ObjectsStructure.md).


Rentang <T> sebagai nilai yang dikembalikan


Terlepas dari semua harmoni, Span memiliki beberapa kendala logis tetapi tak terduga dalam pengembalian dari suatu metode. Jika kita melihat kode berikut:


 unsafe void Main() { var x = GetSpan(); } public Span<byte> GetSpan() { Span<byte> reff = new byte[100]; return reff; } 

kita dapat melihatnya logis dan baik. Namun, jika kami mengganti satu instruksi dengan instruksi lainnya:


 unsafe void Main() { var x = GetSpan(); } public Span<byte> GetSpan() { Span<byte> reff = stackalloc byte[100]; return reff; } 

kompiler akan melarangnya. Sebelum saya katakan alasannya, saya ingin Anda menebak masalah apa yang dikandung oleh konstruk ini.


Yah, saya harap Anda berpikir, menebak, dan mungkin bahkan mengerti alasannya. Jika ya, upaya saya untuk menulis bab terperinci tentang [utas tumpukan] (./ThreadStack.md) terbayar. Karena ketika Anda mengembalikan referensi ke variabel lokal dari metode yang menyelesaikan tugasnya, Anda bisa memanggil metode lain, tunggu sampai selesai juga, dan kemudian membaca nilai variabel-variabel lokal menggunakan x [0,99].


Untungnya, ketika kami mencoba untuk menulis kode seperti itu kompiler menampar pergelangan tangan kami dengan peringatan: CS8352 Cannot use local 'reff' in this context because it may expose referenced variables outside of their declaration scope . Kompiler benar karena jika Anda melewati kesalahan ini, akan ada peluang, saat dalam plug-in, untuk mencuri kata sandi orang lain atau untuk meningkatkan hak istimewa untuk menjalankan plug-in kami.


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 .

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


All Articles