การแนะนำ

คีย์เวิร์ด In ถูกนำมาใช้กับ C# 7.2 พูดตามตรงฉันไม่ได้ตระหนักถึงคุณสมบัตินั้นและประโยชน์ที่ได้รับ เมื่อเร็ว ๆ นี้ฉันพบปัญหาในหัวข้อนี้ในขณะที่ชั้นเรียนที่ฉันกำลังดำเนินการเป็นส่วนหนึ่งของสถาบัน bootcamp มันเป็นหนึ่งในคลาสพื้นฐานคลาสแรกๆ และฉันเล่าเกี่ยวกับพารามิเตอร์ที่ส่งผ่านโดยการอ้างอิงโดยใช้คีย์เวิร์ด ref และ out และฉันมีคำถามเกี่ยวกับคีย์เวิร์ด "ใน" ซึ่งพูดตามตรงทำให้ฉันนิ่งงัน ฉันรู้ว่าคีย์เวิร์ดนี้ช่วยให้เราส่งพารามิเตอร์โดยการอ้างอิง และเราไม่สามารถเปลี่ยนพารามิเตอร์ที่ส่งผ่านโดยใช้คีย์เวิร์ดนั้นภายในเนื้อหาของวิธีการได้ แต่ฉันไม่สามารถนำเสนอการประยุกต์ใช้วิธีนี้ในทางปฏิบัติได้ ด้วยเหตุนี้ในฐานะโปรแกรมเมอร์และโดยเฉพาะอย่างยิ่งในฐานะครู ฉันจึงตัดสินใจเรียนรู้บางอย่างเกี่ยวกับฟังก์ชันนี้ และเราจะได้รับประโยชน์อะไรบ้างจากการใช้ฟังก์ชันเหล่านี้

ตามเอกสาร:

คำหลัก in ทำให้อาร์กิวเมนต์ถูกส่งผ่านโดยการอ้างอิง แต่ต้องแน่ใจว่าไม่มีการแก้ไขอาร์กิวเมนต์

นั่นหมายความว่าพารามิเตอร์เป็นแบบอ่านอย่างเดียวและส่งผ่านไปยังวิธีการโดยการอ้างอิง ความคิดแรกในหัวของฉันคือ “โอเค ฉันจะสามารถทำให้โปรแกรมเร็วขึ้นได้เมื่อฉันเพิ่มคีย์เวิร์ดเพียงคำเดียวทุกที่ที่คอมไพเลอร์อนุญาต” โดยเฉพาะอย่างยิ่งต้องวางคีย์เวิร์ดในคำจำกัดความของเมธอดเท่านั้น การเรียกจะเหมือนกับในเมธอดที่ส่งผ่านพารามิเตอร์ตามค่า ฉันตัดสินใจตรวจสอบทฤษฎีของตัวเองแล้ว และผลลัพธ์ก็ไม่ใช่สิ่งที่ฉันคาดหวังไว้

การทดสอบ

เพื่อทดสอบวิทยานิพนธ์ของฉัน ฉันได้สร้างโซลูชันใหม่ซึ่งประกอบด้วยรายการคลาสที่มีตัวแปรยาวสองสามตัวสำหรับจำลองวัตถุที่มีน้ำหนักมาก

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; }
}

และฉันได้สร้างสองวิธีที่ส่งคืนหนึ่งในคุณสมบัติของพารามิเตอร์ ความแตกต่างหลักคือการส่งผ่านมัน ในวิธีแรก พารามิเตอร์จะถูกส่งผ่านด้วยวิธีมาตรฐาน ดังนั้นตามค่า แต่วิธีที่สองพารามิเตอร์จะถูกส่งผ่านโดยการอ้างอิงแบบอ่านอย่างเดียวผ่านการใช้คีย์เวิร์ด in

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

ขั้นตอนสุดท้ายคือการสร้างคลาสที่เริ่มต้นรายการวัตถุและการวัดประสิทธิภาพซึ่งเรียกวิธีการที่เราสร้างขึ้นสำหรับแต่ละองค์ประกอบของรายการ

[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);
        }
    }
}

สิ่งที่เหลืออยู่คือเพียงแค่เรียกใช้การวัดประสิทธิภาพและดูผลลัพธ์ และก็มีดังข้างล่างนี้

|               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 |

และที่นี่เราก็ต้องแวะกันสักพัก ผลลัพธ์เหล่านี้ทำให้ฉันประหลาดใจเล็กน้อยเพราะตามทฤษฎีแล้ว การเข้าถึงเนื้อหาควรจะเร็วขึ้นโดยการอ้างอิง และเป็นเพียงความแตกต่างภายในข้อผิดพลาดในการวัด แต่ประเภทการอ้างอิงเช่นคลาสโดยพื้นฐานแล้วจะถูกส่งผ่านโดยการอ้างอิงหรือเฉพาะเจาะจง นั่นคือสำเนาของการอ้างอิง ตามเอกสาร:

เมื่อประเภทการอ้างอิงถูกส่งผ่านโดยค่าไปยังวิธีการ วิธีการนั้นจะได้รับสำเนาของการอ้างอิงไปยังอินสแตนซ์ของคลาส นั่นคือ วิธีการเรียกจะได้รับสำเนาที่อยู่ของอินสแตนซ์ และวิธีการเรียกจะเก็บที่อยู่เดิมของอินสแตนซ์ไว้

ความแตกต่างระหว่างการส่งประเภทเหล่านี้ตามค่าและโดยการอ้างอิงคือการเปลี่ยนแปลงสถานะของพารามิเตอร์ที่ทำภายในวิธีการจะมองเห็นได้จากผู้เรียก แต่เนื่องจากคีย์เวิร์ด "ใน" ป้องกันการเปลี่ยนสถานะ การผ่านคลาสโดยการอ้างอิงจึงดูไม่มีจุดหมาย ฉันได้ดูโค้ด IL โดยใช้ ILSpy เพื่อตรวจสอบว่าคอมไพเลอร์จัดการกับคำหลักอย่างไร

.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

มีความแตกต่างที่สำคัญเพียงอย่างเดียวคือคำสั่งเพิ่มเติม ldind.ref ซึ่งโหลดค่าของประเภท object ref ทางอ้อมเป็น objecy บนสแต็ก ทั้งสองวิธีอ่านพารามิเตอร์โดยใช้คำสั่ง ldarg.0 ซึ่งจะเจาะจงมากขึ้นในการโหลดวัตถุลงบนสแต็ก ดังนั้นในทางปฏิบัติแล้ว การใช้คีย์เวิร์ดในวิธีการส่งประเภทการอ้างอิงจะเพิ่มคำสั่งที่ไม่จำเป็นให้กับโค้ดที่คอมไพล์แล้วเท่านั้น

โอเค แต่จะเกิดอะไรขึ้นกับประเภทค่าเช่น structs? ในกรณีนี้การส่งพารามิเตอร์ด้วยคีย์เวิร์ด "ใน" ควรนำมาซึ่งประโยชน์ด้านประสิทธิภาพ เนื่องจากพารามิเตอร์จะถูกส่งผ่านโดยการอ้างอิง และโปรแกรมจะไม่จัดเตรียมสำเนาของเนื้อหาที่ให้ไว้กับวิธีการเช่นเดียวกับการส่งผ่านค่าต่างๆ ฉันได้สร้างโครงสร้างที่มีฟิลด์เดียวกันกับคลาสที่สร้างไว้ก่อนหน้านี้ และยังสร้างสองวิธีในการรับฟิลด์เฉพาะเหมือนกับในตัวอย่างคลาส

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;
}

มีการสร้างเกณฑ์มาตรฐานที่เหมาะสมด้วย

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);
    }
}

หลังจากแก้ไขแล้ว เบนช์มาร์กก็ถูกรันอีกครั้ง และสำหรับโครงสร้างก็ให้ผลลัพธ์ที่แตกต่างออกไป

|                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 |

ดังนั้นข้อสรุปก็คือกลไกเบื้องหลังคีย์เวิร์ดมีประโยชน์เมื่อมีความจำเป็นต้องส่งโครงสร้างเป็นพารามิเตอร์ และไม่มีเจตนาที่จะแก้ไขภายในวิธีการ ฉันได้ดูรหัส IL ในกรณีนี้ด้วย

.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

ในกรณีนี้จะเห็นความแตกต่างในรหัส IL ระหว่างสองวิธีอย่างเห็นได้ชัด วิธีการส่งพารามิเตอร์ตามค่าโหลดจะคัดลอกโครงสร้างทั้งหมดไปยังสแต็กโดยใช้คำสั่ง ldarga.s itemStruct ในขณะที่วิธีที่เราส่งพารามิเตอร์โดยการอ้างอิงอ่านที่อยู่เพื่อสร้างโครงสร้างและโหลดค่าลงบนสแต็กโดยใช้คำสั่ง ldarg.0 ทุกประการเช่นเดียวกับ ประเภทการอ้างอิง

ฉันได้ตรวจสอบแล้วว่าคีย์เวิร์ดในการเปลี่ยนแปลงพฤติกรรมของโปรแกรมเมื่อมีการใช้บันทึก แต่เนื่องจากเป็นประเภทอ้างอิง ผลลัพธ์ของโค้ดเบนช์มาร์กและเอาต์พุต IL จึงเกือบจะเหมือนกันในตัวอย่างคลาส

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

และมีผลลัพธ์สุดท้ายสำหรับทุกประเภท:

|                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 |

ข้อสรุป

การส่งพารามิเตอร์โดยการอ้างอิงโดยใช้คีย์เวิร์ด in อาจมีประโยชน์เมื่อเราต้องทำงานกับโครงสร้างที่หนักหน่วง การส่งผ่านโครงสร้างเหล่านี้โดยการอ้างอิงช่วยให้เราเพิ่มประสิทธิภาพได้ แต่เราไม่สามารถพูดแบบเดียวกันได้เมื่อต้องจัดการกับคลาสและบันทึก แน่นอนว่าอาจมีบางกรณีที่แม้แต่โครงสร้างเหล่านี้ที่ส่งผ่านโครงสร้างเหล่านี้โดยการอ้างอิงแบบอ่านอย่างเดียวก็อาจเป็นประโยชน์ แต่โดยทั่วไปแล้ว ความหมายส่วนใหญ่คือการใช้คีย์เวิร์ดนี้เฉพาะเมื่อเราต้องการส่งประเภทค่าไปยังเมธอด

ลิงก์ไปยังตัวอย่าง: https://github.com/proxna/in-keyword-benchmark

แหล่งที่มา: