dotnet 6 使用 string.Create 提升字元串創建和拼接性能

来源:https://www.cnblogs.com/lindexi/archive/2022/03/23/16046238.html
-Advertisement-
Play Games

對於語音識別,一般有實時語音識別和語音文件的識別處理等方式,如在會議、培訓等場景中,可以對錄製的文件進行文字的轉錄,對於轉錄文字的成功率來說,如果能夠轉換90%以上的正確語音內容,肯定能減輕很多相關語音文本編輯的繁瑣工作,而目前大多數語音轉錄的介面基本都能夠保證在這個成功率上,有些甚至超過98%以上... ...


本文告诉大家,在 dotnet 6 或更高版本的 dotnet 里,如何使用 string.Create 提升字符串创建和拼接的性能,减少拼接字符串时,需要额外申请的内存,从而减少内存回收压力

本文也是跟着 Stephen Toub 大佬学性能优化系列博客之一。这是 Stephen Toub 大佬在给 WPF 做的性能优化里面其中的一个小点。只是刚好这个优化点,是 Stephen Toub 大佬参与设计(预计是主导)和进行开发的。此优化点需要修改 Roslyn 内核,编写分析器,以及在 dotnet runtime 层进行支持才可以做到的优化。在过去完成了从 Roslyn 到分析器到 runtime 的支持之后,就到了应用框架层的支持了,这就是 Stephen Toub 大佬会在 WPF 仓库活跃的其中一个原因了

歪个楼,大家知道 dotnet 的各个层之间的关系吧。在 dotnet 里面,各个部分的角色是:

  • Roslyn: 编译器内核层
  • Runtime: 提供运行时的支持,广义的运行时,包括了执行引擎和基础库
  • WPF: 应用代码框架层

在 WPF 上方就是业务代码逻辑了

在 WPF 仓库里 Stephen Toub 大佬的改动代码可以从 Remove some unnecessary StringBuilders by stephentoub · Pull Request #6275 · dotnet/wpf 找到。这就是本文的例子代码了

在 dotnet 6 里面,新提供了 string.Create 方法的两个新重载方法,此两个重载方法签名分别如下

第一个重载方法:

public static string Create (IFormatProvider? provider, Span<char> initialBuffer, ref System.Runtime.CompilerServices.DefaultInterpolatedStringHandler handler);

以上的三个参数的说明如下:

  • provider: 一个提供区域性特定的格式设置信息的对象。
  • initialBuffer: 初始缓冲区,可用作格式设置操作的一部分的临时空间。 此缓冲区的内容可能会被覆盖。
  • handler: 通过引用传递的内插字符串。

第二个重载方法:

public static string Create (IFormatProvider? provider, ref System.Runtime.CompilerServices.DefaultInterpolatedStringHandler handler);

第二个重载方法只是将第一个方法的 Span<char> initialBuffer 干掉而已

本文核心和大家聊的就是第一个重载方法

为什么这两个方法只有在 dotnet 6 或更高版本才能使用?为什么低版本的不能使用?如本文开始所说,这是因为这两个方法需要从 Roslyn 改到 dotnet runtime 才能支持。那为什么需要改那么多才能支持呢?因为这两个方法别看起来简单,实际上用到了 Roslyn 的黑科技。当然了用上了 Roslyn 黑科技,就可以让你告诉老师们,你的知识又需要更新了

敲黑板,第一个知识更新点是内插字符串。有趣的是在 C# 6.0 提出的内插字符串的知识点,刚好在 dotnet 6 的时候进行更新。别混了哦,这里说的 C# 版本和 dotnet 的版本可是两回事哦。如以下的内插字符串,你猜猜这是什么

  $"lindexi is {doubi}"

在 dotnet 6 或更低的版本,你可以听从老师的话,说这是一个 string.Format 的语法优化而已,和以下的代码是完全等价的

 string.Format("lindexi is {0}", doubi);

当然了,这么简单的代码我可没有开IDE来写,如果语法写错了,还请大家忽略吧

但是在 dotnet 6 或更高的版本,这些知识就需要更新了哈。看到了内插字符串,可不一定是 string.Format 的语法优化,还可以是 System.Runtime.CompilerServices.DefaultInterpolatedStringHandler 类型的创建哦

官方有一篇博客,嗯,又是 Stephen Toub 大佬写的,来告诉大家,这个 DefaultInterpolatedStringHandler 类型的来源以及是如何工作的,详细请看 String Interpolation in C# 10 and .NET 6 - .NET Blog

简单来说就是使用内插字符串时,在 C# 10 和 dotnet 6 之前,将会额外创建一些对象,这些对象将会造成内存回收的压力。嗯,只是造成压力而已,不用担心,咱996都不怕。一点压力,没多少

如下面的代码,就是一个标准的内插字符串的用法

public static string FormatVersion(int major, int minor, int build, int revision) =>
    $"{major}.{minor}.{build}.{revision}";

在 C# 10 和 dotnet 6 之前,经过了构建的代码,将会拆分以上的语法优化大概为如下代码

public static string FormatVersion(int major, int minor, int build, int revision)
{
    var array = new object[4];
    array[0] = major;
    array[1] = minor;
    array[2] = build;
    array[3] = revision;
    return string.Format("{0}.{1}.{2}.{3}", array);
}

可以看到,其实这将需要额外多创建了一个 object 数组,同时在 string.Format 方法里面,还有很多其他的损耗

在 C# 10 和 dotnet 6 同时满足时,将在构建时,修改为如下结果等价的代码

public static string FormatVersion(int major, int minor, int build, int revision)
{
    var handler = new DefaultInterpolatedStringHandler(literalLength: 3, formattedCount: 4);
    handler.AppendFormatted(major);
    handler.AppendLiteral(".");
    handler.AppendFormatted(minor);
    handler.AppendLiteral(".");
    handler.AppendFormatted(build);
    handler.AppendLiteral(".");
    handler.AppendFormatted(revision);
    return handler.ToStringAndClear();
}

这个 DefaultInterpolatedStringHandler 是一个结构体对象。根据一个完全不对的知识,结构体是在栈上分配的,以上的代码将除了返回的字符串之外,不会需要额外的内存申请。虽然知识完全是错的,不过结果是对的哈。辟谣时间:结构体可以是在栈上分配,也可以是在堆上分配的。对于大部分的局部变量创建的结构体来说,此结构体就是在栈上分配的。至少,以上的代码就是在栈上分配了一个 DefaultInterpolatedStringHandler 结构体对象。由于栈的内存是固定且明确的,可以认为用到 栈 上的内存就不属于额外申请的内存,再因为栈的空间,将会在方法执行完成之后,自动栈回收,也就没有了内存回收压力。相当于此方法执行完成之后,此方法内用到的栈空间,都会抹掉,自然就不需要算内存回收了。当然了,本文的主角可不是栈内存,细聊下去,我预计还能吹很久。还是回到本文主题吧,大家就只需要记得,以上的代码超级超级省内存分配资源

以上的代码,分配的对象,只有一个字符串,没错,就是返回值的字符串

也就是说在 dotnet 6 以及更高的版本,可以让构建时,将 $ 内插字符串,构建成为 DefaultInterpolatedStringHandler 结构体对象,而不需要走 string.Format 方法的逻辑。这是一个很大的优势。可以让内插的字符串,不需要创建额外的数组存放参数列表,不需要在 string.Format 方法里面解析字符串

但大家又有另外一个疑惑,在使用 DefaultInterpolatedStringHandler 的 ToStringAndClear 方法的时候,难道底层不需要一个缓存使用的数组么?实际上还是有用到的,要不然,还要本文的主角做啥。在 ToStringAndClear 方法里面,实际上是需要用到一个数组进行缓存的,不然的话,代码还是有点坑。用到了数组缓存,为什么在本文上面还说没有额外的内存分配?别忘了数组池哦

默认在 DefaultInterpolatedStringHandler 里,将申请 ArrayPool<char>.Shared 一个数组池的数组空间来作为缓存。在大部分情况下,可以认为这是一个无伤的过程。然而数组池也不见得每次都有那么空闲。而且,借和还是需要算利息的哦

为了减少利息,减少 CPU 计算的耗时,就到了本文的主角,也就是 string.Create 新加入的重载方法出场的时候

如上文,调用 DefaultInterpolatedStringHandler 里,也需要一个缓存数组。那这个数组,如果也是从栈上过来的呢,是不是就更省一些了?没错。那如何将从栈上的数组给到 DefaultInterpolatedStringHandler 结构体,这就需要用到本文的主角了

先通过 stackalloc 申请一定的数组空间,再将数组空间给到 DefaultInterpolatedStringHandler 结构体,即可实现几乎所有内存的分配逻辑都是在栈上分配的。将随着方法的结束,自动清理垃圾

用法如下:

public static string FormatVersion(int major, int minor, int build, int revision) =>
    string.Create(null, stackalloc char[64], $"{major}.{minor}.{build}.{revision}");

以上的用法属于高级用法部分。在构建的时候,将自动拆分内插字符串为 DefaultInterpolatedStringHandler 结构体,提示将传入的 stackalloc char[64] 作为缓冲的数组传入使用。如此即可实现,除了返回值的字符串,就不需要从堆上额外申请空间。而且在传入的缓冲数组够用的情况下,也不用数组池里申请缓存数组空间,减少了一借一还的时间损耗,从而达到极高的性能

但,这是高级的用法,还是要需要小心的事项的。第一个就是,咱使用 stackalloc 是在栈上分配内存空间,分配的大小可要小心哦,如果将栈上的空间玩爆了,那就只能再见了。默认分配 512 一下,可以认为是安全的。不过,分配越小越好,刚刚好够用就好哦。千万别多打了几个 0 哦

第二个就是如果传入的缓存空间不足了,那依然会需要从数组池里申请内存空间。而不是进行栈空间越界炸掉你的应用。更进一步的说明,有时,咱是无法预估此内插字符串所使用的缓存大小需要多大的。如果真的难以预估的话,而且实际业务预期也会超过预估的大小,那么使用以上的方法,相当于白申请一段栈空间,不如不要

如果实际所需要的字符串拼接的缓存空间比传入的 stackalloc 的空间还要更大。那么在 runtime 底层,将抛弃传入的数组空间,改用从数组池申请的空间。因此,传入 stackalloc 申请的预估的固定大小的数组,在开发中是安全的。预估的固定大小,如果小了,是不会有逻辑上的问题的

例如使用的内插字符串的拼接需要 5000 的 char 数组空间大小作为缓存空间,然而传入的 stackalloc 申请的空间是 stackalloc char[64] 那显然不够用。这是没有问题的,在底层将重新和数组池借足够的空间。不会强行在你的栈上分配空间越界的

对于字符串来说,还有一个很重要的就是语言文化。例如对于日期来说,美国和中国的文化的日期的字符串表示是不相同的。自然在格式化输出字符串时,最好是带上日期。咱上面的例子只是为了简单,将 IFormatProvider 传入空值而已。实际上可以传入符合你预期的格式化方法,例如无视语言文化的格式化

public static string FormatVersion(int major, int minor, int build, int revision) =>
    string.Create(CultureInfo.InvariantCulture, stackalloc char[64], $"{major}.{minor}.{build}.{revision}");

以上的 CultureInfo.InvariantCulture 将对后续的内插字符串进行对应的格式化,如此可以解决很多语言文化的坑

对于咱的应用代码,如果需要给用户展示的,最好是根据当地的语言文化进行展示。而对于咱应用里层的计算逻辑,最好是做语言文化无关的。如此才能保持逻辑的符合预期,毕竟诡异的语言格式化还是很多的,采用语言文化无关,可以保持咱应用内计算逻辑符合预期

在 dotnet 6 下,如有使用 string.Create 这两个新的重载方法进行拼接字符串,性能上是比 StringBuilder 更高的

如以下的代码,是采用 StringBuilder 进行拼接创建字符串

StringBuilder stringBuilder = new StringBuilder(64);
stringBuilder.Append(cr.TopLeft.ToString(cultureInfo));
stringBuilder.Append(listSeparator);
stringBuilder.Append(cr.TopRight.ToString(cultureInfo));
stringBuilder.Append(listSeparator);
stringBuilder.Append(cr.BottomRight.ToString(cultureInfo));
stringBuilder.Append(listSeparator);
stringBuilder.Append(cr.BottomLeft.ToString(cultureInfo));
return sb.ToString();

以上代码是需要多在栈上分配一个 StringBuilder 对象的,而且还需要为此对象申请至少一个 64 长度的数组。而在优化之后,采用 string.Create 的方式,如以下代码则几乎除了返回值的字符串之外,就不需要再申请任何的空间

return string.Create(cultureInfo, stackalloc char[128], $"{cr.TopLeft}{listSeparator}{cr.TopRight}{listSeparator}{cr.BottomRight}{listSeparator}{cr.BottomLeft}");

实际上,也不是所有在使用字符串拼接的地方,都使用 StringBuilder 都能提升性能。如果字符串拼接只是很简单的两个字符串相加,那么大多数的时候,使用两个字符串相加的性能是大于采用 StringBuilder 拼接的

这就是本文和大家聊的性能优化点,采用 C# 10 和 dotnet 6 配合的字符串内插优化方法

博客园博客只做备份,博客发布就不再更新,如果想看最新博客,请到 https://blog.lindexi.com/

知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名[林德熙](http://blog.csdn.net/lindexi_gd)(包含链接:http://blog.csdn.net/lindexi_gd ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我[联系](mailto:[email protected])。
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、安裝 pip install Selenium 二、初始化瀏覽器 Chrome 是初始化谷歌瀏覽器 Firefox 是初始化火狐瀏覽器 Edge 是初始化IE瀏覽器 PhantomJS 是一個無界面瀏覽器。 from selenium import webdriver driver = webd ...
  • 需求:通過鍵盤錄入的年份獲取該年的二月共有多少天? 分析: 1.使用Scanner類獲取輸入的年份 2.設置輸入的值的日曆的年月日 月份因為是從零開始的需要加一,月份設置為2,也就是三月 天數設置為1,那麼再往前推一天就是二月份的最後一天也就是我們要的天數 3.獲取這一天輸出 public clas ...
  • 1.前言 他總是微信撤回,是有什麼東西不能給我看嗎?我得想一個法子治治他,看看是不是在外面有人了,哈哈哈… 2 有微信聯想起的哲思 2.1 哲學思維開始冒頭 在這個信息量大增的信息時代,每天腦袋要處理很大的信息量,還是需要瞭解一點底層邏輯。 你看中國的名家,它甚至把的問題基本都提到了,也就是中國在前 ...
  • 很多小伙伴說自己的公司在監控自己有沒有摸魚、偷懶。有時候想偷偷懶都會被髮現,今天就帶大家來解開這神秘的面紗。搞懂了這個,估計你就知道怎麼去摸魚了。 監控鍵盤 如果公司偷偷在我們的電腦上運行了一個後臺進程,來監控我們的鍵盤事件,最簡單的 python 寫法大致是這樣的: from pynput imp ...
  • 方式一: 創建一個新的集合進行數據重覆元素的去除 //boolean contains(Object o):判斷集合中是否包含指定的元素 分析: * A:創建集合對象 * B:添加多個字元串元素(包含內容相同的) * C:創建新集合 * D:遍歷舊集合,獲取得到每一個元素 * E:拿這個元素到新集合 ...
  • 快下班了,今天給大家分享一下,平常我都是怎麼發送電子郵件,這個方法能夠幫助大家提高工作效率、,擺脫繁重的重覆性工作。一般我都會借用Python來實現自動化郵件發送,相信你用過這個方法之後就會愛上它。 Python有兩個內置庫:smtplib和email,能夠實現郵件功能,smtplib庫負責發送郵件 ...
  • 關係型的結構化存儲存在一定的弊端,因為它需要預先定義好所有的列以及列對應的類型。但是業務在發展過程中,或許需要擴展單個列的描述功能,這時,如果能用好 JSON 數據類型,那就能打通關係型和非關係型數據的存儲之間的界限,為業務提供更好的架構選擇。 當然,很多同學在用 JSON 數據類型時會遇到各種各樣 ...
  • 網關中間件-Nginx(一) 第一部分我們主要介紹如下幾點: 1.nginx的基本概念 2.nginx結合業務場景實現負載均衡 3.常見問題的舉例 這一部分主要介紹Nginx中限流,緩存,動靜分離,以及Nginx的集群搭建,如果涉及舉例的話,依然使用上一部分的業務 一、限流 1.為什麼要限流? 對於 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...