在之前的文章中我們介紹了 c# 中的 唯讀結構體(readonly struct)
[1] 和與其緊密相關的in
引數
[2]。
今天我們來討論一下從 c# 8 開始引入的乙個特性:可變結構體中的唯讀例項成員(當結構體可變時,將不會改變結構體狀態的例項成員宣告為readonly
)。
簡單來說,還是為了提公升效能。
我們已經知道了唯讀結構體(readonly struct
)和in
引數可以通過減少建立副本,來提高**執行的效能。當我們建立唯讀結構體型別時,編譯器強制所有成員都是唯讀的(即沒有例項成員修改其狀態)。但是,在某些場景,比如您有乙個現有的 api,具有公開可訪問字段或者兼有可變成員和不可變成員。在這種情形下,不能將型別標記為readonly
(因為這關係到所有例項成員)。
通常,這沒有太大的影響,但是在使用in
引數的情況下就例外了。對於非唯讀結構體的in
引數,編譯器將為每個例項成員的呼叫建立引數的防禦性副本,因為它無法保證此呼叫不會修改其內部狀態。這可能會導致建立大量副本,並且比直接按值傳遞結構體時的總體效能更差(因為按值傳遞只會在傳參時建立一次副本)。
看乙個例子您就明白了,我們定義這樣乙個一般結構體,然後將其作為in
引數傳遞:
public struct rect
}}public class sampleclass
}
編譯後,類sampleclass
中的方法m
**執行邏輯實際上是這樣的:
public float m([in] [isreadonly] ref rect value)
我們把上面的可變結構體rect
修改一下,新增乙個readonly
方法getareareadonly
,如下:
public struct rect
}public readonly float getareareadonly()
}
此時,**是可以通過編譯的,但是會提示一條這樣的的警告:從 "readonly" 成員呼叫非 readonly 成員 "rect.area.get" 將產生 "this" 的隱式副本。
翻譯成大白話就是說,我們在唯讀方法getareareadonly
中呼叫了非唯讀area
屬性將會產生 "this" 的防禦性副本。用**演示一下編譯後方法getareareadonly
的方法體執行邏輯實際上是這樣的:
[isreadonly]
public float getareareadonly()
所以為了避免建立多餘的防禦性副本而影響效能,我們應該給唯讀方法體中呼叫的屬性或方法都加上readonly
修飾符(在本例中,即給屬性area
加上readonly
修飾符)。
我們將上面的示例再修改一下:
public struct rect
}public readonly float getareareadonly()
public float getarea()
}public class sampleclass
public float callgetareain(in rect vector)
public float callgetareareadonly(in rect vector)
}
類sampleclass
中定義三個方法:
我們來重點看一下第二個和第三個方法有什麼區別,還是把它們的 il **邏輯翻譯成易懂的執行邏輯,如下所示:
public float callgetareain([in] [isreadonly] ref rect vector)
public float callgetareareadonly([in] [isreadonly] ref rect vector)
可以看出,callgetareareadonly
在呼叫結構體的(唯讀)成員方法時,相對於callgetareain
(呼叫結構體的非唯讀成員方法)少建立了一次本地的防禦性副本,所以在執行效能上應該是有優勢的。
效能的提公升在結構體較大的時候比較明顯,所以在測試的時候為了能夠突出三個方法效能的差異,我在rect
結構體中新增了 30 個 decimal 型別的屬性,然後在類sampleclass
中新增了三個測試方法,**如下所示:
public struct rect
}public readonly float getareareadonly()
public float getarea()
public decimal number1
public decimal number2
//...
public decimal number30
}public class sampleclass
[benchmark(baseline = true)]
public float donormalloop()
return result;
}[benchmark]
public float donormalloopbyin()
return result;
}[benchmark]
public float doreadonlyloopbyin()
return result;
}public float callgetarea(rect vector)
public float callgetareain(in rect vector)
public float callgetareareadonly(in rect vector)
}
在沒有使用in
引數的方法中,意味著每次呼叫傳入的是變數的乙個新副本; 而在使用in
修飾符的方法中,每次不是傳遞變數的新副本,而是傳遞同一副本的唯讀引用。
使用 benchmarkdotnet 工具測試三個方法的執行時間,結果如下:
method
mean
error
stddev
ratio
ratiosd
donormalloop
2.034 s
0.0392 s
0.0348 s
1.00
0.00
donormalloopbyin
3.490 s
0.0667 s
0.0557 s
1.71
0.03
doreadonlyloopbyin
1.041 s
0.0189 s
0.0202 s
0.51
0.01
從結果可以看出,當結構體可變時,使用in
引數呼叫結構體的唯讀方法,效能高於其他兩種; 使用in
引數呼叫可變結構體的非唯讀方法,執行時間最長,嚴重影響了效能,應該避免這樣呼叫。
作者 : 技術譯民c# 中的唯讀結構體 ↩︎出品 : 技術譯站
c# 中的 in 引數和效能分析 ↩︎
C 可變結構體中的唯讀例項成員
簡單來說,還是為了提公升效能。我們已經知道了唯讀結構體 readonly struct 和 in 引數可以通過減少建立副本,來提高 執行的效能。當我們建立唯讀結構體型別時,編譯器強制所有成員都是唯讀的 即沒有例項成員修改其狀態 但是,在某些場景,比如您有乙個現有的 api,具有公開可訪問字段或者兼有...
C 8中的範圍型別 Range Type
c 8.0中加入了乙個新的範圍型別 range type 這裡我們首先展示一些 並一步一步為 新增一些不同的東西,為大家展示一下範圍型別的功能和用法。我們最原始的 如下 copy static void main string args for int i 1 i 3 i console.readl...
結構體中的成員對齊
關於結構體中成員對齊的總結 a.結構體中,結構體成員要對齊到其對齊值倍數的位址上,對齊值為min 成員型別對齊值,編譯器結構成員對齊值b.結構體本身的對齊值為其所有成員中最大的對齊值。c.結構體本身要對齊到其對齊值倍數的位址上。d.結構體中的結構體要對齊到其對齊值倍數的位址上。e.結構體成員在記憶體...