Bagikan melalui


Mengurangi alokasi memori menggunakan fitur C# baru

Penting

Teknik yang dijelaskan di bagian ini meningkatkan performa saat diterapkan ke jalur panas dalam kode Anda. Jalur panas adalah bagian basis kode Anda yang sering dijalankan dan berulang kali dalam operasi normal. Menerapkan teknik ini ke kode yang tidak sering dijalankan akan berdampak minimal. Sebelum membuat perubahan apa pun untuk meningkatkan performa, sangat penting untuk mengukur garis besar. Kemudian, analisis garis besar tersebut untuk menentukan di mana penyempitan memori terjadi. Anda dapat mempelajari tentang banyak alat lintas platform untuk mengukur performa aplikasi Anda di bagian diagnostik dan instrumentasi. Anda dapat mempraktikkan sesi pembuatan profil dalam tutorial untuk Mengukur penggunaan memori dalam dokumentasi Visual Studio.

Setelah Anda mengukur penggunaan memori dan telah menentukan bahwa Anda dapat mengurangi alokasi, gunakan teknik di bagian ini untuk mengurangi alokasi. Setelah setiap perubahan berturut-turut, ukur penggunaan memori lagi. Pastikan setiap perubahan memiliki dampak positif pada penggunaan memori di aplikasi Anda.

Pekerjaan terkait kinerja di .NET sering kali berarti mengoptimalkan dengan menghapus alokasi dari kode Anda. Setiap blok memori yang Anda alokasikan pada akhirnya harus dibebaskan. Lebih sedikit alokasi mengurangi waktu yang dihabiskan dalam pengumpulan sampah. Ini memungkinkan waktu eksekusi yang lebih dapat diprediksi dengan menghapus koleksi sampah dari jalur kode tertentu.

Taktik umum untuk mengurangi alokasi adalah mengubah struktur data penting dari class jenis ke struct jenis. Perubahan ini berdampak pada semantik penggunaan jenis tersebut. Parameter dan nilai balik sekarang dikirimkan sebagai nilai alih-alih sebagai referensi. Biaya penyalinan nilai dapat diabaikan jika jenisnya kecil, tiga kata atau kurang (mempertimbangkan satu kata dengan ukuran alami satu bilangan bulat). Ini dapat diukur dan dapat memiliki dampak performa nyata untuk jenis yang lebih besar. Untuk melawan dampak penyalinan, pengembang dapat melewatkan jenis-jenis ini dengan ref yang dimaksudkan untuk mendapatkan kembali semantik yang diinginkan.

Fitur C# ref memberi Anda kemampuan untuk mengekspresikan semantik yang diinginkan untuk struct jenis tanpa berdampak negatif pada kegunaan keseluruhannya. Sebelum peningkatan ini, pengembang perlu menggunakan unsafe konstruksi dengan pointer dan memori mentah untuk mencapai pengaruh kinerja yang sama. Pengkompilasi menghasilkan kode yang aman untuk fitur terkait baru ref . Kode aman yang dapat diverifikasi berarti kompilator mendeteksi kemungkinan buffer overruns atau mengakses memori yang tidak dialokasikan atau dibebaskan. Pengkompilasi mendeteksi dan mencegah beberapa kesalahan.

Meneruskan dan mengembalikan berdasarkan referensi

Variabel di C# menyimpan nilai. Dalam tipe struct, nilai adalah isi dari sebuah instance tipe tersebut. Dalam tipe class, nilai adalah referensi ke blok memori yang menyimpan instance tipe. Menambahkan pengubah ref berarti variabel menyimpan referensi ke nilai . Dalam jenis struct, acuan menunjuk ke tempat penyimpanan yang berisi nilai. Pada jenis class, referensi menunjuk ke penyimpanan yang berisi referensi ke blok memori.

Dalam C#, parameter untuk metode dikirim sebagai nilai, dan nilai yang dikembalikan dikembalikan sebagai nilai. Nilai argumen diteruskan ke metode . Nilai argumen pengembalian adalah nilai yang dikembalikan.

Pengubah ref, in, ref readonly, atau out menunjukkan bahwa argumen dikirimkan melalui referensi. Referensi ke lokasi penyimpanan diteruskan ke metode . Menambahkan ref ke tanda tangan metode berarti bahwa nilai pengembalian dikembalikan melalui referensi. Referensi ke lokasi penyimpanan adalah nilai pengembalian.

Anda juga dapat menggunakan penetapan ref untuk memiliki variabel yang merujuk ke variabel lain. Penugasan yang tipikal menyalin nilai dari sisi kanan ke variabel pada sisi kiri penugasan. Penetapan ref menyalin lokasi memori variabel di sisi kanan ke variabel di sisi kiri. Sekarang ref mengacu pada variabel asli:

int anInteger = 42; // assignment.
ref int location = ref anInteger; // ref assignment.
ref int sameLocation = ref location; // ref assignment

Console.WriteLine(location); // output: 42

sameLocation = 19; // assignment

Console.WriteLine(anInteger); // output: 19

Saat menetapkan variabel, Anda mengubah nilainya. Saat Ref menetapkan variabel, Anda mengubah apa yang dirujuknya.

Anda dapat bekerja langsung dengan penyimpanan untuk nilai menggunakan ref variabel, melewati referensi, dan penetapan ref. Aturan cakupan yang diberlakukan oleh pengkompilasi memastikan keamanan saat bekerja langsung dengan penyimpanan.

Pengubah ref readonly dan in menunjukkan bahwa argumen harus diteruskan oleh referensi dan tidak dapat ditetapkan kembali dalam metode . Perbedaannya adalah yang ref readonly menunjukkan bahwa metode menggunakan parameter sebagai variabel. Metode ini mungkin menangkap parameter, atau mungkin mengembalikan parameter dengan referensi baca-saja. Dalam kasus tersebut, Anda harus menggunakan pengubah ref readonly . Jika tidak, pengubah in menawarkan lebih banyak fleksibilitas. Anda tidak perlu menambahkan pengubah in ke argumen untuk in parameter, sehingga Anda dapat memperbarui tanda tangan API yang ada dengan aman menggunakan pengubah in . Pengkompilasi mengeluarkan peringatan jika Anda tidak menambahkan ref pengubah atau in ke argumen untuk ref readonly parameter.

Referensi konteks yang aman

C# menyertakan aturan untuk ref ekspresi untuk memastikan bahwa ref ekspresi tidak dapat diakses di mana penyimpanan yang dirujuknya tidak lagi valid. Pertimbangkan contoh berikut:

public ref int CantEscape()
{
    int index = 42;
    return ref index; // Error: index's ref safe context is the body of CantEscape
}

Pengkompilasi melaporkan kesalahan karena Anda tidak dapat mengembalikan referensi ke variabel lokal dari metode . Pemanggil tidak dapat mengakses penyimpanan yang dirujuk. Konteks aman ref menentukan cakupan di mana ref ekspresi aman untuk diakses atau dimodifikasi. Tabel berikut mencantumkan konteks aman ref untuk jenis variabel. ref bidang tidak dapat dideklarasikan dalam class atau non-ref struct, sehingga baris tersebut tidak ada dalam tabel:

Deklarasi referensi konteks aman
lokal tidak terujuk blok tempat variabel lokal dideklarasikan
parameter non-ref metode saat ini
ref, ref readonly, in parameter metode panggilan
parameter out metode saat ini
class bidang metode panggilan
bidang non-referensi struct metode saat ini
ref bidang dari ref struct metode panggilan

Variabel dapat ref dikembalikan jika konteks aman ref-nya adalah metode panggilan. Jika konteks aman referensinya adalah metode saat ini atau blok, ref pengembalian tidak diperbolehkan. Cuplikan berikut menunjukkan dua contoh. Bidang anggota dapat diakses dari cakupan metode yang memanggil, sehingga konteks aman ref dari bidang kelas atau struct adalah metode yang memanggil. Konteks aman ref untuk parameter dengan ref, atau in pengubah adalah seluruh metode. Keduanya dapat ref dikembalikan dari metode anggota:

private int anIndex;

public ref int RetrieveIndexRef()
{
    return ref anIndex;
}

public ref int RefMin(ref int left, ref int right)
{
    if (left < right)
        return ref left;
    else
        return ref right;
}

Nota

Ketika pengubah ref readonly atau in diterapkan ke parameter, parameter tersebut dapat dikembalikan oleh ref readonly, bukan ref.

Pengkompilasi memastikan bahwa referensi tidak dapat lolos dari konteks aman ref-nya. Anda dapat menggunakan ref parameter, ref return, dan ref variabel lokal dengan aman karena pengompilasi mendeteksi apakah Anda tidak sengaja menulis kode di mana ref ekspresi dapat diakses saat penyimpanannya tidak valid.

Konteks aman dan struktur ref

ref struct jenis memerlukan lebih banyak aturan untuk memastikan mereka dapat digunakan dengan aman. Jenis ref struct dapat menyertakan ref bidang. Itu membutuhkan pengenalan konteks yang aman. Untuk sebagian besar jenis, konteks aman adalah metode panggilan. Dengan kata lain, nilai yang bukan ref struct dapat selalu dikembalikan dari metode.

Secara informal, cakupan yang aman untuk adalah konteks di mana semua field-field ref struct dapat diakses. Dengan kata lain, ini adalah persimpangan konteks aman ref dari semua bidangnya ref . Metode berikut mengembalikan ReadOnlySpan<char> ke bidang milik anggota, sehingga konteks amannya ada pada metode tersebut:

private string longMessage = "This is a long message";

public ReadOnlySpan<char> Safe()
{
    var span = longMessage.AsSpan();
    return span;
}

Sebaliknya, kode berikut menghasilkan kesalahan karena anggota ref field dari Span<int> mengacu pada array bilangan bulat yang dialokasikan di stack. Ini tidak dapat menghindari metode tersebut.

public Span<int> M()
{
    int length = 3;
    Span<int> numbers = stackalloc int[length];
    for (var i = 0; i < length; i++)
    {
        numbers[i] = i;
    }
    return numbers; // Error! numbers can't escape this method.
}

Menyatukan jenis memori

Pengenalan System.Span<T> dan System.Memory<T> menyediakan model terpadu untuk bekerja dengan memori. System.ReadOnlySpan<T> dan System.ReadOnlyMemory<T> menyediakan versi readonly untuk mengakses memori. Semuanya memberikan abstraksi atas blok memori yang menyimpan array elemen serupa. Perbedaannya adalah bahwa Span<T> dan ReadOnlySpan<T> merupakan ref struct jenis sedangkan Memory<T> dan ReadOnlyMemory<T> merupakan struct jenis. Rentang berisi ref field. Oleh karena itu, contoh rentang tidak dapat meninggalkan konteks yang aman. Konteks aman dari ref struct adalah konteks aman ref dari ref field. Implementasi Memory<T> dan ReadOnlyMemory<T> hapus pembatasan ini. Anda menggunakan jenis ini untuk langsung mengakses buffer memori.

Meningkatkan performa dengan keamanan ref

Menggunakan fitur-fitur ini untuk meningkatkan performa melibatkan tugas-tugas ini:

  • Hindari alokasi: Saat Anda mengubah jenis dari class menjadi struct, Anda mengubah cara penyimpanannya. Variabel lokal disimpan pada tumpukan. Anggota disimpan sebaris saat objek kontainer dialokasikan. Perubahan ini berarti lebih sedikit alokasi dan yang mengurangi pekerjaan yang dilakukan pengumpul sampah. Ini mungkin juga mengurangi tekanan memori sehingga pengumpul sampah berjalan lebih jarang.
  • Mempertahankan semantik referensi: Mengubah tipe dari class menjadi struct mengubah semantik saat meneruskan variabel ke metode. Kode yang memodifikasi status parameternya perlu dimodifikasi. Sekarang setelah parameter adalah struct, metode memodifikasi salinan objek asli. Anda dapat memulihkan semantik asli dengan meneruskan parameter tersebut sebagai ref parameter. Setelah perubahan itu, metode tersebut memodifikasi struct yang asli lagi.
  • Hindari menyalin data: Menyalin jenis yang lebih besar struct dapat memengaruhi performa di beberapa jalur kode. Anda juga dapat menambahkan pengubah ref untuk meneruskan struktur data yang lebih besar ke metode berdasarkan referensi, bukan berdasarkan nilai.
  • Batasi modifikasi: Saat struct tipe dikirimkan melalui referensi, metode yang dipanggil dapat mengubah status struktur. Anda dapat mengganti pengubah ref dengan pengubah ref readonly atau in untuk menunjukkan bahwa argumen tidak dapat dimodifikasi. Lebih suka ref readonly ketika metode mengambil parameter atau mengembalikannya dengan referensi baca-saja. Anda juga dapat membuat tipe readonly struct atau struct dengan anggota readonly untuk memberikan kontrol lebih lanjut atas anggota dari struct yang bisa dimodifikasi.
  • Memanipulasi memori secara langsung: Beberapa algoritma paling efisien saat memperlakukan struktur data sebagai blok memori yang berisi urutan elemen. Jenis Span dan Memory menyediakan akses aman ke blok memori.

Tidak ada teknik ini yang memerlukan unsafe kode. Digunakan dengan bijak, Anda bisa mendapatkan karakteristik performa dari kode aman yang sebelumnya hanya dimungkinkan dengan menggunakan teknik yang tidak aman. Anda dapat mencoba teknik sendiri dalam tutorial tentang mengurangi alokasi memori.