การแนะนำ
คีย์เวิร์ด 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
แหล่งที่มา: