自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

為什么 .NET的反射這么慢?

開發(fā) 后端
如果(也只是如果)你發(fā)現(xiàn)自己在使用反射的時(shí)候有性能問題,有一些辦法可以讓它變得更快。獲得這些速度提升是因?yàn)槲袔淼膶?duì)屬性/字段/方法進(jìn)行直接訪問,這避免了每次進(jìn)行反射的開銷。

大家都知道 .NET 的反射很慢,但是為什么會(huì)出現(xiàn)這種情況呢?這篇文章會(huì)帶你尋找這個(gè)問題的真正原因。

CLR 類型系統(tǒng)的設(shè)計(jì)目標(biāo)

原因之一是,在設(shè)計(jì)的時(shí)候反射本身就不是以高性能為目標(biāo)的,可以參考Type System Overview – ‘Design Goals and Non-goals’(類型系統(tǒng)概覽 – ‘設(shè)計(jì)目標(biāo)和非目標(biāo)’):

目標(biāo)

  • 運(yùn)行時(shí)通過快速執(zhí)行(非反射)代碼訪問需要的信息。
  • 編譯時(shí)直接訪問所需要的信息來生成代碼。
  • 垃圾回收/遍歷??梢栽L問需要信息而不需要鎖或分配內(nèi)存。
  • 一次只加載最少量的類型。
  • 類型加載時(shí)只加載最少需要加載的類型。
  • 類型系統(tǒng)的數(shù)據(jù)結(jié)構(gòu)必須在 NGEN 映像中保存。

非目標(biāo)

  • 元數(shù)據(jù)的所有信息能直接反射 CLR 數(shù)據(jù)結(jié)構(gòu)。
  • 快速使用反射。

參閱出處相同的 Type Loader Design – ‘Key Data Structures’(類型加載器設(shè)計(jì) – ‘關(guān)鍵數(shù)據(jù)結(jié)構(gòu)’):

EEClass

MethodTable(方法表)數(shù)據(jù)分為“熱”和“冷”兩種結(jié)構(gòu),以提高工作集和緩存的利用率。MethodTable 本身只存儲(chǔ)程序穩(wěn)定狀態(tài)的“熱”數(shù)據(jù)。EEClass 存儲(chǔ)“冷”數(shù)據(jù),它們通常是類型加載、JITing或反射所需要的。每個(gè) MethodTable 指向一個(gè) EEClass。

反射是如何工作的?

我們已經(jīng)知道反射本身就不是以快為目標(biāo)來設(shè)計(jì)的,但是它為什么需要那么多時(shí)間呢?

為了說明這個(gè)問題,來看看反射調(diào)用過程中,托管代碼和非托管代碼的調(diào)用棧。

  • System.Reflection.RuntimeMethodInfo.Invoke(..) - 源碼鏈接
    • 調(diào)用 System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(..)
  • System.RuntimeMethodHandle.PerformSecurityCheck(..) - 鏈接
    • 調(diào)用 System.GC.KeepAlive(..)
  • System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(..) - 鏈接
    • 調(diào)用 System.RuntimeMethodHandle.InvokeMethod(..) 的存根
  • System.RuntimeMethodHandle.InvokeMethod(..) 的存根 - 鏈接

即使不點(diǎn)擊鏈接,想必你也能直觀感受到改方法執(zhí)行的大量代碼。參考示例:System.RuntimeMethodHandle.InvokeMethodis 超過 400 行代碼!

那么,它具體在做什么?

獲取方法信息

要使用反射來調(diào)用字段/屬性/方法,你必須獲得 FieldInfo/PropertyInfo/MethodInfo,使用這樣的代碼:

Type t = typeof(Person);      
FieldInfo m = t.GetField("Name");

這需要一定的成本,因?yàn)樾枰崛∠嚓P(guān)的元數(shù)據(jù),并對(duì)其進(jìn)行解析。運(yùn)行時(shí)會(huì)幫我們維持一個(gè)內(nèi)部緩存,緩存著所有字段/屬性/方法。這個(gè)緩存由 RuntimeTypeCache 類實(shí)現(xiàn),用法示例在 RuntimeMethodInfo 類中.

運(yùn)行 gist 中的代碼你可以看到緩存的何運(yùn)作方式,它恰如其分地使用反射檢查運(yùn)行時(shí)內(nèi)部!

gist 上的代碼會(huì)在你使用反射獲得 FieldInfo 之前輸出下列內(nèi)容:

Type: ReflectionOverhead.Program
Reflection Type: System.RuntimeType (BaseType: System.Reflection.TypeInfo)
m_fieldInfoCache is null, cache has not been initialised yet

不過一旦你獲得字段,就會(huì)輸出:

Type: ReflectionOverhead.Program
Reflection Type: System.RuntimeType (BaseType: System.Reflection.TypeInfo)
RuntimeTypeCache: System.RuntimeType+RuntimeTypeCache, 
m_cacheComplete = True, 4 items in cache
  [0] - Int32 TestField1 - Private
  [1] - System.String TestField2 - Private
  [2] - Int32 <TestProperty1>k__BackingField - Private
  [3] - System.String TestField3 - Private, Static

ReflectionOverhead.Program 看起來像這樣:

class Program
{
    private int TestField1;
    private string TestField2;
    private static string TestField3;

    private int TestProperty1 { get; set; }
}

看來運(yùn)行時(shí)會(huì)篩選已經(jīng)創(chuàng)建過的東西,這意味著調(diào)用 GetFeild 或 GetFields 不需要多大代價(jià)。對(duì)于 GetMethod 和 GetProperty 來說也是如此,MethodInfo 或 PropertyInfo 會(huì)在你***次調(diào)用的時(shí)候創(chuàng)建并緩存起來。

參數(shù)校驗(yàn)和錯(cuò)誤處理

得到 MethodInfo 之后,如果調(diào)用它的 Invoke 方法,會(huì)要處理很多事項(xiàng)。假設(shè)編寫代碼如下:

PropertyInfo stringLengthField = 
    typeof(string).GetProperty("Length", 
        BindingFlags.Instance | BindingFlags.Public);
var length = stringLengthField.GetGetMethod().Invoke(new Uri(), new object[0]);

如果運(yùn)行上述代碼,會(huì)得到下面的異常:

System.Reflection.TargetException: Object does not match target type.
   at System.Reflection.RuntimeMethodInfo.CheckConsistency(..)
   at System.Reflection.RuntimeMethodInfo.InvokeArgumentsCheck(..)
   at System.Reflection.RuntimeMethodInfo.Invoke(..)
   at System.Reflection.RuntimePropertyInfo.GetValue(..)

這是因?yàn)槲覀儷@得了 String 類 Length 屬性的 PropertyInfo,但是卻在 Uri 對(duì)象上調(diào)用它,顯然,這是個(gè)錯(cuò)誤的類型!

此外,你還必須在調(diào)用方法時(shí)對(duì)傳遞給方法的參數(shù)進(jìn)行校驗(yàn)。為了能傳遞參數(shù),反射 API 使用了一個(gè) object 的數(shù)組作為參數(shù),其中每一個(gè)元素表示一個(gè)參數(shù)。所以,如果你使用反射來調(diào)用 Add(int x, int y) 方法,你得調(diào)用 methodInfo.Invoke(.., new [] { 5, 6 })。運(yùn)行時(shí)會(huì)對(duì)傳入?yún)?shù)的數(shù)量和類型進(jìn)行檢查,在這個(gè)示例中你要確保是 2 個(gè) int 類型的參數(shù)。這些工作不好的地方是常常需要裝箱,這會(huì)增加額外的成本。希望這在將來會(huì)降到***。

安全性檢查

另一個(gè)主要任務(wù)是多重安全性檢查。例如,你不允許使用反射來任意調(diào)用你想調(diào)用的方法。這里存在一些限制的或 ‘危險(xiǎn)方法’,只能由可信度高的 .NET 框架代碼調(diào)用。除了黑名單外,還有動(dòng)態(tài)安全檢查,它由調(diào)用時(shí)必須檢查的的當(dāng)前代碼訪問安全權(quán)限決定。

反射機(jī)制耗時(shí)多少?

了解反射的實(shí)際操作后,我們來看看實(shí)際耗時(shí)。請(qǐng)注意,這些基準(zhǔn)測試是通過反射直接比較讀/寫屬性來完成的。在 .NET 中屬性是一對(duì) Get/Set 方法,這是由編譯器生成的,但當(dāng)屬性只包含一個(gè)簡單的內(nèi)嵌字段時(shí),.NET JIT 會(huì)使用內(nèi)聯(lián) Get/Set 方法以提升性能。這意味著使用反射訪問屬性可能會(huì)遇到反射性能最差的情況,但它會(huì)被選擇是因?yàn)檫@是最常見的用例,數(shù)據(jù)位于 ORMs, Json 序列化/反序列化庫和對(duì)象映射工具中。

以下是由 BenchmarkDotNet 提供的原始結(jié)果,后面是在2個(gè)單獨(dú)的表中顯示的相同結(jié)果。 (全部Benchmark代碼由此下載 

[[180057]]

讀取屬性值(‘Get’)

寫屬性值(‘Set’)

我們可以清楚地看到,正常的反射代碼(GetViaReflection/SetViaReflection)比直接訪問屬性(GetViaProperty/SetViaProperty)要慢得多。 其他結(jié)果,我們還要進(jìn)一步分析。

設(shè)置

首先我們從 aTestClass 開始,代碼如下:

public class TestClass
{
    public TestClass(String data)     {
        Data = data;
    }

    private string data;
    private string Data
    {
        get { return data; }
        set { data = value; }
    }
}

 以及下面的通用代碼,這里包含了所有可用的選項(xiàng):

// Setup code, done only once 
TestClass testClass = new TestClass("A String");
Type @class = testClass.GetType();
BindingFlag bindingFlags = BindingFlags.Instance | 
                           BindingFlags.NonPublic | 
                           BindingFlags.Public;

正常的反射

首先我們使用常規(guī)基準(zhǔn)代碼來表示我們的起始情況和“最壞情況”:

[Benchmark]public string GetViaReflection() {
    PropertyInfo property = @class.GetProperty("Data", bindingFlags);
    return (string)property.GetValue(testClass, null);
}

選擇1 – 緩存 PropertyInfo

接下來,我們通過保存引用至 PropertyInfo 以獲得速度上的少量提升,而不是每次都去獲取。但即使這樣,與直接訪問屬性相比,也仍然慢得多,這就表明在反射的“調(diào)用”部分成本很高。

// Setup code, done only once
PropertyInfo cachedPropertyInfo = @class.GetProperty("Data", bindingFlags);

[Benchmark]
public string GetViaReflection() {    
    return (string)cachedPropertyInfo.GetValue(testClass, null);
}

選擇2 – 使用 FastMember

這里使用了 Marc Gravell 優(yōu)秀的 Fast Member 庫,這個(gè)庫用起來很簡單!

// Setup code, done only once
TypeAccessor accessor = TypeAccessor.Create(@class, allowNonPublicAccessors: true);

[Benchmark]
public string GetViaFastMember() {
    return (string)accessor[testClass, "Data"];
}

注意,與其他選擇稍有不同,它創(chuàng)建了一個(gè) TypeAccessor 來訪問類型中的所有屬性,而不僅是某一個(gè)。這帶來的負(fù)面影響是會(huì)導(dǎo)致運(yùn)行時(shí)間變長,因?yàn)樗趦?nèi)部首先要為你請(qǐng)求的屬性(這個(gè)例子中是‘Data’)創(chuàng)建委托,然后再獲取其值。不過這種開銷是很小的,F(xiàn)astMember 仍然比其它反射方法更快,也更易用。所以我建議你先去看看。

這個(gè)選擇及隨后的選擇將反射代碼轉(zhuǎn)換委托,這樣就可以直接調(diào)用而不再需要每次都進(jìn)行反射,速度因此得到提升!

必須指出創(chuàng)建一個(gè)委托需要一定的成本(可以從 ‘相關(guān)閱讀’ 了解更多)。總之,速度提升是因?yàn)槲覀冊(cè)谄渲羞M(jìn)行過一次大投入(安全檢查等)并保存了一個(gè)強(qiáng)類型的委托,之后我們只要稍微付出一點(diǎn)就可以一次次調(diào)用。如果反射只進(jìn)行一次,那你大可不必使用這些技術(shù)。但是如果你只進(jìn)行一次反射操作,它也不會(huì)出現(xiàn)性能瓶頸,你就完全不用在乎它會(huì)變慢!

通過委托讀某個(gè)屬性仍然不如直接訪問來得快,因?yàn)?.NET JIT 不會(huì)將對(duì)委托方法的調(diào)用進(jìn)行內(nèi)聯(lián)優(yōu)化,而直接訪問屬性則會(huì)。因此即使使用委托,我們也需要為調(diào)用方法付出成本,而直接訪問屬性就不會(huì)。

選項(xiàng)3——創(chuàng)建代理(Delegate)

在這個(gè)選項(xiàng)中,我們使用 CreateDelegate 函數(shù)來將 PropertyInfo 轉(zhuǎn)換為常規(guī)的 delegate:

// Setup code, done only once
PropertyInfo property = @class.GetProperty("Data", bindingFlags);
Func<TestClass, string> getDelegate = 
    (Func<TestClass, string>)Delegate.CreateDelegate(
             typeof(Func<TestClass, string>), 
             property.GetGetMethod(nonPublic: true));

[Benchmark]
public string GetViaDelegate() {
    return getDelegate(testClass);
}

它的缺點(diǎn)是你必須知道編譯時(shí)的具體類型,也就是上面的代碼中的 Func<TestClass,string> 部分(如果使用 Func<object,string>,編譯器會(huì)拋出一個(gè)異常!)。不過,在大多數(shù)情況下,使用反射不會(huì)遇到這么多麻煩。

有效避免麻煩,請(qǐng)參閱 MagicMethodHelper 代碼(在 Jon Skeet 發(fā)布的“Making Reflection fly and exploring delegates“博客中),或閱讀下面的選項(xiàng) 4 或 5。

選項(xiàng)4——編譯表達(dá)式樹(Compiled Expression Trees)

這里我們生成一個(gè) delegate,但不同的是我們可以傳入一個(gè) object,所以我們會(huì)看到“選項(xiàng)4”的限制。我們使用支持動(dòng)態(tài)代碼生成的 .NET Expression tree API

// Setup code, done only once
PropertyInfo property = @class.GetProperty("Data", bindingFlags);
ParameterExpression = Expression.Parameter(typeof(object), "instance");
UnaryExpression instanceCast = 
    !property.DeclaringType.IsValueType ? 
        Expression.TypeAs(instance, property.DeclaringType) : 
        Expression.Convert(instance, property.DeclaringType);
Func<object, object> GetDelegate = 
    Expression.Lambda<Func<object, object>>(
        Expression.TypeAs(
            Expression.Call(instanceCast, property.GetGetMethod(nonPublic: true)),
            typeof(object)), 
        instance)
    .Compile();

[Benchmark]
public string GetViaCompiledExpressionTrees() {
    return (string)GetDelegate(testClass);
}

關(guān)于 Expression 的全部代碼可以從“Faster Reflection using Expression Trees(使用表達(dá)式樹的快速反射機(jī)制)“博客下載。

選項(xiàng) 5——IL Emit 動(dòng)態(tài)代碼生成

***,雖然“權(quán)力越大,責(zé)任越大”,但這里我們還是使用***層的方法調(diào)用原始 IL,:

// Setup code, done only once
PropertyInfo property = @class.GetProperty("Data", bindingFlags);
Sigil.Emit getterEmiter = Emit<Func<object, string>>
    .NewDynamicMethod("GetTestClassDataProperty")
    .LoadArgument(0)
    .CastClass(@class)
    .Call(property.GetGetMethod(nonPublic: true))
    .Return();
Func<object, string> getter = getterEmiter.CreateDelegate();

[Benchmark]
public string GetViaILEmit() {
    return getter(testClass);
}

使用 Expression tress(如選項(xiàng) 4 中所說),并沒有給出像直接調(diào)用 IL 代碼那么多的靈活性,盡管它確實(shí)能防止你調(diào)用無效代碼! 考慮到這一點(diǎn),如果你發(fā)現(xiàn)自己確實(shí)需要 emit IL,我強(qiáng)烈推薦你使用性能卓越的 Sigil 庫,因?yàn)樗茉诔鲥e(cuò)時(shí)提供更好的錯(cuò)誤提示消息!

小結(jié)

如果(也只是如果)你發(fā)現(xiàn)自己在使用反射的時(shí)候有性能問題,有一些辦法可以讓它變得更快。獲得這些速度提升是因?yàn)槲袔淼膶?duì)屬性/字段/方法進(jìn)行直接訪問,這避免了每次進(jìn)行反射的開銷。

請(qǐng)?jiān)?nbsp;/r/programming 和 /r/csharp 參考討論這篇文章

責(zé)任編輯:張燕妮 來源: Tocy, 邊城, Viyi
相關(guān)推薦

2018-08-16 08:03:21

Python語言解釋器

2020-08-14 09:11:29

RedisQPS數(shù)據(jù)庫

2024-04-15 04:00:00

C#反射代碼

2022-06-30 08:01:53

mysqlmyisamcount

2021-05-29 06:23:47

webpack esbuild

2015-09-09 11:04:28

Wi-Fi網(wǎng)速

2018-10-28 15:40:23

Python編程語言

2024-06-04 00:00:30

C#反射編程

2025-03-04 07:30:00

開發(fā)前端Node.js

2020-02-27 15:44:41

Nginx服務(wù)器反向代理

2013-03-04 10:10:36

WebKit瀏覽器

2019-08-30 14:58:47

JavaScript程序員編程語言

2024-02-26 21:15:20

Kafka緩存參數(shù)

2022-06-02 08:03:19

PyCharmPython代碼

2020-02-27 21:03:30

調(diào)度器架構(gòu)效率

2022-06-13 21:52:02

CDN網(wǎng)絡(luò)節(jié)點(diǎn)

2022-12-22 07:44:04

2012-08-17 10:01:07

云計(jì)算

2020-03-30 15:05:46

Kafka消息數(shù)據(jù)

2020-06-16 14:13:50

Kubernetes容器Linux
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)