ref penduduk setempat dan pengembalian ref dalam C #: jebakan kinerja

Sejak awal, C # mendukung argumen lewat nilai atau referensi. Tetapi sebelum versi 7, kompiler C # hanya mendukung satu cara untuk mengembalikan nilai dari metode (atau properti) - kembali dengan nilai. Dalam C # 7, situasinya telah berubah dengan diperkenalkannya dua fitur baru: pengembalian ref dan penduduk lokal ref. Lebih banyak tentang mereka dan kinerja mereka - di bawah potongan.



Alasan


Ada banyak perbedaan antara array dan koleksi lainnya dalam hal runtime bahasa umum. Sejak awal, CLR mendukung array, dan mereka dapat dianggap sebagai fungsi bawaan. Lingkungan CLR dan kompiler JIT dapat bekerja dengan array, dan mereka juga memiliki satu fitur lagi: pengindeks array mengembalikan elemen dengan referensi, dan bukan dengan nilai.

Untuk mendemonstrasikan ini, kita harus beralih ke metode terlarang - gunakan tipe nilai yang bisa diubah:

public struct Mutable { private int _x; public Mutable(int x) => _x = x; public int X => _x; public void IncrementX() { _x++; } } [Test] public void CheckMutability() { var ma = new[] {new Mutable(1)}; ma[0].IncrementX(); // X has been changed! Assert.That(ma[0].X, Is.EqualTo(2)); var ml = new List<Mutable> {new Mutable(1)}; ml[0].IncrementX(); // X hasn't been changed! Assert.That(ml[0].X, Is.EqualTo(1)); } 

Pengujian akan berhasil karena pengindeks array berbeda secara signifikan dari pengindeks Daftar.

Kompiler C # memberikan instruksi khusus kepada pengindeks array - ldelema, yang mengembalikan tautan terkelola ke elemen array ini. Pada dasarnya, pengindeks array mengembalikan elemen dengan referensi. Namun, Daftar tidak dapat berperilaku dengan cara yang sama, karena dalam C # itu tidak mungkin * untuk mengembalikan alias keadaan internal. Oleh karena itu, pengindeks daftar mengembalikan elemen dengan nilai, yaitu, mengembalikan salinan elemen ini.

* Seperti yang akan segera kita lihat, pengindeks Daftar masih tidak dapat mengembalikan elemen dengan referensi.

Ini berarti bahwa ma [0] .IncrementX () memanggil metode yang memodifikasi elemen pertama array, sementara ml [0] .IncrementX () memanggil metode yang memodifikasi salinan elemen tanpa mempengaruhi daftar asli.

Nilai Pengembalian dan Referensi Variabel Lokal: Dasar-dasar


Arti fungsi-fungsi ini sangat sederhana: mendeklarasikan nilai referensi yang dikembalikan memungkinkan Anda untuk mengembalikan alias dari variabel yang ada, dan variabel lokal referensi dapat menyimpan alias tersebut.

1. Contoh sederhana:

 [Test] public void RefLocalsAndRefReturnsBasics() { int[] array = { 1, 2 }; // Capture an alias to the first element into a local ref int first = ref array[0]; first = 42; Assert.That(array[0], Is.EqualTo(42)); // Local function that returns the first element by ref ref int GetByRef(int[] a) => ref a[0]; // Weird syntax: the result of a function call is assignable GetByRef(array) = -1; Assert.That(array[0], Is.EqualTo(-1)); } 

2. Nilai referensi yang dikembalikan dan pengubah hanya baca

Nilai referensi yang dikembalikan dapat mengembalikan alias dari bidang contoh, dan mulai dengan C # versi 7.2, Anda dapat mengembalikan alias tanpa bisa menulis ke objek terkait menggunakan pengubah ref readonly:

 class EncapsulationWentWrong { private readonly Guid _guid; private int _x; public EncapsulationWentWrong(int x) => _x = x; // Return an alias to the private field. No encapsulation any more. public ref int X => ref _x; // Return a readonly alias to the private field. public ref readonly Guid Guid => ref _guid; } [Test] public void NoEncapsulation() { var instance = new EncapsulationWentWrong(42); instance.X++; Assert.That(instance.X, Is.EqualTo(43)); // Cannot assign to property 'EncapsulationWentWrong.Guid' because it is a readonly variable // instance.Guid = Guid.Empty; } 

  • Metode dan properti dapat mengembalikan "alias" dari keadaan internal. Dalam hal ini, metode tugas tidak boleh ditentukan untuk properti.
  • Kembali dengan referensi memecah enkapsulasi, karena klien mendapatkan kontrol penuh atas keadaan internal objek.
  • Kembali melalui tautan hanya baca menghindari penyalinan jenis nilai yang tidak perlu, sambil tidak mengizinkan klien untuk mengubah keadaan internal.
  • Tautan baca-saja dapat digunakan untuk jenis referensi, meskipun ini tidak masuk akal dalam kasus-kasus non-standar.

3. Batasan yang ada. Mengembalikan alias bisa berbahaya: menggunakan alias untuk variabel yang ditempatkan di tumpukan setelah metode selesai akan membuat aplikasi macet. Untuk membuat fungsi ini aman, kompiler C # menerapkan berbagai batasan:

  • Tidak dapat mengembalikan tautan ke variabel lokal.
  • Tidak dapat mengembalikan referensi ke ini dalam struktur.
  • Anda bisa mengembalikan tautan ke variabel yang terletak di heap (misalnya, ke anggota kelas).
  • Anda dapat mengembalikan tautan ke parameter ref / out.

Untuk informasi lebih lanjut, kami sarankan Anda memeriksa publikasi yang sangat baik, Aman untuk mengembalikan aturan pengembalian ref . Penulis artikel, Vladimir Sadov, adalah pencipta fungsi referensi balik untuk kompiler C #.

Sekarang kita memiliki gagasan umum tentang nilai referensi yang dikembalikan dan referensi variabel lokal, mari kita lihat bagaimana mereka dapat digunakan.

Menggunakan nilai referensi yang dikembalikan dalam pengindeks


Untuk menguji dampak fungsi-fungsi ini terhadap kinerja, kami akan membuat koleksi unik dan tidak dapat diubah yang disebut NaiveImmutableList <T> dan membandingkannya dengan T [] dan Daftar untuk struktur dengan ukuran yang berbeda (4, 16, 32 dan 48).

 public class NaiveImmutableList<T> { private readonly int _length; private readonly T[] _data; public NaiveImmutableList(params T[] data) => (_data, _length) = (data, data.Length); public ref readonly T this[int idx] // R# 2017.3.2 is completely confused with this syntax! // => ref (idx >= _length ? ref Throw() : ref _data[idx]); { get { // Extracting 'throw' statement into a different // method helps the jitter to inline a property access. if ((uint)idx >= (uint)_length) ThrowIndexOutOfRangeException(); return ref _data[idx]; } } private static void ThrowIndexOutOfRangeException() => throw new IndexOutOfRangeException(); } struct LargeStruct_48 { public int N { get; } private readonly long l1, l2, l3, l4, l5; public LargeStruct_48(int n) : this() => N = n; } // Other structs like LargeStruct_16, LargeStruct_32 etc 

Tes kinerja dilakukan untuk semua koleksi dan menambahkan semua nilai properti N untuk setiap item:

 private const int elementsCount = 100_000; private static LargeStruct_48[] CreateArray_48() => Enumerable.Range(1, elementsCount).Select(v => new LargeStruct_48(v)).ToArray(); private readonly LargeStruct_48[] _array48 = CreateArray_48(); [BenchmarkCategory("BigStruct_48")] [Benchmark(Baseline = true)] public int TestArray_48() { int result = 0; // Using elementsCound but not array.Length to force the bounds check // on each iteration. for (int i = 0; i < elementsCount; i++) { result = _array48[i].N; } return result; } 

Hasilnya adalah sebagai berikut:



Rupanya, ada sesuatu yang salah! Kinerja koleksi NaiveImmutableList <T> kami sama dengan Daftar. Apa yang terjadi

Kembalikan Nilai dengan Pengubah baca saja: Cara Kerja


Seperti yang Anda lihat, pengindeks NaiveImmutableList <T> mengembalikan tautan read-only menggunakan pengubah readonly ref. Ini sepenuhnya dibenarkan, karena kami ingin membatasi kemampuan pelanggan untuk mengubah kondisi mendasar dari koleksi yang tidak dapat diubah. Namun, struktur yang kami gunakan dalam tes kinerja tidak hanya dapat dibaca.

Tes ini akan membantu kita memahami perilaku dasar:

 [Test] public void CheckMutabilityForNaiveImmutableList() { var ml = new NaiveImmutableList<Mutable>(new Mutable(1)); ml[0].IncrementX(); // X has been changed, right? Assert.That(ml[0].X, Is.EqualTo(2)); } 

Tes gagal! Tapi mengapa? Karena struktur "read-only links" mirip dengan struktur dalam modifiers dan bidang readonly sehubungan dengan struktur: kompiler menghasilkan salinan pelindung setiap kali elemen struktur digunakan. Ini berarti ml [0]. masih membuat salinan dari elemen pertama, tetapi ini tidak dilakukan oleh pengindeks: salinan dibuat di titik panggilan.

Perilaku ini sebenarnya masuk akal. Kompiler C # mendukung argumen yang lewat dengan nilai, dengan referensi, dan dengan “tautan baca-saja” menggunakan modifier in (untuk detail, lihat The-modifier dan struct readonly di C # (“Struktur read-only in dan modifier di C # ")). Sekarang kompiler mendukung tiga cara berbeda untuk mengembalikan nilai dari suatu metode: dengan nilai, dengan referensi, dan dengan tautan hanya-baca.

Tautan baca-saja sangat mirip dengan tautan biasa sehingga kompiler menggunakan InAttribute yang sama untuk membedakan antara nilai pengembaliannya:

 private int _n; public ref readonly int ByReadonlyRef() => ref _n; 

Dalam hal ini, metode ByReadonlyRef mengkompilasi secara efisien menjadi:

 [InAttribute] [return: IsReadOnly] public int* ByReadonlyRef() { return ref this._n; } 

Kesamaan antara modifier in dan link read-only berarti bahwa fungsi-fungsi ini sangat tidak cocok untuk struktur reguler dan dapat menyebabkan masalah kinerja. Pertimbangkan sebuah contoh:

 public struct BigStruct { // Other fields public int X { get; } public int Y { get; } } private BigStruct _bigStruct; public ref readonly BigStruct GetBigStructByRef() => ref _bigStruct; ref readonly var bigStruct = ref GetBigStructByRef(); int result = bigStruct.X + bigStruct.Y; 

Selain sintaks yang tidak biasa ketika mendeklarasikan variabel untuk bigStruct, kodenya terlihat bagus. Tujuannya jelas: BigStruct kembali dengan referensi untuk alasan kinerja. Sayangnya, karena struktur BigStruct dapat ditulis, salinan pelindung dibuat setiap kali item diakses.

Menggunakan nilai referensi yang dikembalikan dalam pengindeks. Percobaan nomor 2


Mari kita coba serangkaian tes yang sama untuk struktur hanya-baca dengan ukuran berbeda:



Sekarang hasilnya jauh lebih masuk akal. Waktu pemrosesan masih meningkat untuk struktur besar, tetapi ini diharapkan, karena memproses lebih dari 100 ribu struktur lebih besar membutuhkan waktu lebih lama. Tetapi sekarang runtime untuk NaiveimmutableList <T> sangat dekat dengan waktu T [] dan jauh lebih baik daripada dalam kasus Daftar.

Kesimpulan


  • Nilai referensi yang dikembalikan harus ditangani dengan hati-hati karena dapat merusak enkapsulasi.
  • Nilai referensi yang dikembalikan dengan pengubah readonly hanya efektif untuk struktur read-only. Dalam kasus struktur konvensional, masalah kinerja dapat terjadi.
  • Saat bekerja dengan struktur yang dapat ditulisi, mengembalikan nilai referensi dengan pengubah readonly membuat salinan pelindung setiap kali variabel digunakan, yang dapat menyebabkan masalah kinerja.

Nilai referensi yang dikembalikan dan variabel lokal yang dirujuk adalah fungsi yang berguna untuk pembuat perpustakaan dan pengembang kode infrastruktur. Namun, mereka sangat berbahaya untuk digunakan dalam kode perpustakaan: untuk menggunakan koleksi yang secara efektif mengembalikan item menggunakan tautan baca-saja, setiap pengguna perpustakaan harus ingat: tautan baca-saja ke struktur yang dapat ditulis menciptakan salinan pelindung “pada titik panggilan ". Dalam kasus terbaik, ini akan meniadakan kemungkinan peningkatan produktivitas, dan yang terburuk akan mengarah pada kemunduran serius jika pada saat yang sama sejumlah besar permintaan dilakukan ke satu variabel referensi lokal, hanya baca.

Tautan baca-saja PS akan muncul di BCL. Metode referensi yang hanya dapat dibaca untuk mengakses item dalam koleksi yang tidak berubah disajikan dalam permintaan berikut untuk menyertakan perubahan dalam repo corefx ( Menerapkan Proposal API ItemRef ("Proposal untuk memasukkan API ItemRef")). Karena itu, sangat penting bagi setiap orang untuk memahami fitur-fitur menggunakan fungsi-fungsi ini dan bagaimana dan kapan mereka harus diterapkan.

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


All Articles