<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          深入解析 C# 的 String.Create 方法

          共 4954字,需瀏覽 10分鐘

           ·

          2020-12-04 15:10

          作者:Casey McQuillan

          譯者:精致碼農(nóng)

          原文:http://dwz.win/YVW

          說明:原文比較長,翻譯時(shí)精簡了很多內(nèi)容,對于不重要的細(xì)枝末節(jié)只用了一句話概括,但不并影響閱讀。

          你還記得上一次一個(gè)無足輕重的細(xì)節(jié)點(diǎn)燃你思考火花的時(shí)刻嗎?作為一個(gè)軟件工程師,我習(xí)慣于專注于一個(gè)從未見過的微小細(xì)節(jié)。那一時(shí)刻,我大腦的齒輪會(huì)開始轉(zhuǎn)動(dòng),我喜歡這樣的時(shí)刻

          最近,我在逛 Twitter 時(shí)發(fā)生了一件事。我看到了 David Fowler 和 Damian Edwards 之間的這段交流,他們討論了 .NET 的?Span?API。我以前使用過?Span?API,但我在推文中發(fā)現(xiàn)了一些不一樣的新東西。

          上面使用的?String.Create?方法是我從未見過的用法。我決定要揭開?String.Create?的神秘面紗。此時(shí)我在問自己一個(gè)問題:

          為什么用這個(gè)方法創(chuàng)建字符串而不用其它的?

          我便開始探索,它把我?guī)У搅艘恍┯腥さ牡胤剑蚁牒湍惴窒怼T诒疚闹校覀儗⑸钊胩接憥讉€(gè)話題:

          • String.Create?與其它 API 有什么不同?

          • String.Create?做得更好的是什么,它如何讓我的 C# 代碼更快?

          • String.Create?的性能能提高多少?

          為了書寫方便,我將用下面的詞來指代 .NET 中的幾個(gè) API:

          • Create?— 指代?String.Create()

          • Concat?— 指代?String.Concat()+操作符

          • StringBuilder?— 指代StringBuilder構(gòu)造字符串或使用其流式 API。

          它是如何工作的

          .NET Core 代碼庫是在 GitHub 開源的,這提供了一個(gè)很好的機(jī)會(huì)來深入分析微軟自己的實(shí)踐。他們提供了?Create?API,所以看看他們?nèi)绾问褂盟瑧?yīng)該能找到有價(jià)值的發(fā)現(xiàn)。讓我們從深入了解?String?對象及其相關(guān) API 開始。

          要想從原始字符數(shù)據(jù)中構(gòu)造一個(gè)?string,你需要使用構(gòu)造函數(shù),它需要一個(gè)指向?char?數(shù)組的指針。如果直接使用這個(gè) API,則需要將單個(gè)字符放入特定的數(shù)組位置。下面是使用這個(gè)構(gòu)造函數(shù)分配一個(gè)字符串的代碼。創(chuàng)建字符串的方法還有很多,但這是我認(rèn)為與 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;
          }

          這里的兩個(gè)重要步驟是:

          • 根據(jù)數(shù)組長度使用?FastAllocateString?分配內(nèi)存。FastAllocateString?是在 .NET Runtime 中實(shí)現(xiàn)的,它幾乎是所有字符串分配內(nèi)存的基礎(chǔ)。

          • 調(diào)用?Buffer.Memmove,它將原來數(shù)組中的所有字節(jié)復(fù)制到新分配的字符串中。

          要使用這個(gè)構(gòu)造函數(shù),我們需要向它提供一個(gè)?char?數(shù)組。在它的工作完成后,我們最終會(huì)得到一個(gè)(當(dāng)前不必要的)char?數(shù)組和一個(gè)字符串,數(shù)組有與字符串相同的數(shù)據(jù)。如果我們要修改原來的數(shù)組,字符串是不會(huì)被修改的,因?yàn)樗且粋€(gè)獨(dú)立的、不同的數(shù)據(jù)副本。在高性能的 .NET 環(huán)境中,節(jié)省對象和數(shù)組的內(nèi)存分配是非常有價(jià)值的,因?yàn)樗鼫p少了 .NET 垃圾回收器每次運(yùn)行時(shí)需要做的工作。每一個(gè)留在內(nèi)存中的額外對象都會(huì)增加收集的頻率,并損耗總性能。

          為了與構(gòu)造函數(shù)形成對比,并消除這種不必要的內(nèi)存分配,我們來看一下?Create?方法的代碼。

          public?static?string Create(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;
          }

          步驟相似,但有一個(gè)關(guān)鍵的區(qū)別:

          1. FastAllocateString?根據(jù)?length?參數(shù)分配內(nèi)存。

          2. 將新分配的?string?轉(zhuǎn)換為?Span

          3. 調(diào)用?action,并將?Span?實(shí)例與?state?作為參數(shù)。

          這種方法避免了多余的內(nèi)存分配,因?yàn)樗试S我們傳入?SpanAction,這是一組有關(guān)如何創(chuàng)建字符串的方法,而不是要求我們將需要放入字符串中的所有字節(jié)進(jìn)行二次復(fù)制。

          對比上面兩張圖,圖二的?Create?比圖一構(gòu)造函數(shù)少了一塊內(nèi)存分配。

          String.Create 好在哪

          此時(shí),你可能會(huì)對Create方法感到好奇,但你不一定知道為什么它比你之前使用過的方法更好。Create API 的用處是因地制宜的,但在適當(dāng)?shù)那闆r下,它可以發(fā)揮極大的威力。

          • 它會(huì)預(yù)先分配一塊內(nèi)存空間,然后給你一個(gè)接口來安全地填充這個(gè)空間。其他創(chuàng)建字符串的方法可能需要編寫不安全代碼或管理緩沖池。

          • 它避免了對數(shù)據(jù)進(jìn)行額外的復(fù)制操作,這通常使內(nèi)存的分配更少。這也減少了來自垃圾收集器的壓力,可以加快程序的整體效率。

          • 它允許你將高性能代碼集中在應(yīng)用程序的業(yè)務(wù)需求上,而不是將你的字符串構(gòu)建代碼與復(fù)雜的內(nèi)存管理交織在一起。

          ID生成器示例

          只有當(dāng)你已經(jīng)知道最終字符串的長度時(shí),你才能使用Create方法。然而,你可以創(chuàng)造性地使用這個(gè)約束,并發(fā)現(xiàn)幾種利用Create的方法。我在 dotnet/aspnetcore 和 dotnet/runtime 的代碼庫中進(jìn)行了搜索,看看微軟團(tuán)隊(duì)在哪些地方用了這個(gè)API。

          下面這個(gè)類來自 ASP.NET Core 倉庫,用來為每個(gè)Web請求生成相關(guān)ID。這些ID的格式由數(shù)字(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計(jì)數(shù)作為ID的起始值,Tick計(jì)數(shù)數(shù)是一個(gè)64位的整數(shù)。

          • 在每次請求新的ID時(shí)以一遞增。

          • 將值左移5(character_index * 5)位,獲取最右邊的5位(shifted_value & 31),并根據(jù)預(yù)先確定的字符表(encode32Chars)選擇一個(gè)字符,從后向前填充到buffer

          譯者注:64位的整數(shù),每5位一劃分可劃為13段,前十二段為5位,最后一段為4位。之所以5位一劃分是因?yàn)?2^5-1=31,可以確保字符表(encode32Chars)的每個(gè)字符都可以被索引到(encode32Chars[31]?為?V)。若以4位劃分,則最大的索引是15,字符表就有一半的字符輪空。

          我們用 StringBuilder 作為我們比較對象。我之所以選擇StringBuilder,是因?yàn)樗ǔ1煌扑]為常規(guī)字符串拼接性能較好的API。我寫了額外的實(shí)現(xiàn),嘗試使用StringBuilder(有容量)、StringBuilder(無容量)和簡單拼接。

          運(yùn)行性能 Benchmarks:

          內(nèi)存分配 Benchmarks:

          String.Create()?方法在性能(16.58納秒)和內(nèi)存分配(只有48 bytes)方面表現(xiàn)得最好。

          字符串拼接優(yōu)化示例

          C# Roslyn 編譯器在優(yōu)化字符串拼接時(shí)非常聰明。編譯器會(huì)傾向于將多次使用加號(hào)?+?運(yùn)算符轉(zhuǎn)換為對 Concat 的單次調(diào)用,并且很可能有許多我不知道的額外技巧。由于這些原因,拼接通常是一個(gè)快速的操作,但在簡單場景下,它仍然可以用 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 源代碼中只找到一個(gè)真正的例子后,就寫了這個(gè)特殊的示例。這像是一個(gè)可以合理抽象的示例,并且可以在重度使用加號(hào)?+?操作符或?String.Concat?的代碼庫中使用。

          下面是運(yùn)行性能和內(nèi)存分配的 Benchmarks:

          Create 要比 Concat (加號(hào)?+?操作符或?String.Concat)快那么幾個(gè)百分點(diǎn)。對于大部分場景,Concat 拼接的性能還是可以的,不需要封裝 Create 方法做優(yōu)化。但如果你是以每秒幾百萬的速度拼接字符串(比如一個(gè)高流量的Web應(yīng)用),性能提高幾個(gè)百分點(diǎn)也是值得的。

          用與不用

          String.Create 雖然有較好的性能,但一般只在性能要求較高場景下使用。一個(gè)良好的系統(tǒng)取決于很多指標(biāo),作為軟件工程師,我們不能只追求性能指標(biāo),而忽略了大局。一般來說,我認(rèn)為簡潔可維護(hù)的代碼應(yīng)該優(yōu)于夢幻般的性能。

          本文性能測試的有關(guān)代碼都放在了 GitHub:

          https://github.com/cmcquillan/StringCreateBenchmarks

          -

          精致碼農(nóng)

          帶你洞悉編程與架構(gòu)

          長按圖片識(shí)別二維碼關(guān)注,不要錯(cuò)過網(wǎng)海相遇的緣分


          瀏覽 36
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产成人无码在线高清播放 | 一级片在线免费观看 | 天天搞B网 | 伊人在线9999 | 日韩综合亚洲 |