C# 中的 in 参数和性能分析
in?修飾符也是從 C# 7.2 開始引入的,它與我們上一篇中討論的 《C# 中的只讀結構體(readonly struct)》[1]?是緊密相關的。
in 修飾符
in?修飾符通過引用傳遞參數。它讓形參成為實參的別名,即對形參執行的任何操作都是對實參執行的。它類似于?ref?或?out?關鍵字,不同之處在于?in?參數無法通過調用的方法進行修改。
ref?修飾符,指定參數由引用傳遞,可以由調用方法讀取或寫入。
out?修飾符,指定參數由引用傳遞,必須由調用方法寫入。
in?修飾符,指定參數由引用傳遞,可以由調用方法讀取,但不可以寫入。
舉個簡單的例子:
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',因為它是只讀變量//product.ProductName = "測試商品"; // 錯誤 CS8332 不能分配到 變量 'in Product' 的成員,因為它是只讀變量Console.WriteLine($"Id: {product.ProductId}, Name: {product.ProductName}"); // OK }引入 in 參數的原因
我們知道,結構體實例的內存在棧(stack)上進行分配,所占用的內存隨聲明它的類型或方法一起回收,所以通常在內存分配上它是比引用類型占有優勢的。[2]
但是對于有些很大(比如有很多字段或屬性)的結構體,將其作為方法參數,在緊湊的循環或關鍵代碼路徑中調用方法時,復制這些結構的成本就會很高。當所調用的方法不修改該參數的狀態,使用新的修飾符?in?聲明參數以指定此參數可以按引用安全傳遞,可以避免(可能產生的)高昂的復制成本,從而提高代碼運行的性能。
in 參數對性能的提升
為了測試?in?修飾符對性能的提升,我定義了兩個較大的結構體,一個是可變的結構體?NormalStruct,一個是只讀的結構體?ReadOnlyStruct,都定義了 30 個屬性,然后定義三個測試方法:
DoNormalLoop?方法,參數不加修飾符,傳入一般結構體,這是以前比較常見的做法。
DoNormalLoopByIn?方法,參數加?in?修飾符,傳入一般結構體。
DoReadOnlyLoopByIn?方法,參數加?in?修飾符,傳入只讀結構體。
代碼如下所示:
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){//業務邏輯return 0M;}public decimal ComputeIn(in NormalStruct s){//業務邏輯return 0M;}public decimal ComputeIn(in ReadOnlyStruct s){//業務邏輯return 0M;} }在沒有使用?in?參數的方法中,意味著每次調用傳入的是變量的一個新副本; 而在使用?in?修飾符的方法中,每次不是傳遞變量的新副本,而是傳遞同一副本的只讀引用。
使用 BenchmarkDotNet 工具測試三個方法的運行時間,結果如下:
| 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 |從這個結果可以看出,如果使用?in?參數,不管是一般的結構體還是只讀結構體,相對于不用?in?修飾符的參數,性能都有較大的提升。這個性能差異在不同的機器上運行可能會有所不同,但是毫無疑問,使用?in?參數會得到更好的性能。
在 Parallel.For 中使用
在上面簡單的?for?循環中,我們看到?in?參數有助于性能的提升,那么在并行運算中呢?我們把上面的?for?循環改成使用?Parallel.For?來實現,代碼如下:
[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; }事實上,道理是一樣的,在沒有使用?in?參數的方法中,每次調用傳入的是變量的一個新副本; 在使用?in?修飾符的方法中,每次傳遞的是同一副本的只讀引用。
使用 BenchmarkDotNet 工具測試三個方法的運行時間,結果如下:
| 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?參數會得到更好的性能。
使用 in 參數需要注意的地方
我們來看一個例子,定義一個一般的結構體,包含一個屬性?Value?和 一個修改該屬性的方法?UpdateValue。然后在別的地方也定義一個方法?UpdateMyNormalStruct?來修改該結構體的屬性?Value。
代碼如下:
您可以猜想一下它的運行結果是什么呢?2 還是 8?
我們來理一下,在?Main?中先調用了結構體自身的方法?UpdateValue?將?Value?修改為 2, 再調用?Program?中的方法?UpdateMyNormalStruct, 而該方法中又調用了?MyNormalStruct?結構體自身的方法?UpdateValue,那么輸出是不是應該是 8 呢?如果您這么想就錯了。
它的正確輸出結果是?2,這是為什么呢?
這是因為,結構體和許多內置的簡單類型(sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal、bool 和 enum 類型)一樣,都是值類型,在傳遞參數的時候以值的方式傳遞。因此調用方法?UpdateMyNormalStruct?時傳遞的是?myStruct?變量的新副本,在此方法中,其實是此副本調用了?UpdateValue?方法,所以原變量?myStruct?的?Value?不會發生變化。
說到這里,有聰明的朋友可能會想,我們給?UpdateMyNormalStruct?方法的參數加上?in?修飾符,是不是輸出結果就變為 8 了,in?參數不就是引用傳遞嗎?
我們可以試一下,把代碼改成:
運行一下,您會發現,結果依然為?2?!這……就讓人大跌眼鏡了……
用工具查看一下?UpdateMyNormalStruct?方法的中間語言:
您會發現,在?IL_0002、IL_0007?和?IL_0008?這幾行,仍然創建了一個?MyNormalStruct?結構體的防御性副本(defensive copy)。雖然在調用方法?UpdateMyNormalStruct?時以引用的方式傳遞參數,但在方法體中調用結構體自身的?UpdateValue?前,卻創建了一個該結構體的防御性副本,改變的是該副本的?Value。這就有點奇怪了,不是嗎?
Google 了一些資料是這么解釋的:C# 無法知道當它調用一個結構體上的方法(或getter)時,是否也會修改它的值/狀態。于是,它所做的就是創建所謂的“防御性副本”。當在結構體上運行方法(或getter)時,它會創建傳入的結構體的副本,并在副本上運行方法。這意味著原始副本與傳入時完全相同,調用者傳入的值并沒有被修改。
有沒有辦法讓方法?UpdateMyNormalStruct?調用后輸出 8 呢?您將參數改成?ref?修飾符試試看 ???? ???? ????
綜上所述,最好不要把?in?修飾符和一般(非只讀)結構體一起使用,以免產生晦澀難懂的行為,而且可能對性能產生負面影響。
in 參數的限制
不能將?in、ref?和?out?關鍵字用于以下幾種方法:
異步方法,通過使用?async?修飾符定義。
迭代器方法,包括?yield return?或?yield break?語句。
擴展方法的第一個參數不能有?in?修飾符,除非該參數是結構體。
擴展方法的第一個參數,其中該參數是泛型類型(即使該類型被約束為結構體。)
總結
使用?in?參數,有助于明確表明此參數不可修改的意圖。
當只讀結構體(readonly struct)的大小大于?IntPtr.Size?[3]?時,出于性能原因,應將其作為?in?參數傳遞。
不要將一般(非只讀)結構體作為?in?參數,因為結構體是可變的,反而有可能對性能產生負面影響,并且可能產生晦澀難懂的行為。
相關鏈接:
https://mp.weixin.qq.com/s/wwVZbdY7m7da1nmIKb2jCA?C# 中的只讀結構體???
https://mp.weixin.qq.com/s/wVikRMfc4BbrB6WbDy1gXw?C# 中 Struct 和 Class 的區別總結???
https://docs.microsoft.com/zh-cn/dotnet/api/system.intptr.size#System_IntPtr_Size?IntPtr.Size???
作者 :技術譯民??
出品 :技術譯站(https://ITTranslator.cn/)
總結
以上是生活随笔為你收集整理的C# 中的 in 参数和性能分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET架构小技巧(6)——什么是好的架
- 下一篇: C#中形态各异的class