思想 DAO(Data Access Object)數據訪問對象,是我們在做結構化資料庫訪問的時候傳輸的對象,通過這個對象我們可以與資料庫中的表建立映射關係 DTO(Data Transfer Object)是我們在與前端進行數據交換時傳遞的對象 為什麼需要設置這這兩種對象呢? 為了數據安全 如果我 ...
原生 AOT
原生 AOT 在 .NET 7 中發佈。它使 .NET 程式在構建時被編譯成一個完全由原生代碼組成的自包含可執行文件或庫:在執行時不需要 JIT 來編譯任何東西,實際上,編譯的程式中沒有包含 JIT。結果是一個可以有非常小的磁碟占用,小的記憶體占用,和非常快的啟動時間的應用程式。在 .NET 7 中,主要支持的工作負載是控制台應用程式。現在在 .NET 8 中,已經投入了大量的工作來使 ASP.NET 應用程式在使用原生 AOT 編譯時表現出色,同時也降低了總體成本,無論應用模型如何。
在 .NET 8 中,一個重要的焦點是減小構建應用程式的大小,這個效果非常容易看出來。讓我們開始創建一個新的原生 AOT 控制台應用程式:
dotnet new console -o nativeaotexample -f net7.0
這將創建一個新的 nativeaotexample 目錄,並向其中添加一個針對 .NET 7 的新的 "Hello, world" 應用程式。以兩種方式編輯生成的 nativeaotexample.csproj:
- 將
<TargetFramework>net7.0</TargetFramework>
更改為<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
,以便我們可以輕鬆地為 .NET 7 或 .NET 8 構建。 - 在
<PropertyGroup>...</PropertyGroup>
中添加<PublishAot>true</PublishAot>
,以便當我們 dotnet publish 時,它使用 Native AOT。
現在,為 .NET 7 發佈應用程式。我目前正在針對 x64 的 Linux,所以我使用 linux-x64,但你可以在 Windows 上使用 Windows 標識符,如 win-x64,跟隨操作:
dotnet publish -f net7.0 -r linux-x64 -c Release
這應該成功構建應用程式,生成一個獨立的可執行文件,我們可以 ls/dir 輸出目錄以查看生成的二進位大小(這裡我使用了 ls -s --block-size=k):
12820K /home/stoub/nativeaotexample/bin/Release/net7.0/linux-x64/publish/nativeaotexample
所以,在 Linux 上的 .NET 7,這個 "Hello, world" 應用程式,包括所有必要的庫支持,GC,所有的東西,是 ~13Mb。現在,我們可以為 .NET 8 做同樣的事情:
dotnet publish -f net8.0 -r linux-x64 -c Release
再次查看生成的輸出大小:
1536K /home/stoub/nativeaotexample/bin/Release/net8.0/linux-x64/publish/nativeaotexample
現在在 .NET 8,那個 ~13MB 已經降到 ~1.5M!我們還可以使用各種支持的配置標誌使其更小。首先,我們可以設置在 dotnet/runtime#85133 中引入的大小與速度選項,向 .csproj 添加 <OptimizationPreference>Size</OptimizationPreference>
。然後,如果我不需要全球化特定的代碼和數據,並且可以使用不變模式,我可以添加 <InvariantGlobalization>true</InvariantGlobalization>
。也許我不在乎如果發生異常是否有好的堆棧跟蹤?dotnet/runtime#88235 添加了 <StackTraceSupport>false</StackTraceSupport>
選項。添加所有這些並重新發佈:
1248K /home/stoub/nativeaotexample/bin/Release/net8.0/linux-x64/publish/nativeaotexample
很好。
這些改進的大部分來自於一種無情的努力,涉及到在這裡削減10Kb,那裡削減20Kb。以下是一些降低這些大小的例子:
-
Native AOT 編譯器需要創建各種數據結構,然後在應用程式執行時由運行時使用。dotnet/runtime#77884 添加了對這些數據結構的支持,包括包含指針的數據結構,可以存儲到應用程式中,然後在執行時重新激活。即使在後續的 PR 以各種方式擴展之前,這就已經從應用程式大小中削減了幾百千位元組,無論是在 Windows 還是 Linux(但在 Linux 上更多)。
-
每個具有包含引用的靜態欄位的類型都有一個與之關聯的包含幾個指針的數據結構。dotnet/runtime#78794 使這些指針相對化,節省了 HelloWorld 應用程式大小的約0.5%(至少在 Linux 上,Windows 上稍微少一些)。dotnet/runtime#78801 對另一組指針做了同樣的處理,又節省了約1%。
-
dotnet/runtime#79594 移除了一些過度積極的跟蹤類型和方法,這些類型和方法需要存儲關於它們的反射數據。這又在 HelloWorld 上節省了約32Kb。
-
在某些情況下,即使它們從未被使用並因此為空,也會創建泛型類型字典。dotnet/runtime#82591 擺脫了這些,又在一個簡單的 ASP.NET 最小 API 應用程式上節省了約1.5%。dotnet/runtime#83367 通過擺脫其他空的類型字典,又節省了約20Kb。
-
在泛型類型上聲明的成員有其代碼複製並專門用於替代泛型類型參數的每個值類型。然而,如果通過一些調整,這些成員可以被非泛型化並移出類型,例如移入一個非泛型基類型,那麼就可以避免這種複製。dotnet/runtime#82923 對數組枚舉器做了這樣的處理,移動了 IDisposable 和非泛型 IEnumerator 介面實現。
-
CoreLib 有一個空數組枚舉器的實現,當枚舉一個空的 T[] 時可以使用,這個單例可能在非數組的可枚舉對象中使用,例如,枚舉一個空的 (IEnumerable<KeyValuePair<TKey, TValue>>)Dictionary<TKey, TValue> 可能會產生那個數組枚舉器單例。然而,那個枚舉器有一個引用到 T[],在 Native AOT 世界中,使用枚舉器意味著需要為 T[] 的各個成員產生代碼。然而,如果問題中的枚舉器是一個不太可能在其他地方使用的 T[](例如,KeyValuePair<TKey, TValue>[]),dotnet/runtime#82899 提供了一個專門的枚舉器單例,它不引用 T[],避免強制創建和保留那個代碼(例如,Dictionary<TKey, TValue> 的 IEnumerable<KeyValuePair<TKey, TValue>> 的代碼)。
-
沒有人會在 C# 編譯器為非同步方法生成的 AsyncStateMachine 結構上調用 Equals/GetHashCode 方法;它們是一個隱藏的實現細節,但即便如此,這些虛方法通常在 Native AOT 應用程式中保持根源(而 CoreCLR 可以使用反射為值類型提供這些方法的實現,Native AOT 需要為每個值類型發出定製的代碼)。dotnet/runtime#83369 對這些進行了特殊處理,以避免它們被保留,從而在最小 API 應用程式上又節省了約1%。
-
dotnet/runtime#83937 減小了靜態構造函數上下文的大小,這些數據結構用於在系統的各個部分之間傳遞關於類型的靜態 cctor 的信息。
-
dotnet/runtime#84463 做了一些調整,最終避免了為 double/float 創建 MethodTables,並減少了對一些數組方法的依賴,從 HelloWorld 上又節省了約3%。
-
dotnet/runtime#84156 手動將一個方法分成兩部分,使得一些較少使用的代碼不總是在使用更常用的代碼時引入;這又節省了幾百千位元組。
-
dotnet/runtime#84224 改進了處理常見模式 typeof(T) == typeof(Something) 的方式,這種模式經常用於進行泛型專門化(例如,在像 MemoryExtensions 這樣的代碼中),並以一種更容易去除被剪掉的分支的副作用的方式進行。
-
GC 包括一個名為 vxsort 的向量化排序實現。在使用優化大小的配置構建時,dotnet/runtime#85036 允許移除那個吞吐量優化,節省了幾百千位元組。
-
ValueTuple<...> 是一個非常方便的類型,但它帶來了大量的代碼,因為它實現了多個介面,這些介面然後在泛型類型參數上根源功能。dotnet/runtime#87120 從 SynchronizationContext 中移除了對 ValueTuple<T1, T2> 的使用,節省了約200Kb。
-
特別是在 Linux 上,一個大的改進來自 dotnet/runtime#85139。調試符號以前被存儲在發佈的可執行文件中;有了這個改變,符號從可執行文件中剝離出來,而是存儲在旁邊構建的一個單獨的 .dbg 文件中。想要恢復到在可執行文件中保留符號的人可以在他們的項目中添加
<StripSymbols>false</StripSymbols>
。
你已經明白了。然而,改進不僅僅在於 Native AOT 編譯器內部的修修補補。單個庫也做出了貢獻。例如:
- HttpClient 支持自動解壓響應流,包括 deflate 和 brotli,這反過來意味著任何 HttpClient 的使用都隱式地帶有大部分的 System.IO.Compression。然而,預設情況下,這種解壓縮是不啟用的,你需要通過在使用的 HttpClientHandler 或 SocketsHttpHandler 上顯式設置 AutomaticDecompression 屬性來選擇啟用它。所以,dotnet/runtime#78198 使用了一個技巧,其中 SocketsHttpHandler 的主要代碼路徑不是直接依賴於執行這項工作的內部 DecompressionHandler,而是依賴於一個委托。存儲該委托的欄位開始時為 null,然後作為 AutomaticDecompression setter 的一部分,該欄位被設置為一個將執行解壓縮工作的委托。這意味著,如果修剪器沒有看到任何訪問 AutomaticDecompression setter 的代碼,以便可以修剪掉 setter,那麼所有的 DecompressionHandler 及其對 DeflateStream 和 BrotliStream 的依賴也可以被修剪掉。因為它有點難以理解,所以這裡有一個表示它的圖示:
private DecompressionMethods _automaticDecompression;
private Func<Stream, Stream>? _getStream;
public DecompressionMethods AutomaticDecompression
{
get => _automaticDecompression;
set
{
_automaticDecompression = value;
_getStream ??= CreateDecompressionStream;
}
}
public Stream GetStreamAsync()
{
Stream response = ...;
return _getStream is not null ? _getStream(response) : response;
}
private static Stream CreateDecompressionStream(Stream stream) =>
UseGZip ? new GZipStream(stream, CompressionMode.Decompress) :
UseZLib ? new ZLibStream(stream, CompressionMode.Decompress) :
UseBrotli ? new BrotliStream(stream, CompressionMode.Decompress) :
stream;
}
這裡的 CreateDecompressionStream 方法是引用所有壓縮相關代碼的地方,唯一接觸它的代碼路徑是在 AutomaticDecompression setter 中。因此,如果應用程式中沒有任何東西訪問 setter,那麼 setter 可以被修剪,這意味著 CreateDecompressionStream 方法也可以被修剪,這意味著如果應用程式中的其他任何東西都沒有使用這些壓縮流,它們也可以被修剪。
-
runtime#80884 是另一個例子,當使用 Regex 時,只需在其實現中更有目的性地使用什麼類型(例如,使用 bool[30] 而不是 HashSet
來存儲點陣圖),就可以節省約90Kb的大小。 -
或者特別有趣的,dotnet/runtime#84169,它為 System.Xml 添加了一個新的特性開關。System.Xml 中的各種 API 使用 Uri,這可能會觸發 XmlUrlResolver 的使用,這反過來又引用了網路堆棧;一個使用 XML 但不使用網路的應用程式可能會無意中引入超過3MB的網路代碼,只是通過使用像 XDocument.Load("filepath.xml") 這樣的 API。這樣的應用程式可以使用 dotnet/sdk#34412 中添加的
<XmlResolverIsNetworkingEnabledByDefault>
MSBuild 屬性來啟用所有這些在 XML 中的代碼路徑被修剪掉。 -
Microsoft.Extensions.DependencyInjection.Abstractions 中的 ActivatorUtilities.CreateFactory 試圖通過提前花費一些時間來構建一個然後非常有效地創建事物的工廠來優化吞吐量。它的主要策略是使用 System.Linq.Expressions 作為使用反射發射的更簡單的 API,為正在構造的確切事物構建自定義 IL。當你有一個 JIT 時,這可以工作得很好。但是,當不支持動態代碼時,System.Linq.Expressions 不能使用反射發射,而是回退到使用解釋器。這使得在 CreateFactory 中的這種“優化”實際上是一種去優化,而且它帶來了 System.Linq.Expression.dll 的大小影響。dotnet/runtime#81262 為 !RuntimeFeature.IsDynamicCodeSupported 添加了一個基於反射的替代方案,從而產生更快的代碼,並允許修剪掉 System.Linq.Expression 的使用。
當然,雖然大小是 .NET 8 的一個重點,但是有許多其他方式可以提高 Native AOT 的性能。例如,dotnet/runtime#79709 和 dotnet/runtime#80969 避免了在讀取靜態欄位時的輔助調用。BenchmarkDotNet 也支持 Native AOT,所以我們可以運行以下基準測試進行比較;我們只使用 --runtimes nativeaot7.0 nativeaot8.0,而不使用 --runtimes net7.0 net8.0(BenchmarkDotNet 目前也不支持 Native AOT 的 [DisassemblyDiagnoser]):
// dotnet run -c Release -f net7.0 --filter "*" --runtimes nativeaot7.0 nativeaot8.0
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
BenchmarkSwitcher.FromAssembly(typeof(Tests).Assembly).Run(args);
[HideColumns("Error", "StdDev", "Median", "RatioSD")]
public class Tests
{
private static readonly int s_configValue = 42;
[Benchmark]
public int GetConfigValue() => s_configValue;
}
對於這個,BenchmarkDotNet 輸出:
Method | Runtime | Mean | Ratio |
---|---|---|---|
GetConfigValue | NativeAOT 7.0 | 1.1759 ns | 1.000 |
GetConfigValue | NativeAOT 8.0 | 0.0000 ns | 0.000 |
包括:
// * Warnings *
ZeroMeasurement
Tests.GetConfigValue: Runtime=NativeAOT 8.0, Toolchain=Latest ILCompiler -> The method duration is indistinguishable from the empty method duration
(當看到優化的輸出時,這個警告總是讓我笑了。)
dotnet/runtime#83054 是另一個好例子。它通過確保比較器可以存儲在一個靜態的只讀欄位中,以在消費者中實現更好的常量摺疊,從而改進了 Native AOT 中的 EqualityComparer
// dotnet run -c Release -f net7.0 --filter "*" --runtimes nativeaot7.0 nativeaot8.0
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Runtime.CompilerServices;
BenchmarkSwitcher.FromAssembly(typeof(Tests).Assembly).Run(args);
[HideColumns("Error", "StdDev", "Median", "RatioSD")]
public class Tests
{
private readonly int[] _array = Enumerable.Range(0, 1000).ToArray();
[Benchmark]
public int FindIndex() => FindIndex(_array, 999);
[MethodImpl(MethodImplOptions.NoInlining)]
private static int FindIndex<T>(T[] array, T value)
{
for (int i = 0; i < array.Length; i++)
if (EqualityComparer<T>.Default.Equals(array[i], value))
return i;
return -1;
}
}
Method | Runtime | Mean | Ratio |
---|---|---|---|
FindIndex | NativeAOT 7.0 | 876.2 ns | 1.00 |
FindIndex | NativeAOT 8.0 | 367.8 ns | 0.42 |
作為另一個例子,dotnet/runtime#83911 避免了一些與靜態類初始化相關的開銷。正如我們在 JIT 部分討論的,JIT 能夠依賴分層來知道如果一個方法從 tier 0 提升到 tier 1,那麼方法訪問的靜態欄位必須已經被初始化,但是在 Native AOT 世界中,分層並不存在,所以這個 PR 添加了一個快速路徑檢查,以幫助避免大部分的開銷。
其他基本的支持也有所改進。例如,dotnet/runtime#79519 改變了 Native AOT 的鎖的實現方式,採用了一種混合方法,開始時使用輕量級的自旋鎖,然後升級到使用 System.Threading.Lock 類型(這個類型目前是 Native AOT 的內部類型,但可能在 .NET 9 中公開發佈)。
VM
粗略地說,VM 是運行時的一部分,不包括 JIT 或 GC。它處理的事情包括裝配和類型載入。雖然整個過程中有很多改進,但我將突出三個顯著的改進。
首先,dotnet/runtime#79021 優化了將指令指針映射到 MethodDesc(表示方法的數據結構,包含關於它的各種信息,如其簽名)的操作,這在任何時候進行堆棧遍歷(例如,異常處理,Environment.Stacktrace 等)以及作為一些委托創建的一部分時都會發生。這個改變不僅使這種轉換更快,而且大部分都是無鎖的,這意味著在以下的基準測試中,對於順序使用有顯著的改進,對於多線程使用的改進甚至更大:
// dotnet run -c Release -f net7.0 --filter "*" --runtimes net7.0 net8.0
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Runtime.CompilerServices;
BenchmarkSwitcher.FromAssembly(typeof(Tests).Assembly).Run(args);
[HideColumns("Error", "StdDev", "Median", "RatioSD")]
public class Tests
{
[Benchmark]
public void InSerial()
{
for (int i = 0; i < 10_000; i++)
{
CreateDelegate<string>();
}
}
[Benchmark]
public void InParallel()
{
Parallel.For(0, 10_000, i =>
{
CreateDelegate<string>();
});
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static Action<T> CreateDelegate<T>() => new Action<T>(GenericMethod);
private static void GenericMethod<T>(T t) { }
}
Method | Runtime | Mean | Ratio |
---|---|---|---|
InSerial | .NET 7.0 | 1,868.4 us | 1.00 |
InSerial | .NET 8.0 | 706.5 us | 0.38 |
InParallel | .NET 7.0 | 1,247.3 us | 1.00 |
InParallel | .NET 8.0 | 222.9 us | 0.18 |
其次,dotnet/runtime#83632 提高了 ExecutableAllocator 的性能。這個分配器負責與運行時中所有可執行記憶體相關的分配,例如,JIT 使用它來獲取記憶體,然後將生成的代碼寫入這些記憶體,然後需要執行這些代碼。當記憶體被映射時,它有與之關聯的許可權,用於確定可以對該記憶體進行什麼操作,例如,是否可以讀取和寫入,是否可以執行等。分配器維護一個緩存,這個 PR 通過減少緩存未命中的次數和減少這些緩存未命中時的成本來提高分配器的性能。
第三,dotnet/runtime#85743 進行了一系列的改變,主要目的是顯著減少啟動時間。這包括減少在 R2R 圖像中驗證類型所花費的時間,由於 R2R 圖像中有專用的元數據,使得在 R2R 圖像中查找泛型參數和嵌套類型變得更快,通過在方法描述中存儲一個額外的索引,將 O(n^2) 的查找轉變為 O(1) 的查找,以及確保 vtable 塊始終被共用。
GC
在這篇文章的開頭,我建議在用於運行這篇文章中的基準測試的 csproj 中添加 <ServerGarbageCollection>true</ServerGarbageCollection>
。這個設置將 GC 配置為“伺服器”模式,而不是“工作站”模式。工作站模式是為客戶端應用程式設計的,資源消耗較少,更傾向於使用較少的記憶體,但可能以吞吐量和可擴展性為代價,如果系統承受更重的負載。相反,伺服器模式是為大規模服務設計的。它對資源的需求要大得多,每個邏輯核心預設有一個專用堆,每個堆有一個專用線程來服務該堆,但它也顯著地更可擴展。這種權衡通常會導致複雜性,因為雖然應用程式可能需要伺服器 GC 的可擴展性,但它們也可能希望記憶體消耗接近工作站,至少在需求較低,服務不需要那麼多堆的時候。
在 .NET 8 中,伺服器 GC 現在支持動態堆計數,這要歸功於 dotnet/runtime#86245,dotnet/runtime#87618,和 dotnet/runtime#87619,它們添加了一個被稱為“動態適應應用程式大小”或 DATAS 的特性。它在 .NET 8 中通常是預設關閉的(儘管在為 Native AOT 發佈時預設開啟),但可以很容易地啟用,要麼通過將 DOTNET_GCDynamicAdaptationMode 環境變數設置為 1,要麼通過 <GarbageCollectionAdaptationMode>1</GarbageCollectionAdaptationMode>
MSBuild 屬性。所使用的演算法能夠隨著時間的推移增加和減少堆計數,試圖最大化其對吞吐量的視圖,併在此和總體記憶體占用之間保持平衡。
這裡有一個簡單的例子。我創建了一個控制台應用程式,.csproj 中有 <ServerGarbageCollection>true</ServerGarbageCollection>
,併在 Program.cs 中有以下代碼,它只是生成一堆不斷分配的線程,然後反覆列印出工作集:
// dotnet run -c Release -f net8.0
using System.Diagnostics;
for (int i = 0; i < 32; i++)
{
new Thread(() =>
{
while (true) Array.ForEach(new byte[1], b => { });
}).Start();
}
using Process process = Process.GetCurrentProcess();
while (true)
{
process.Refresh();
Console.WriteLine($"{process.WorkingSet64:N0}");
Thread.Sleep(1000);
}
當我運行這個程式時,我一直看到如下的輸出:
154,226,688
154,226,688
154,275,840
154,275,840
154,816,512
154,816,512
154,816,512
154,824,704
154,824,704
154,824,704
然後,當我在 .csproj 中添加 <GarbageCollectionAdaptationMode>1</GarbageCollectionAdaptationMode>
時,工作集顯著下降:
71,430,144
72,187,904
72,196,096
72,196,096
72,245,248
72,245,248
72,245,248
72,245,248
72,245,248
72,253,440
要更詳細地瞭解這個特性和它的計劃,請參閱“動態適應應用程式大小”。
Mono
到目前為止,我已經提到了“運行時”、“JIT”、“GC”等等。這都是在“CoreCLR”運行時的上下文中,這是用於控制台應用程式、ASP.NET 應用程式、服務、桌面應用程式等的主要運行時。然而,對於移動和瀏覽器 .NET 應用程式,使用的主要運行時是“Mono”運行時。在 .NET 8 中,它也有了一些巨大的改進,這些改進對於像 Blazor WebAssembly 應用這樣的場景有所幫助。
正如 CoreCLR 既有 JIT 又有 AOT 的能力一樣,Mono 也有多種方式可以發佈代碼。Mono 包括一個 AOT 編譯器;對於 WASM 特別是,AOT 編譯器使所有的 IL 都可以編譯成 WASM,然後發送到瀏覽器。然而,就像 CoreCLR 一樣,AOT 是可選的。WASM 的預設體驗是使用解釋器:IL 被髮送到瀏覽器,然後解釋器(本身就是編譯成 WASM 的)解釋 IL。當然,解釋有性能影響,所以 .NET 7 增強瞭解釋器,使用了一個類似於 CoreCLR JIT 使用的分層方案。解釋器有自己的代碼表示,當一個方法被調用幾次時,它只是解釋那個位元組碼,幾乎不做優化。然後在足夠多的調用之後,解釋器會花一些時間優化那個內部表示,以加速後續的解釋。然而,即使是這樣,它仍然是在解釋:它仍然是一個在 WASM 中實現的解釋器,讀取指令並執行它們。在 .NET 8 中,Mono 的最顯著的改進之一是在解釋器中引入了一個部分 JIT,擴展了這個分層。dotnet/runtime#76477 提供了這個“jiterpreter”的初始代碼,有些人就是這樣稱呼它的。作為解釋器的一部分,這個 JIT 能夠參與解釋器使用的相同的數據結構,並處理相同的位元組碼,它通過替換那個位元組碼的序列與即時生成的 WASM 來工作。這可能是一個整個方法,也可能只是一個方法中的熱迴圈,或者只是幾條指令。這提供了顯著的靈活性,包括一個非常漸進的入口,可以逐步添加優化,將越來越多的邏輯從解釋轉移到 JIT 的 WASM。數十個 PR 為 .NET 8 的 jiterpreter 成為現實做出了貢獻,比如 dotnet/runtime#82773 添加了基本的 SIMD 支持,dotnet/runtime#82756 添加了基本的迴圈支持,和 dotnet/runtime#83247 添加了一個控制流優化通道。
讓我們看看這個在實踐中的應用。我創建了一個新的 .NET 7 Blazor WebAssembly 項目,添加了對 System.IO.Hashing 項目的 NuGet 引用,並將 Counter.razor 的內容替換為以下內容:
@page "/counter"
@using System.Diagnostics;
@using System.IO.Hashing;
@using System.Text;
@using System.Threading.Tasks;
<h1>.NET 7</h1>
<p role="status">Current time: @_time</p>
<button class="btn btn-primary" @onclick="Hash">Click me</button>
@code {
private TimeSpan _time;
private void Hash()
{
var sw = Stopwatch.StartNew();
for (int i = 0; i < 50_000; i++) XxHash64.HashToUInt64(_data);
_time = sw.Elapsed;
}
private byte[] _data =
@"Shall I compare thee to a summer's day?
Thou art more lovely and more temperate:
Rough winds do shake the darling buds of May,
And summer's lease hath all too short a date;
Sometime too hot the eye of heaven shines,
And often is his gold complexion dimm'd;
And every fair from fair sometime declines,
By chance or nature's changing course untrimm'd;
But thy eternal summer shall not fade,
Nor lose possession of that fair thou ow'st;
Nor shall death brag thou wander'st in his shade,
When in eternal lines to time thou grow'st:
So long as men can breathe or eyes can see,
So long lives this, and this gives life to thee."u8.ToArray();
}
然後我做了完全相同的事情,但是對於 .NET 8,我在 Release 中構建了它們,並運行了它們。當每個結果頁面打開時,我點擊了“Click me”按鈕(點擊了幾次,但結果沒有改變)。
NET 7 與 .NET 8 中操作所需時間的測量結果自明。
除了 jiterpreter,解釋器本身也有許多改進,例如:
dotnet/runtime#79165 為 stobj IL 指令添加了特殊處理,當值類型不包含任何引用,因此不需要與 GC 交互。
dotnet/runtime#80046 對比較後的 brtrue/brfalse 進行了特殊處理,為非常常見的模式創建了一個解釋器操作碼。
dotnet/runtime#79392 為解釋器添加了一個用於字元串創建的內置函數。
dotnet/runtime#78840 為 Mono 運行時(包括但不限於解釋器)添加了一個緩存,用於存儲關於類型的各種信息,如 IsValueType,IsGenericTypeDefinition 和 IsDelegate。
dotnet/runtime#81782 為 Vector128 上的一些最常見操作添加了內置函數,dotnet/runtime#86859 增強了這個功能,以便對 Vector
dotnet/runtime#83498 對 2 的冪的除法進行了特殊處理,以使用移位操作代替。
dotnet/runtime#83490 調整了內聯大小限制,以確保關鍵方法可以被內聯,如 List
dotnet/runtime#85528 在有足夠類型信息的情況下添加了去虛化支持。
我已經多次提到 Mono 中的向量化,但這本身就是 Mono 在 .NET 8 中的所有後端的一個重點關註領域。截至 dotnet/runtime#86546,該 PR 完成了對 Mono 的 AMD64 JIT 後端的 Vector128 支持,現在所有的 Mono 後端都支持 Vector128。Mono 的 WASM 後端不僅支持 Vector128,.NET 8 還包括新的 System.Runtime.Intrinsics.Wasm.PackedSimd 類型,這是特定於 WASM 的,並暴露了數百個重載,這些重載映射到 WASM SIMD 操作。這個類型的基礎在 dotnet/runtime#73289 中引入,其中添加了初始的 SIMD 支持作為內部功能。dotnet/runtime#76539 通過添加更多功能並將類型公開,繼續了這項工作,就像現在在 .NET 8 中一樣。十幾個 PR 繼續構建它,比如 dotnet/runtime#80145 添加了 ConditionalSelect 內置函數,dotnet/runtime#87052 和 dotnet/runtime#87828 添加了載入和存儲內置函數,dotnet/runtime#85705 添加了浮點支持,以及 dotnet/runtime#88595,它根據自初始設計以來的學習成果對錶面區域進行了改造。
.NET 8 中,另一個與應用大小相關的工作是減少對 ICU 數據文件的依賴(ICU 是 .NET 和許多其他系統使用的全球化庫)。相反,目標是儘可能依賴目標平臺的原生 API(對於 WASM,由瀏覽器提供的 API)。這個工作被稱為“混合全球化”,因為對 ICU 數據文件的依賴仍然存在,只是減少了,並且它帶來了行為上的變化,所以它是可選的,適用於真正希望減小大小並願意處理行為適應的情況。許多 PR 也為 .NET 8 實現這一目標做出了貢獻,如 dotnet/runtime#81470,dotnet/runtime#84019 和 dotnet/runtime#84249。要啟用這個特性,你可以在你的 .csproj 中添加
原載:http://www.cnblogs.com/yahle
版權所有。轉載時必須以鏈接形式註明作者和原始出處。
歡迎贊助: