作者:Casey McQuillan
譯者:精緻碼農
原文:http://dwz.win/YVW
說明:原文比較長,翻譯時精簡了很多内容,對于不重要的細枝末節隻用了一句話概括,但不并影響閱讀。
你還記得上一次一個無足輕重的細節點燃你思考火花的時刻嗎?作為一個軟體工程師,我習慣于專注于一個從未見過的微小細節。那一時刻,我大腦的齒輪會開始轉動,我喜歡這樣的時刻。
最近,我在逛 Twitter 時發生了一件事。我看到了 David Fowler 和 Damian Edwards 之間的這段交流,他們讨論了 .NET 的
Span<T>
API。我以前使用過
Span<T>
API,但我在推文中發現了一些不一樣的新東西。
上面使用的
String.Create
方法是我從未見過的用法。我決定要揭開
String.Create
的神秘面紗。此時我在問自己一個問題:
為什麼用這個方法建立字元串而不用其它的?
我便開始探索,它把我帶到了一些有趣的地方,我想和你分享。在本文中,我們将深入探讨幾個話題:
-
與其它 API 有什麼不同?String.Create
-
做得更好的是什麼,它如何讓我的 C# 代碼更快?String.Create
-
的性能能提高多少?String.Create
為了書寫友善,我将用下面的詞來指代 .NET 中的幾個 API:
- Create — 指代
String.Create()
- Concat — 指代
或String.Concat()
操作符+
- StringBuilder — 指代
構造字元串或使用其流式 API。StringBuilder
它是如何工作的
.NET Core 代碼庫是在 GitHub 開源的,這提供了一個很好的機會來深入分析微軟自己的實踐。他們提供了 Create API,是以看看他們如何使用它,應該能找到有價值的發現。讓我們從深入了解
String
對象及其相關 API 開始。
要想從原始字元資料中構造一個
string
,你需要使用構造函數,它需要一個指向
char
數組的指針。如果直接使用這個 API,則需要将單個字元放入特定的數組位置。下面是使用這個構造函數配置設定一個字元串的代碼。建立字元串的方法還有很多,但這是我認為與 Create 方法最相近的。
string Ctor(char[]? value)
{
if (value == null || value.Length == 0)
return Empty;
string result = FastAllocateString(value.Length);
Buffer.Memmove(
elementCount: (uint)result.Length, // derefing Length now allows JIT to prove 'result' not null below
destination: ref result._firstChar,
source: ref MemoryMarshal.GetArrayDataReference(value));
return result;
}
這裡的兩個重要步驟是:
- 根據數組長度使用
配置設定記憶體。FastAllocateString
是在 .NET Runtime 中實作的,它幾乎是所有字元串配置設定記憶體的基礎。FastAllocateString
- 調用
,它将原來數組中的所有位元組複制到新配置設定的字元串中。Buffer.Memmove
要使用這個構造函數,我們需要向它提供一個
char
數組。在它的工作完成後,我們最終會得到一個(目前不必要的)
char
數組和一個字元串,數組有與字元串相同的資料。如果我們要修改原來的數組,字元串是不會被修改的,因為它是一個獨立的、不同的資料副本。在高性能的 .NET 環境中,節省對象和數組的記憶體配置設定是非常有價值的,因為它減少了 .NET 垃圾回收器每次運作時需要做的工作。每一個留在記憶體中的額外對象都會增加收集的頻率,并損耗總性能。
為了與構造函數形成對比,并消除這種不必要的記憶體配置設定,我們來看一下 Create 方法的代碼。
public static string Create<TState>(int length, TState state, SpanAction<char, TState> action)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
if (length <= 0)
{
if (length == 0)
return Empty;
throw new ArgumentOutOfRangeException(nameof(length));
}
string result = FastAllocateString(length);
action(new Span<char>(ref result.GetRawStringData(), length), state);
return result;
}
步驟相似,但有一個關鍵的差別:
-
根據FastAllocateString
參數配置設定記憶體。length
- 将新配置設定的
轉換為string
。Span<char>
-
,并将action
執行個體與Span<char>
作為參數。state
這種方法避免了多餘的記憶體配置設定,因為它允許我們傳入
SpanAction
,這是一組有關如何建立字元串的方法,而不是要求我們将需要放入字元串中的所有位元組進行二次複制。
對比上面兩張圖,圖二的 Create 比圖一構造函數少了一塊記憶體配置設定。
String.Create 好在哪
此時,你可能會對Create方法感到好奇,但你不一定知道為什麼它比你之前使用過的方法更好。Create API 的用處是因地制宜的,但在适當的情況下,它可以發揮極大的威力。
- 它會預先配置設定一塊記憶體空間,然後給你一個接口來安全地填充這個空間。其他建立字元串的方法可能需要編寫不安全代碼或管理緩沖池。
- 它避免了對資料進行額外的複制操作,這通常使記憶體的配置設定更少。這也減少了來自垃圾收集器的壓力,可以加快程式的整體效率。
- 它允許你将高性能代碼集中在應用程式的業務需求上,而不是将你的字元串建構代碼與複雜的記憶體管理交織在一起。
ID生成器示例
隻有當你已經知道最終字元串的長度時,你才能使用Create方法。然而,你可以創造性地使用這個限制,并發現幾種利用Create的方法。我在 dotnet/aspnetcore 和 dotnet/runtime 的代碼庫中進行了搜尋,看看微軟團隊在哪些地方用了這個API。
下面這個類來自 ASP.NET Core 倉庫,用來為每個Web請求生成相關ID。這些ID的格式由數字(0-9)和大寫字母(A-V)組成。
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading;
namespace Microsoft.AspNetCore.Connections
{
internal static class CorrelationIdGenerator
{
// Base32 encoding - in ascii sort order for easy text based sorting
private static readonly char[] s_encode32Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUV".ToCharArray();
// Seed the _lastConnectionId for this application instance with
// the number of 100-nanosecond intervals that have elapsed since 12:00:00 midnight, January 1, 0001
// for a roughly increasing _lastId over restarts
private static long _lastId = DateTime.UtcNow.Ticks;
public static string GetNextId() => GenerateId(Interlocked.Increment(ref _lastId));
private static string GenerateId(long id)
{
return string.Create(13, id, (buffer, value) =>
{
char[] encode32Chars = s_encode32Chars;
buffer[12] = encode32Chars[value & 31];
buffer[11] = encode32Chars[(value >> 5) & 31];
buffer[10] = encode32Chars[(value >> 10) & 31];
buffer[9] = encode32Chars[(value >> 15) & 31];
buffer[8] = encode32Chars[(value >> 20) & 31];
buffer[7] = encode32Chars[(value >> 25) & 31];
buffer[6] = encode32Chars[(value >> 30) & 31];
buffer[5] = encode32Chars[(value >> 35) & 31];
buffer[4] = encode32Chars[(value >> 40) & 31];
buffer[3] = encode32Chars[(value >> 45) & 31];
buffer[2] = encode32Chars[(value >> 50) & 31];
buffer[1] = encode32Chars[(value >> 55) & 31];
buffer[0] = encode32Chars[(value >> 60) & 31];
});
}
}
}
算法很簡單:
- 使用UTC的最新Tick計數作為ID的起始值,Tick計數數是一個64位的整數。
- 在每次請求新的ID時以一遞增。
- 将值右移5(
)位,擷取最右邊的5位(character_index * 5
),并根據預先确定的字元表(shifted_value & 31
)選擇一個字元,從後向前填充到encode32Chars
buffer
譯者注:64位的整數,每5位一劃分可劃為13段,前十二段為5位,最後一段為4位。之是以5位一劃分是因為 2^5-1=31,可以確定字元表()的每個字元都可以被索引到(
encode32Chars
為
encode32Chars[31]
)。若以4位劃分,則最大的索引是15,字元表就有一半的字元輪空。
V
我們用 StringBuilder 作為我們比較對象。我之是以選擇StringBuilder,是因為它通常被推薦為正常字元串拼接性能較好的API。我寫了額外的實作,嘗試使用StringBuilder(有容量)、StringBuilder(無容量)和簡單拼接。
運作性能 Benchmarks:
記憶體配置設定 Benchmarks:
String.Create()
方法在性能(16.58納秒)和記憶體配置設定(隻有48 bytes)方面表現得最好。
字元串拼接優化示例
C# Roslyn 編譯器在優化字元串拼接時非常聰明。編譯器會傾向于将多次使用加号
+
運算符轉換為對 Concat 的單次調用,并且很可能有許多我不知道的額外技巧。由于這些原因,拼接通常是一個快速的操作,但在簡單場景下,它仍然可以用 Create 替代。
用 Create 方法示範拼接的示例代碼:
public static class ConcatenationStringCreate
{
public static string Concat(string first, string second)
{
first ??= string.Empty;
second ??= String.Empty;
bool addSpace = second.Length > 0;
int length = first.Length + (addSpace ? 1 : 0) + second.Length;
return string.Create(length, (first, second, addSpace),
(dst, v) =>
{
ReadOnlySpan<char> prefix = v.first;
prefix.CopyTo(dst);
if (v.addSpace)
{
dst[prefix.Length] = ' ';
ReadOnlySpan<char> detail = v.second;
detail.CopyTo(dst.Slice(prefix.Length + 1, detail.Length));
}
});
}
}
我在 .NET Core 源代碼中隻找到一個真正的例子後,就寫了這個特殊的示例。這像是一個可以合理抽象的示例,并且可以在重度使用加号
+
操作符或
String.Concat
的代碼庫中使用。
下面是運作性能和記憶體配置設定的 Benchmarks:
Create 要比 Concat (加号
+
String.Concat
)快那麼幾個百分點。對于大部分場景,Concat 拼接的性能還是可以的,不需要封裝 Create 方法做優化。但如果你是以每秒幾百萬的速度拼接字元串(比如一個高流量的Web應用),性能提高幾個百分點也是值得的。
用與不用
String.Create 雖然有較好的性能,但一般隻在性能要求較高場景下使用。一個良好的系統取決于很多名額,作為軟體工程師,我們不能隻追求性能名額,而忽略了大局。一般來說,我認為簡潔可維護的代碼應該優于夢幻般的性能。
本文性能測試的有關代碼都放在了 GitHub:
https://github.com/cmcquillan/StringCreateBenchmarks
作者:精緻碼農-王亮
出處:http://cnblogs.com/willick
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。如有問題或建議,請多多賜教,非常感謝。