Perkenalan

Kata kunci In diperkenalkan dengan C# 7.2. Sejujurnya saya tidak mengetahui fitur itu dan apa manfaatnya. Baru-baru ini saya mengalami masalah dengan topik ini saat kelas yang saya adakan sebagai bagian dari akademi bootcamp. Itu adalah salah satu kelas dasar pertama dan saya berbicara tentang parameter passing dengan referensi menggunakan kata kunci ref dan out. Dan saya mendapat pertanyaan tentang kata kunci “di” yang, sejujurnya, membuat saya bingung. Saya tahu bahwa kata kunci ini memungkinkan kita meneruskan parameter dengan referensi dan kita tidak dapat mengubah parameter yang diteruskan menggunakannya di dalam badan metode. Namun saya tidak dapat menyajikan penerapan metode ini dalam praktiknya. Oleh karena itu sebagai seorang programmer dan khususnya sebagai guru saya memutuskan untuk mempelajari sesuatu tentang fungsi ini dan manfaat apa yang akan kita peroleh dengan menggunakannya.

Menurut dokumentasi:

kata kunci in menyebabkan argumen diteruskan dengan referensi tetapi memastikan argumen tersebut tidak diubah

Itu berarti parameternya hanya bisa dibaca dan diteruskan ke metode dengan referensi. Pikiran pertama di kepala saya adalah "Oke, jadi saya bisa membuat program saya lebih cepat ketika saya menambahkan hanya satu kata kunci di mana pun kompiler mengizinkan saya." Khususnya kata kunci harus ditempatkan hanya dalam definisi metode — pemanggilannya sama dengan metode yang meneruskan parameter berdasarkan nilai. Saya telah memutuskan untuk memeriksa teori saya dan yah… hasilnya tidak seperti yang saya harapkan.

Pengujian

Untuk menguji tesis saya, saya telah membuat solusi baru, yang berisi kelas Item dengan beberapa variabel panjang untuk simulasi benda berat.

public class Item
{
    public long ItemId { get; set; }
    public string Title { get; set; }
        
    public double Price { get; set; }
    public long BookshelfId { get; set; }
    public long ImageId { get; set; }
    public int Year { get; set; }
    public int Isin { get; set; }
}

dan saya telah membuat dua metode yang mengembalikan salah satu properti parameter — perbedaan utamanya adalah meneruskannya. Pada metode pertama, parameter dilewatkan dengan cara standar sehingga berdasarkan nilai, tetapi pada metode kedua parameter diteruskan dengan referensi readonly melalui penggunaan kata kunci in.

public static int ReturnIsinWithOutIn(Item item)
{
    return item.Isin;
}
public static int ReturnIsinWithIn(in Item item)
{
    return item.Isin;
}

Langkah terakhir adalah membuat kelas yang menginisialisasi daftar objek dan tolok ukur yang memanggil metode yang kita buat untuk setiap elemen daftar.

[MinColumn, MaxColumn]
public class BenchmarkManager
{
    private readonly Random _random = new Random();
    private readonly List<Item> items = new List<Item>();
    private const int N = 10000;
    public BenchmarkManager()
    {
        for (int i = 0; i < N; i++)
        {
            items.Add(new Item()
            {
                ItemId = _random.NextInt64(),
                BookshelfId = _random.NextInt64(),
                ImageId = _random.NextInt64(),
                Isin = _random.Next(),
            });
        }
    }
    [Benchmark]
    public void ChangeWithoutInClass()
    {
        int sum = 0;
        foreach (Item item in items)
        {
            sum += ItemChanger.ReturnIsinWithOutIn(item);
        }
    }
    [Benchmark]
    public void ChangeWithInClass()
    {
        int sum = 0;
        foreach (Item item in items)
        {
            sum += ItemChanger.ReturnIsinWithIn(item);
        }
    }
}

Satu-satunya yang tersisa hanyalah menjalankan benchmark dan melihat hasilnya. Dan mereka seperti di bawah ini.

|               Method |     Mean |    Error |   StdDev |      Min |      Max |
|--------------------- |---------:|---------:|---------:|---------:|---------:|
| ChangeWithoutInClass | 16.09 us | 0.293 us | 0.274 us | 15.67 us | 16.41 us |
|    ChangeWithInClass | 15.93 us | 0.143 us | 0.120 us | 15.62 us | 16.06 us |

Dan di sini kita harus mampir sebentar. Hasil ini sedikit mengejutkan saya karena, secara teori, akses ke aset seharusnya lebih cepat dengan referensi. Dan memang demikian, tetapi perbedaannya terletak pada kesalahan pengukuran. Tapi tipe referensi seperti kelas pada dasarnya diteruskan dengan referensi atau lebih spesifiknya, salinan referensi. Menurut dokumentasi:

Ketika tipe referensi diteruskan berdasarkan nilai ke suatu metode, metode tersebut menerima salinan referensi ke instance kelas. Artinya, metode yang dipanggil menerima salinan alamat instance, dan metode pemanggilan mempertahankan alamat asli instance tersebut.

Perbedaan antara meneruskan tipe ini berdasarkan nilai dan referensi adalah perubahan status parameter yang dilakukan dalam metode akan terlihat dari pemanggil. Namun karena kata kunci “in” mencegah perubahan status, meneruskan kelas dengan referensi sepertinya tidak ada gunanya. Saya telah melihat kode IL menggunakan ILSpy untuk memeriksa bagaimana kompiler menangani kata kunci.

.method public hidebysig static 
    int32 ReturnIsinWithOutIn (
        class InBenchmark.Item item
    ) cil managed 
{
    // Method begins at RVA 0x247b
    // Header size: 1
    // Code size: 7 (0x7)
    .maxstack 8
 
    IL_0000: ldarg.0
    IL_0001: callvirt instance int32 InBenchmark.Item::get_Isin()
    IL_0006: ret
} // end of method ItemChanger::ReturnIsinWithOutIn
 
.method public hidebysig static 
    int32 ReturnIsinWithIn (
        [in] class InBenchmark.Item& item
    ) cil managed 
{
    .param [1]
        .custom instance void
[System.Runtime]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
         01 00 00 00
     )
     // Method begins at RVA 0x2483
     // Header size: 1
     // Code size: 8 (0x8)
     .maxstack 8
 
     IL_0000: ldarg.0
     IL_0001: ldind.ref
     IL_0002: callvirt instance int32 InBenchmark.Item::get_Isin()
     IL_0007: ret
} // end of method ItemChanger::ReturnIsinWithIn

Hanya ada satu perbedaan signifikan yaitu instruksi tambahan ldind.ref yang secara tidak langsung memuat nilai tipe objek ref sebagai objecy pada stack. Kedua metode membaca parameter menggunakan instruksi ldarg.0 yang lebih spesifik memuat objek ke dalam tumpukan. Jadi praktisnya menggunakan kata kunci dalam metode meneruskan tipe referensi hanya menambahkan instruksi yang tidak perlu ke kode yang dikompilasi.

Oke tapi bagaimana dengan tipe nilai seperti struct? Dalam hal ini meneruskan parameter dengan kata kunci “in” akan membawa manfaat kinerja karena parameter akan diteruskan dengan referensi dan program tidak akan memberikan salinan aset yang diberikan ke metode seperti meneruskannya dengan nilai. Saya telah membuat struct dengan bidang yang sama dengan kelas yang dibuat sebelumnya dan juga membuat dua metode untuk mendapatkan bidang tertentu persis seperti pada contoh kelas.

public struct ItemStruct
{
    public long ItemId { get; set; }
    public string Title { get; set; }
    public double Price { get; set; }
    public long BookshelfId { get; set; }
    public long ImageId { get; set; }
    public int Year { get; set; }
    public int Isin { get; set; }
}
public static int ReturnIsinWithoutInStruct(ItemStruct itemStruct)
{
    return itemStruct.Isin;
}
public static int ReturnIsinWithInStruct(in ItemStruct itemStruct)
{
    return itemStruct.Isin;
}

Tolok ukur yang tepat juga telah dibuat.

private readonly List<ItemStruct> itemsStruct = new List<ItemStruct>();
public BenchmarkManager()
{
    for (int i = 0; i < N; i++)
    {
        items.Add(new Item()
        {
            ItemId = _random.NextInt64(),
            BookshelfId = _random.NextInt64(),
            ImageId = _random.NextInt64(),
            Isin = _random.Next(),
        });
        itemsStruct.Add(new ItemStruct()
        {
            ItemId = _random.NextInt64(),
            BookshelfId = _random.NextInt64(),
            ImageId = _random.NextInt64(),
            Isin = _random.Next(),
        });
    }
}
[Benchmark]
public void ChangeWithoutInStruct()
{
    int sum = 0;
    foreach (ItemStruct item in itemsStruct)
    {
        sum += ItemChanger.ReturnIsinWithoutInStruct(item);
    }
}
[Benchmark]
public void ChangeWithInStruct()
{
    int sum = 0;
    foreach (ItemStruct item in itemsStruct)
    {
        sum += ItemChanger.ReturnIsinWithInStruct(item);
    }
}

Setelah modifikasi benchmark dijalankan sekali lagi dan untuk struct memberikan hasil yang berbeda.

|                Method |     Mean |    Error |   StdDev |      Min |      Max |
|---------------------- |---------:|---------:|---------:|---------:|---------:|
|  ChangeWithoutInClass | 16.37 us | 0.327 us | 0.321 us | 15.89 us | 17.16 us |
|     ChangeWithInClass | 16.11 us | 0.150 us | 0.140 us | 15.92 us | 16.34 us |
| ChangeWithoutInStruct | 44.11 us | 0.423 us | 0.375 us | 43.70 us | 44.72 us |
|    ChangeWithInStruct | 31.45 us | 0.628 us | 0.816 us | 30.20 us | 33.23 us |

Jadi kesimpulannya adalah mekanisme di balik kata kunci bermanfaat ketika ada kebutuhan untuk meneruskan struct sebagai parameter dan tidak ada niat untuk mengubahnya dalam metode. Saya telah mencari kode IL dalam kasus ini juga.

.method public hidebysig static 
    int32 ReturnIsinWithoutInStruct (
        valuetype InBenchmark.ItemStruct itemStruct
    ) cil managed 
{
    // Method begins at RVA 0x248c
    // Header size: 1
    // Code size: 8 (0x8)
    .maxstack 8
 
    IL_0000: ldarga.s itemStruct
    IL_0002: call instance int32 InBenchmark.ItemStruct::get_Isin()
    IL_0007: ret
} // end of method ItemChanger::ReturnIsinWithoutInStruct
 
.method public hidebysig static 
    int32 ReturnIsinWithInStruct (
        [in] valuetype InBenchmark.ItemStruct& itemStruct
    ) cil managed 
{
    .param [1]
        .custom instance void
[System.Runtime]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
        01 00 00 00
    )
    // Method begins at RVA 0x2495
    // Header size: 1
    // Code size: 7 (0x7)
    .maxstack 8
 
    IL_0000: ldarg.0
    IL_0001: call instance int32 InBenchmark.ItemStruct::get_Isin()
    IL_0006: ret
} // end of method ItemChanger::ReturnIsinWithInStruct

Dalam hal ini perbedaan kode IL antara kedua metode terlihat jelas. Metode dengan meneruskan parameter berdasarkan nilai memuat salinan seluruh struct ke tumpukan menggunakan instruksi ldarga.s itemStruct sementara metode yang kita lewati parameter dengan referensi membaca alamat ke struct dan memuat nilai ke tumpukan menggunakan instruksi ldarg.0 persis seperti dengan tipe referensi.

Saya juga telah memeriksa bagaimana kata kunci dalam mengubah perilaku program ketika catatan digunakan tetapi karena itu adalah tipe referensi, hasil benchmark dan kode IL keluaran hampir sama seperti pada contoh kelas.

public record ItemRecord
{
    public long ItemId { get; set; }
    public string Title { get; set; }
    public double Price { get; set; }
    public long BookshelfId { get; set; }
    public long ImageId { get; set; }
    public int Year { get; set; }
    public int Isin { get; set; }
}
public static int ReturnIsinWithoutInRecord(ItemRecord itemRecord)
{
    return itemRecord.Isin;
}
public static int ReturnIsinWithInRecord(in ItemRecord itemRecord)
{
   return itemRecord.Isin;
}
[Benchmark]
public void ChangeWithoutInRecord()
{
    int sum = 0;
    foreach (ItemRecord item in itemsRecord)
    {
        sum += ItemChanger.ReturnIsinWithoutInRecord(item);
    }
}
[Benchmark]
public void ChangeWithoutInRecord()
{
    int sum = 0;
    foreach (ItemRecord item in itemsRecord)
    {
        sum += ItemChanger.ReturnIsinWithInRecord(item);
    }
}
.method public hidebysig static 
    int32 ReturnIsinWithoutInRecord (
        class InBenchmark.ItemRecord itemRecord
    ) cil managed 
{
    // Method begins at RVA 0x249d
    // Header size: 1
    // Code size: 7 (0x7)
    .maxstack 8
 
    IL_0000: ldarg.0
    IL_0001: callvirt instance int32 InBenchmark.ItemRecord::get_Isin()
    IL_0006: ret
} // end of method ItemChanger::ReturnIsinWithoutInRecord
 
.method public hidebysig static 
    int32 ReturnIsinWithInRecord (
        [in] class InBenchmark.ItemRecord& itemRecord
    ) cil managed 
{
    .param [1]
        .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
        01 00 00 00
    )
    // Method begins at RVA 0x24a5
    // Header size: 1
    // Code size: 8 (0x8)
    .maxstack 8
 
    IL_0000: ldarg.0
    IL_0001: ldind.ref
    IL_0002: callvirt instance int32 InBenchmark.ItemRecord::get_Isin()
    IL_0007: ret
} // end of method ItemChanger::ReturnIsinWithInRecord

Dan ada hasil akhir untuk semua tipe :

|                Method |     Mean |    Error |   StdDev |      Min |      Max |
|---------------------- |---------:|---------:|---------:|---------:|---------:|
|  ChangeWithoutInClass | 15.68 us | 0.085 us | 0.080 us | 15.54 us | 15.80 us |
|     ChangeWithInClass | 15.68 us | 0.127 us | 0.113 us | 15.47 us | 15.86 us |
| ChangeWithoutInStruct | 41.99 us | 0.657 us | 0.615 us | 41.20 us | 43.30 us |
|    ChangeWithInStruct | 31.01 us | 0.476 us | 0.445 us | 30.28 us | 31.72 us |
| ChangeWithoutInRecord | 16.10 us | 0.221 us | 0.196 us | 15.77 us | 16.46 us |
|    ChangeWithInRecord | 16.19 us | 0.268 us | 0.250 us | 15.93 us | 16.79 us |

Kesimpulan

Melewati parameter dengan referensi menggunakan kata kunci in bisa berguna ketika kita harus bekerja dengan struct yang berat. Melewati struktur ini dengan referensi memberi kita peningkatan kinerja, namun kita tidak bisa mengatakan hal yang sama ketika berhadapan dengan kelas dan catatan. Tentu saja mungkin ada kasus tertentu di mana bahkan untuk struktur ini meneruskannya dengan referensi hanya-baca mungkin bermanfaat tetapi secara umum yang paling masuk akal adalah menggunakan kata kunci ini hanya ketika kita ingin meneruskan tipe nilai ke metode.

Tautan ke contoh: https://github.com/proxna/in-keyword-benchmark

Sumber: