本文也叫 跟著 Stephen Toub 大佬學性能優化系列,大家都知道在 .NET 7 有眾多的性能優化,其中就包括了對布爾和字串互轉的性能優化,在對布爾和字串的轉換的性能優化上,有著非常巧妙的思路,值得寫篇博客記錄
在 Performance Improvements in .NET 7 - .NET Blog 這篇官方開發博客里面提到了 dotnet 7 的各個方面的性能優化,其中就包括了本身的主角,對 bool 與 string 互轉的性能優化,此優化的核心實作代碼請參閱 https://github.com/dotnet/runtime/pull/64782
在將 "True" 和 "False" 字串和 bool 布爾互相轉換時,假定不使用框架內置的轉換方法,那將會如何做呢? 先從字串轉到布爾開始
開始轉換的第一步可以通過字串的長度進行快速的判斷,例如無論是 "True" 還是 "False" 字串,字符的長度都一定是大于 3 的,因此第一步可以嘗試判斷一下,是否大于 3 個字符,如果這一條不滿足,那肯定不能作為布爾的轉換字串了,如果剛好等于 4 個字符長度,那也許就是 "True" 字串了
轉換字串為布爾時,將無視字串的大小寫,人類方便理解的一個版本如下
internal static bool IsTrueStringIgnoreCase(ReadOnlySpan<char> value)
{
return value.Length == 4 &&
(value[0] == 't' || value[0] == 'T') &&
(value[1] == 'r' || value[1] == 'R') &&
(value[2] == 'u' || value[2] == 'U') &&
(value[3] == 'e' || value[3] == 'E');
}
沒錯,就是每個字串都判斷一下,剛好這就是 dotnet 6 所采用的轉換方法,同理,判斷 "False" 也是類似的代碼邏輯
internal static bool IsFalseStringIgnoreCase(ReadOnlySpan<char> value)
{
return value.Length == 5 &&
(value[0] == 'f' || value[0] == 'F') &&
(value[1] == 'a' || value[1] == 'A') &&
(value[2] == 'l' || value[2] == 'L') &&
(value[3] == 's' || value[3] == 'S') &&
(value[4] == 'e' || value[4] == 'E');
}
假定要對以上的代碼進行性能優化,可以怎么做呢?似乎我想不到有什么方法
在閱讀了 Stephen Toub 大佬的優化,我才了解到原來還有如此的新思路,通過編碼的方式進行優化,剛好的是在 C# 里面字串的記憶體存盤編碼采用的是 Utf16 編碼,采用 UTF16 編碼的好處在于任意字符所在的記憶體的偏移量是可以完全瞬時確定的,可以快速和二進制進行映射從而提供更多優化的方向,例如剛好一個 UInt64 也就是一個 ulong 的長度就等于 4 個字符,而剛好 "True" 字串就是 4 個字符
同樣感謝遠古的 ASCII 編碼的定義的超級大佬,英文的大小寫轉換只需要取一個 0x20 的或即可讓字符轉換為小寫,無論之前的字符是大寫還是小寫,于是先對輸入的字串內容,從字串取其中前 4 個字符,轉換為 UInt64 的資料,轉換方法如下
internal static bool IsTrueStringIgnoreCase(ReadOnlySpan<char> value)
{
var theUInt64Value = https://www.cnblogs.com/lindexi/archive/2022/09/29/BinaryPrimitives.ReadUInt64LittleEndian(MemoryMarshal.AsBytes(value));
// 忽略代碼
}
通過 MemoryMarshal.AsBytes 方法,快速將 value 映射為 byte 陣列,這里必須說明的是,通過 MemoryMarshal.AsBytes 方法是做記憶體映射而不是一個轉換的程序,這是非常快速且安全的一個程序,接著呼叫 BinaryPrimitives.ReadUInt64LittleEndian 方法轉換為 UInt64 的資料
根據上文的 ASCII 編碼規則,為了方便判斷邏輯,將轉換的結果全部轉換為小寫的內容,轉換方法是使用 或 對每個字符進行 或 邏輯,代碼如下
var caseValue = https://www.cnblogs.com/lindexi/archive/2022/09/29/theUInt64Value | 0x0020002000200020;
由于字符使用 UTF16 編碼,需要每個使用 0x0020 進行 或 邏輯,連起來就是 0020_0020_0020_0020 的數值
接著將 true 字串轉換為 UInt64 資料,轉換之后的常量的編碼是 0x65007500720074 這個數字,剛好進行一次 UInt64 判斷即可,耗時可以被忽略
return caseValue =https://www.cnblogs.com/lindexi/archive/2022/09/29/= 0x65007500720074
干掉這些變數,連成一句代碼,最終的代碼如下
internal static bool IsTrueStringIgnoreCase(ReadOnlySpan<char> value) =>
value.Length == 4 &&
(BinaryPrimitives.ReadUInt64LittleEndian(MemoryMarshal.AsBytes(value)) | 0x0020002000200020) == 0x65007500720074; // "true" as a ulong, each char |'d with 0x0020 for case-insensitivity
以上代碼的 => 是替換原本的方法體的簡寫代碼,原本的代碼里面,方法是使用 {} 組織方法體代碼,在新 C# 里面,如果方法體的代碼只有一句話,那就和辣么大一樣,用箭頭的方式即可,如此可以讓代碼更加簡短,同時方便淘汰那些落后的開發者
同理對 "False" 字串也進行相同的處理,嗯,對于我這個學渣來說,最怕看到“同理”這兩個字,因為我拿出腳趾都算不出來,好像 "False" 字串有五個字符吧,大家算算看
那既然有五個字符,這就意味著不能整個字串轉換為 UInt64 了,畢竟 5 x 16 > 64 了,那咋辦呢?沒關系,先取 "False" 字串前面的 "Fals" 字符出來,按照上文的方式進行比較,最后再比較 e 這個字符好了
internal static bool IsFalseStringIgnoreCase(ReadOnlySpan<char> value) =>
value.Length == 5 &&
(((BinaryPrimitives.ReadUInt64LittleEndian(MemoryMarshal.AsBytes(value)) | 0x0020002000200020) == 0x73006C00610066) & // "fals" as a ulong, each char |'d with 0x0020 for case-insensitivity
((value[4] | 0x20) == 'e'));
這能提升多少呢?非常多,大佬的基準測驗如下,可以看到在 .NET 7 的耗時,接近是 .NET 6 的三分之一,優化特別大
| Method | Runtime | Mean | Ratio |
|---|---|---|---|
| ParseTrue | .NET 6.0 | 7.347 ns | 1.00 |
| ParseTrue | .NET 7.0 | 2.327 ns | 0.32 |
在從字串轉換,可以使用整數對比和轉換的方法提升性能,那轉換為字串呢?其實也相同,也可以使用相同的方法,嗯,又是同理,同理,在將布爾轉換為字串時,可以通過寫入整數的方式提升性能
例如將 true 寫為 "True" 字串,原本的寫入采用的是如下的方法
public bool TryFormat(Span<char> destination, out int charsWritten)
{
// 這里的 m_value 就是實際存盤的欄位的值,表示當前的布林值
// 這里傳入的 destination 則是一段需要被寫入的字符內容,咱這個函式就是需要在
// 傳入的 destination 里將布林值作為 True 或 False 字串寫入
// 寫入成功回傳 true 的值,且記錄 charsWritten 說明寫入了多少個字符了
///寫入失敗,回傳 false 的值
if (m_value)
{
// 在這里需要寫入 "True" 字串到 destination 里
if ((uint)destination.Length > 3) // 如果長度都還沒達到能寫入 "True" 字串的最小所需空間,那就不干活了
{
destination[0] = 'T';
destination[1] = 'r';
destination[2] = 'u';
destination[3] = 'e';
charsWritten = 4;
return true;
}
}
}
上面的代碼也看到起來非常方便理解,在了解了可以使用整數的方式提升性能之后,試試換成使用整數賦值的方式,更改之后的代碼如下
public bool TryFormat(Span<char> destination, out int charsWritten)
{
// 這里的 m_value 就是實際存盤的欄位的值,表示當前的布林值
// 這里傳入的 destination 則是一段需要被寫入的字符內容,咱這個函式就是需要在
// 傳入的 destination 里將布林值作為 True 或 False 字串寫入
// 寫入成功回傳 true 的值,且記錄 charsWritten 說明寫入了多少個字符了
///寫入失敗,回傳 false 的值
if (m_value)
{
// 在這里需要寫入 "True" 字串到 destination 里
if ((uint)destination.Length > 3) // 如果長度都還沒達到能寫入 "True" 字串的最小所需空間,那就不干活了
{
// destination[0] = 'T';
// destination[1] = 'r';
// destination[2] = 'u';
// destination[3] = 'e';
BinaryPrimitives.WriteUInt64LittleEndian(MemoryMarshal.AsBytes(destination), 0x65007500720054); // "True"
charsWritten = 4;
return true;
}
}
}
先使用 MemoryMarshal.AsBytes 方法將要寫入的 destination 當成 byte 二進制,接著使用 BinaryPrimitives.WriteUInt64LittleEndian 將 "True" 字串對應的整數寫入到二進制里面去,如此即可進行快速的完成寫入字串
同理,也對 "False" 字串進行寫入
BinaryPrimitives.WriteUInt64LittleEndian(MemoryMarshal.AsBytes(destination), 0x73006C00610046); // "Fals"
destination[4] = 'e';
charsWritten = 5;
如此寫入的性能提升也是很多的,大佬的基準測驗里面,對比 dotnet 6 的版本,耗時比例是之前的 0.66 倍
| Method | Runtime | Mean | Ratio |
|---|---|---|---|
| FormatTrue | .NET 6.0 | 3.030 ns | 1.00 |
| FormatTrue | .NET 7.0 | 1.997 ns | 0.66 |
在底層里面,對布爾這樣的型別進行優化是非常有必要的,可以極大的提升整個上層應用的性能,因為如此底層的邏輯,在很多角落里,無論是否被程式員所關注到,都會被默默的很多次的被呼叫
既然在性能優化的角度上,通過使用整數替換逐個字符的方式,可以提升一些性能,既然學到了,那肯定要用上來呀,我在檔案下載庫的斷點續傳保存檔案里面,就嘗試使用此思想撰寫一點代碼,代碼請看 https://github.com/dotnet-campus/dotnetCampus.FileDownloader/blob/aa99be3a6c9efe5bc590111a5a4b95085061d622/src/dotnetCampus.FileDownloader/Utils/BreakpointResumptionTransmissions/BreakpointResumptionTransmissionRecordFileFormatter.cs#L128-L136
原本我是期望寫入的是 DCFBPRTI 的 ASCII 編碼的字串的,現在更換為使用一個 long 代替,如下面代碼
private static long GetHeader()
{
// 檔案頭是 dotnet campus File Downloader BreakPointResumptionTransmissionInfo 幾個單詞的首個字符 DCFBPRTI 縮寫的 ASCII 值
// 剛好將這個 ASCII 的 byte 陣列轉換為一個 long 的值
//var headerByteList = System.Text.Encoding.ASCII.GetBytes("DCFBPRTI");
// var headerByteList = new byte[] { 68, 67, 70, 66, 80, 82, 84, 73 };
//return BitConverter.ToInt64(headerByteList)
return 5283938767475196740;
}
以上的代碼只是因為我學到了這個方式進行優化,強行想試試而已,不代表著在業務代碼里面一定要使用此方式哦
其實在撰寫代碼的時候,以可讀性為第一,除非遇到的模塊是屬于性能敏感的,但愿閱讀本文不會帶壞一些新手開發者,讓新手開發者想著在任何的地方強行使用寫整數代替可讀性比較高的字串處理方法
博客園博客只做備份,博客發布就不再更新,如果想看最新博客,請到 https://blog.lindexi.com/

本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可,歡迎轉載、使用、重新發布,但務必保留文章署名[林德熙](http://blog.csdn.net/lindexi_gd)(包含鏈接:http://blog.csdn.net/lindexi_gd ),不得用于商業目的,基于本文修改后的作品務必以相同的許可發布,如有任何疑問,請與我[聯系](mailto:[email protected]),
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/509803.html
標籤:.NET技术
