.Net 7內容彙總(3)--反射優化

2022-09-26 18:02:31

反射這玩意,一直以來都是慢的代名詞。一說XXX系統大量的反射,好多人第一印象就是會慢。


但是呢,我們又不得不使用反射來做一些事情,畢竟這玩意可以說啥都能幹了對吧。

It’s immensely powerful, providing the ability to query all of the metadata for code in your process and for arbitrary assemblies you might encounter, to invoke arbitrary functionality dynamically, and even to emit dynamically-generated IL at run-time.

當然.Net也提供了一些效能更高的方法。

比如SG,這玩意是效能最好的方案,它在編譯的時候生成程式碼,執行的時候一點反射沒有,同時也完美支援Native AOT。但是呢,它還不是真正的動態生成,只能說是開發時動態。所以更適合一些框架程式使用來提高執行效率。

還有比如Emit,這玩意是動態編織IL程式碼的,效率也比反射要快。但是呢,寫起來極度複雜,10個人有8個都撓頭。


所以,.Net 7裡反射還是非常重要的一部分,也針對它做了一些比較牛逼的優化。

  1. 我們知道,給MethodBase使用CreateDelegate<T>來建立一個委託,然後呼叫這個委託是最佳方法。但是呢,我們編譯的時候經常是不知道這個方法簽名的,也就是沒法生成這個委託。部分庫已經使用Emit來生成程式碼提高速度了。但是我們普通使用者顯然區寫一堆Emit是不現實的。.Net 7優化後,會把我們的反射程式碼優化為DynamicMethod形式的委託,然後呼叫。

    我們來看一下資料

private MethodInfo _method;

[GlobalSetup]
public void Setup() => _method = typeof(Program).GetMethod("MyMethod", BindingFlags.NonPublic | BindingFlags.Static);

[Benchmark]
public void MethodInfoInvoke() => _method.Invoke(null, null);

private static void MyMethod() { }

Method

Runtime

Mean

Ratio

MethodInfoInvoke

.NET 6.0

43.846 ns

1.00

MethodInfoInvoke

.NET 7.0

8.078 ns

0.18

我們可以看到,這玩意速度提升了好幾倍。


反射還有一個用處就是對型別、方法、屬性等等這些東西進行獲取。一些其他的改進也會影響到這一部分。比如.Net最近一直在做的把原生型別轉換為託管型別的工作,就產生了這麼一個東西。

[Benchmark]
public Type GetUnderlyingType() => Enum.GetUnderlyingType(typeof(DayOfWeek));

Method

Runtime

Mean

Ratio

GetUnderlyingType

.NET 6.0

27.413 ns

1.00

GetUnderlyingType

.NET 7.0

5.115 ns

0.19

是的,原生型別轉換為託管型別,不但沒有拖慢反射,反而讓它快了好幾倍。


同樣的例子,有大量的AssemblyName的內容從原生轉向了CoreLib,所以Activator.CreateInstance也跟著變快了。

private readonly string _assemblyName = typeof(MyClass).Assembly.FullName;
private readonly string _typeName = typeof(MyClass).FullName;
public class MyClass { }

[Benchmark]
public object CreateInstance() => Activator.CreateInstance(_assemblyName, _typeName);

Method

Runtime

Mean

Ratio

CreateInstance

.NET 6.0

3.827 us

1.00

CreateInstance

.NET 7.0

2.276 us

0.60

這玩意雖然沒有那麼誇張,但是提升可以說也是不小了。


RuntimeType.CreateInstanceImpl現在使用Type.EmptyTypes代替了new Type[0],所以節省了一部分開銷。

[Benchmark]
public void CreateInstance() => Activator.CreateInstance(typeof(MyClass), BindingFlags.NonPublic | BindingFlags.Instance, null, Array.Empty<object>(), null);

internal class MyClass
{
    internal MyClass() { }
}

Method

Runtime

Mean

Ratio

Allocated

Alloc Ratio

CreateInstance

.NET 6.0

167.8 ns

1.00

320 B

1.00

CreateInstance

.NET 7.0

143.4 ns

0.85

200 B

0.62

我們再回到AssemblyName來,AssemblyName裡把AssemblyName.FullName的實現由StringBuilder改為了ArrayPool<char>,所以:

private AssemblyName[] _names = AppDomain.CurrentDomain.GetAssemblies().Select(a => new AssemblyName(a.FullName)).ToArray();

[Benchmark]
public int Names()
{
    int sum = 0;
    foreach (AssemblyName name in _names)
    {
        sum += name.FullName.Length;
    }
    return sum;
}

Method

Runtime

Mean

Ratio

Allocated

Alloc Ratio

Names

.NET 6.0

3.423 us

1.00

9.14 KB

1.00

Names

.NET 7.0

2.010 us

0.59

2.43 KB

0.27

另外由於JIT編譯器又進化了,現在可以在編譯過程中計算結果,所以:

[Benchmark]
public bool IsByRefLike() => typeof(ReadOnlySpan<char>).IsByRefLike;

Method

Runtime

Mean

Ratio

Code Size

IsByRefLike

.NET 6.0

2.1322 ns

1.00

31 B

IsByRefLike

.NET 7.0

0.0000 ns

0.00

6B

是的,你沒看錯,時間是0,因為這裡在執行的時候已經不需要計算了,直接就是個賦值操作,所以這個時間就。。。

我們來看一下生成的組合

; Program.IsByRefLike()
       mov       eax,1
       ret
; Total bytes of code 6

這就是反射優化的主要內容。反正就高喊666,知道反射又快了,用起來心裡負擔又小了就搞定了^ ^。