C#中的in參數(shù)與性能分析詳解
前言
in 修飾符也是從 C# 7.2 開始引入的,它與我們上一篇中討論的 《C# 中的只讀結(jié)構(gòu)體(readonly struct)》1 是緊密相關(guān)的。
in 修飾符
in 修飾符通過引用傳遞參數(shù)。 它讓形參成為實(shí)參的別名,即對形參執(zhí)行的任何操作都是對實(shí)參執(zhí)行的。 它類似于 ref 或 out 關(guān)鍵字,不同之處在于 in 參數(shù)無法通過調(diào)用的方法進(jìn)行修改。
- ref 修飾符,指定參數(shù)由引用傳遞,可以由調(diào)用方法讀取或?qū)懭搿?/li>
- out 修飾符,指定參數(shù)由引用傳遞,必須由調(diào)用方法寫入。
- in 修飾符,指定參數(shù)由引用傳遞,可以由調(diào)用方法讀取,但不可以寫入。
舉個簡單的例子:
struct Product { public int ProductId { get; set; } public string ProductName { get; set; } } public static void Modify(in Product product) { //product = new Product(); // 錯誤 CS8331 無法分配到 變量 'in Product',因?yàn)樗侵蛔x變量 //product.ProductName = "測試商品"; // 錯誤 CS8332 不能分配到 變量 'in Product' 的成員,因?yàn)樗侵蛔x變量 Console.WriteLine($"Id: {product.ProductId}, Name: {product.ProductName}"); // OK }
引入 in 參數(shù)的原因
我們知道,結(jié)構(gòu)體實(shí)例的內(nèi)存在棧(stack)上進(jìn)行分配,所占用的內(nèi)存隨聲明它的類型或方法一起回收,所以通常在內(nèi)存分配上它是比引用類型占有優(yōu)勢的。2
但是對于有些很大(比如有很多字段或?qū)傩裕┑慕Y(jié)構(gòu)體,將其作為方法參數(shù),在緊湊的循環(huán)或關(guān)鍵代碼路徑中調(diào)用方法時,復(fù)制這些結(jié)構(gòu)的成本就會很高。當(dāng)所調(diào)用的方法不修改該參數(shù)的狀態(tài),使用新的修飾符 in 聲明參數(shù)以指定此參數(shù)可以按引用安全傳遞,可以避免(可能產(chǎn)生的)高昂的復(fù)制成本,從而提高代碼運(yùn)行的性能。
in 參數(shù)對性能的提升
為了測試 in 修飾符對性能的提升,我定義了兩個較大的結(jié)構(gòu)體,一個是可變的結(jié)構(gòu)體 NormalStruct,一個是只讀的結(jié)構(gòu)體 ReadOnlyStruct,都定義了 30 個屬性,然后定義三個測試方法:
- DoNormalLoop 方法,參數(shù)不加修飾符,傳入一般結(jié)構(gòu)體,這是以前比較常見的做法。
- DoNormalLoopByIn 方法,參數(shù)加 in 修飾符,傳入一般結(jié)構(gòu)體。
- DoReadOnlyLoopByIn 方法,參數(shù)加 in 修飾符,傳入只讀結(jié)構(gòu)體。
代碼如下所示:
public struct NormalStruct { public decimal Number1 { get; set; } public decimal Number2 { get; set; } //... public decimal Number30 { get; set; } } public readonly struct ReadOnlyStruct { public readonly decimal Number1 { get; } public readonly decimal Number2 { get; } //... public readonly decimal Number30 { get; } } public class BenchmarkClass { const int loops = 50000000; NormalStruct normalInstance = new NormalStruct(); ReadOnlyStruct readOnlyInstance = new ReadOnlyStruct(); [Benchmark(Baseline = true)] public decimal DoNormalLoop() { decimal result = 0M; for (int i = 0; i < loops; i++) { result = Compute(normalInstance); } return result; } [Benchmark] public decimal DoNormalLoopByIn() { decimal result = 0M; for (int i = 0; i < loops; i++) { result = ComputeIn(in normalInstance); } return result; } [Benchmark] public decimal DoReadOnlyLoopByIn() { decimal result = 0M; for (int i = 0; i < loops; i++) { result = ComputeIn(in readOnlyInstance); } return result; } public decimal Compute(NormalStruct s) { //業(yè)務(wù)邏輯 return 0M; } public decimal ComputeIn(in NormalStruct s) { //業(yè)務(wù)邏輯 return 0M; } public decimal ComputeIn(in ReadOnlyStruct s) { //業(yè)務(wù)邏輯 return 0M; } }
在沒有使用 in 參數(shù)的方法中,意味著每次調(diào)用傳入的是變量的一個新副本; 而在使用 in 修飾符的方法中,每次不是傳遞變量的新副本,而是傳遞同一副本的只讀引用。
使用 BenchmarkDotNet 工具測試三個方法的運(yùn)行時間,結(jié)果如下:
| Method | Mean | Error | StdDev | Median | Ratio | RatioSD |
|------------------- |-----------:|---------:|----------:|-----------:|------:|--------:|
| DoNormalLoop | 1,536.3 ms | 65.07 ms | 191.86 ms | 1,425.7 ms | 1.00 | 0.00 |
| DoNormalLoopByIn | 480.9 ms | 27.05 ms | 79.32 ms | 446.3 ms | 0.32 | 0.07 |
| DoReadOnlyLoopByIn | 581.9 ms | 35.71 ms | 105.30 ms | 594.1 ms | 0.39 | 0.10 |
從這個結(jié)果可以看出,如果使用 in 參數(shù),不管是一般的結(jié)構(gòu)體還是只讀結(jié)構(gòu)體,相對于不用 in 修飾符的參數(shù),性能都有較大的提升。這個性能差異在不同的機(jī)器上運(yùn)行可能會有所不同,但是毫無疑問,使用 in 參數(shù)會得到更好的性能。
在 Parallel.For 中使用
在上面簡單的 for 循環(huán)中,我們看到 in 參數(shù)有助于性能的提升,那么在并行運(yùn)算中呢?我們把上面的 for 循環(huán)改成使用 Parallel.For 來實(shí)現(xiàn),代碼如下:
[Benchmark(Baseline = true)] public decimal DoNormalLoop() { decimal result = 0M; Parallel.For(0, loops, i => Compute(normalInstance)); return result; } [Benchmark] public decimal DoNormalLoopByIn() { decimal result = 0M; Parallel.For(0, loops, i => ComputeIn(in normalInstance)); return result; } [Benchmark] public decimal DoReadOnlyLoopByIn() { decimal result = 0M; Parallel.For(0, loops, i => ComputeIn(in readOnlyInstance)); return result; }
事實(shí)上,道理是一樣的,在使用 in 參數(shù)的方法中,每次調(diào)用傳入的是變量的一個新副本; 在使用 in 修飾符的方法中,每次傳遞的是同一副本的只讀引用。
使用 BenchmarkDotNet 工具測試三個方法的運(yùn)行時間,結(jié)果如下:
| Method | Mean | Error | StdDev | Ratio |
|------------------- |---------:|---------:|---------:|------:|
| DoNormalLoop | 793.4 ms | 13.02 ms | 11.54 ms | 1.00 |
| DoNormalLoopByIn | 352.4 ms | 6.99 ms | 17.27 ms | 0.42 |
| DoReadOnlyLoopByIn | 341.1 ms | 6.69 ms | 10.02 ms | 0.43 |
同樣表明,使用 in 參數(shù)會得到更好的性能。
使用 in 參數(shù)需要注意的地方
我們來看一個例子,定義一個一般的結(jié)構(gòu)體,包含一個屬性 Value 和 一個修改該屬性的方法 UpdateValue。 然后在別的地方也定義一個方法 UpdateMyNormalStruct 來修改該結(jié)構(gòu)體的屬性 Value。 代碼如下:
struct MyNormalStruct { public int Value { get; set; } public void UpdateValue(int value) { Value = value; } } class Program { static void UpdateMyNormalStruct(MyNormalStruct myStruct) { myStruct.UpdateValue(8); } static void Main(string[] args) { MyNormalStruct myStruct = new MyNormalStruct(); myStruct.UpdateValue(2); UpdateMyNormalStruct(myStruct); Console.WriteLine(myStruct.Value); } }
您可以猜想一下它的運(yùn)行結(jié)果是什么呢? 2 還是 8?
我們來理一下,在 Main 中先調(diào)用了結(jié)構(gòu)體自身的方法 UpdateValue 將 Value 修改為 2, 再調(diào)用 Program 中的方法 UpdateMyNormalStruct, 而該方法中又調(diào)用了 MyNormalStruct 結(jié)構(gòu)體自身的方法 UpdateValue,那么輸出是不是應(yīng)該是 8 呢? 如果您這么想就錯了。
它的正確輸出結(jié)果是 2,這是為什么呢?
這是因?yàn)?,結(jié)構(gòu)體和許多內(nèi)置的簡單類型(sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal、bool 和 enum 類型)一樣,都是值類型,在傳遞參數(shù)的時候以值的方式傳遞。因此調(diào)用方法 UpdateMyNormalStruct 時傳遞的是 myStruct 變量的新副本,在此方法中,其實(shí)是此副本調(diào)用了 UpdateValue 方法,所以原變量 myStruct 的 Value 不會發(fā)生變化。
說到這里,有聰明的朋友可能會想,我們給 UpdateMyNormalStruct 方法的參數(shù)加上 in 修飾符,是不是輸出結(jié)果就變?yōu)?8 了,in 參數(shù)不就是引用傳遞嗎?
我們可以試一下,把代碼改成:
static void UpdateMyNormalStruct(in MyNormalStruct myStruct) { myStruct.UpdateValue(8); } static void Main(string[] args) { MyNormalStruct myStruct = new MyNormalStruct(); myStruct.UpdateValue(2); UpdateMyNormalStruct(in myStruct); Console.WriteLine(myStruct.Value); }
運(yùn)行一下,您會發(fā)現(xiàn),結(jié)果依然為 2 !這……就讓人大跌眼鏡了……
用工具查看一下 UpdateMyNormalStruct 方法的中間語言:
.method private hidebysig static void UpdateMyNormalStruct ( [in] valuetype ConsoleApp4InTest.MyNormalStruct& myStruct ) cil managed { .param [1] .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = ( 01 00 00 00 ) // Method begins at RVA 0x2164 // Code size 18 (0x12) .maxstack 2 .locals init ( [0] valuetype ConsoleApp4InTest.MyNormalStruct ) IL_0000: nop IL_0001: ldarg.0 IL_0002: ldobj ConsoleApp4InTest.MyNormalStruct IL_0007: stloc.0 IL_0008: ldloca.s 0 IL_000a: ldc.i4.8 IL_000b: call instance void ConsoleApp4InTest.MyNormalStruct::UpdateValue(int32) IL_0010: nop IL_0011: ret } // end of method Program::UpdateMyNormalStruct
您會發(fā)現(xiàn),在 IL_0002、IL_0007 和 IL_0008 這幾行,仍然創(chuàng)建了一個 MyNormalStruct 結(jié)構(gòu)體的防御性副本(defensive copy)。雖然在調(diào)用方法 UpdateMyNormalStruct 時以引用的方式傳遞參數(shù),但在方法體中調(diào)用結(jié)構(gòu)體自身的 UpdateValue 前,卻創(chuàng)建了一個該結(jié)構(gòu)體的防御性副本,改變的是該副本的 Value。這就有點(diǎn)奇怪了,不是嗎?
Google 了一些資料是這么解釋的:C# 無法知道當(dāng)它調(diào)用一個結(jié)構(gòu)體上的方法(或getter)時,是否也會修改它的值/狀態(tài)。于是,它所做的就是創(chuàng)建所謂的“防御性副本”。當(dāng)在結(jié)構(gòu)體上運(yùn)行方法(或getter)時,它會創(chuàng)建傳入的結(jié)構(gòu)體的副本,并在副本上運(yùn)行方法。這意味著原始副本與傳入時完全相同,調(diào)用者傳入的值并沒有被修改。
有沒有辦法讓方法 UpdateMyNormalStruct 調(diào)用后輸出 8 呢?您將參數(shù)改成 ref 修飾符試試 :stuck_out_tongue_winking_eye: :grin: :joy:
綜上所述,最好不要把 in 修飾符和一般(非只讀)結(jié)構(gòu)體一起使用,以免產(chǎn)生晦澀難懂的行為,而且可能對性能產(chǎn)生負(fù)面影響。
in 參數(shù)的限制
不能將 in、ref 和 out 關(guān)鍵字用于以下幾種方法:
- 異步方法,通過使用 async 修飾符定義。
- 迭代器方法,包括 yield return 或 yield break 語句。
- 擴(kuò)展方法的第一個參數(shù)不能有 in 修飾符,除非該參數(shù)是結(jié)構(gòu)體。
- 擴(kuò)展方法的第一個參數(shù),其中該參數(shù)是泛型類型(即使該類型被約束為結(jié)構(gòu)體。)
總結(jié)
使用 in 參數(shù),有助于明確表明此參數(shù)不可修改的意圖。
當(dāng)只讀結(jié)構(gòu)體(readonly struct)的大小大于 IntPtr.Size 3 時,出于性能原因,應(yīng)將其作為 in 參數(shù)傳遞。
不要將一般(非只讀)結(jié)構(gòu)體作為 in 參數(shù),因?yàn)榻Y(jié)構(gòu)體是可變的,反而有可能對性能產(chǎn)生負(fù)面影響,并且可能產(chǎn)生晦澀難懂的行為。
到此這篇關(guān)于C#中的in參數(shù)與性能分析的文章就介紹到這了,更多相關(guān)C#中in參數(shù)與性能內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C# OpenVINO實(shí)現(xiàn)圖片旋轉(zhuǎn)角度檢測
這篇文章主要為大家詳細(xì)介紹了C#?OpenVINO如何實(shí)現(xiàn)圖片旋轉(zhuǎn)角度檢測,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-02-02UnityShader使用速度映射圖實(shí)現(xiàn)運(yùn)動模糊
這篇文章主要為大家詳細(xì)介紹了UnityShader使用速度映射圖實(shí)現(xiàn)運(yùn)動模糊,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-02-02C#實(shí)現(xiàn)對Json字符串處理實(shí)例
這篇文章主要介紹了C#實(shí)現(xiàn)對Json字符串處理,通過一個json實(shí)例分析了C#進(jìn)行JSON操作的方法,需要的朋友可以參考下2014-09-09C#使用ICSharpCode.SharpZipLib.dll進(jìn)行文件的壓縮與解壓功能
這篇文章主要介紹了C#使用ICSharpCode.SharpZipLib.dll進(jìn)行文件的壓縮與解壓功能,需要的朋友可以參考下2017-12-12自定義實(shí)現(xiàn)Json字符串向C#對象轉(zhuǎn)變的方法
自定義實(shí)現(xiàn)Json字符串向C#對象轉(zhuǎn)變的方法,需要的朋友可以參考一下2013-03-03Unity3D運(yùn)行報DllNotFoundException錯誤的解決方案
這篇文章主要介紹了Unity3D運(yùn)行報DllNotFoundException錯誤的解決方案,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-04-04C#?使用Fluent?API?創(chuàng)建自己的DSL(推薦)
DSL領(lǐng)域?qū)S谜Z言是描述特定領(lǐng)域問題的語言,聽起來很唬人,其實(shí)不是什么高深的東西,下面通過實(shí)例代碼介紹下C#?使用Fluent?API?創(chuàng)建自己的DSL,感興趣的朋友參考下吧2021-12-12