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

作者:Casey McQuillan

译者:精致码农

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

说明:原文比较长,翻译时精简了很多内容,对于不重要的细枝末节只用了一句话概括,但不并影响阅读。

你还记得上一次一个无足轻重的细节点燃你思考火花的时刻吗?作为一个软件工程师,我习惯于专注于一个从未见过的微小细节。那一时刻,我大脑的齿轮会开始转动,我喜欢这样的时刻。

最近,我在逛 Twitter 时发生了一件事。我看到了 David Fowler 和 Damian Edwards 之间的这段交流,他们讨论了 .NET 的 Span<T> API。我以前使用过 Span<T> API,但我在推文中发现了一些不一样的新东西。

上面使用的 String.Create 方法是我从未见过的用法。我决定要揭开 String.Create 的神秘面纱。此时我在问自己一个问题:

为什么用这个方法创建字符串而不用其它的?

我便开始探索,它把我带到了一些有趣的地方,我想和你分享。在本文中,我们将深入探讨几个话题:

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

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

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

为了书写方便,我将用下面的词来指代 .NET 中的几个 API:

  • Create — 指代 String.Create()

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

  • StringBuilder — 指代StringBuilder构造字符串或使用其流式 API。

它是如何工作的

.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 belowdestination: ref result._firstChar,source: ref MemoryMarshal.GetArrayDataReference(value));return result;
}

这里的两个重要步骤是:

  • 根据数组长度使用 FastAllocateString 分配内存。FastAllocateString 是在 .NET Runtime 中实现的,它几乎是所有字符串分配内存的基础。

  • 调用 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;
}

步骤相似,但有一个关键的区别:

  1. FastAllocateString 根据 length 参数分配内存。

  2. 将新分配的 string 转换为 Span<char>

  3. 调用 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 sortingprivate 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 restartsprivate 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(character_index * 5)位,获取最右边的5位(shifted_value & 31),并根据预先确定的字符表(encode32Chars)选择一个字符,从后向前填充到buffer

译者注:64位的整数,每5位一划分可划为13段,前十二段为5位,最后一段为4位。之所以5位一划分是因为 2^5-1=31,可以确保字符表(encode32Chars)的每个字符都可以被索引到(encode32Chars[31] 为 V)。若以4位划分,则最大的索引是15,字符表就有一半的字符轮空。

我们用 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://www.mzph.cn/news/306579.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

算法设计与分析——回溯法——n皇后问题

一、什么是N皇后问题&#xff1f; 在nn格的棋盘上放置彼此不受攻击的n个皇后。按照国际象棋的规则&#xff0c;皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。n后问题等价于再nn的棋盘上放置n个皇后&#xff0c;任何2个皇后不妨在同一行或同一列或同一斜线上。 问题…

全局程序集缓存gac中安装程序集_我就不信2W字把源码拆的这么碎,你还不明白mybatis缓存...

前言不知道大家看到这张图感觉怎么样&#xff0c;不是难&#xff0c;一共也没有几个组件&#xff0c;但是真的让我想当头疼&#xff0c;因为在面试的时候&#xff0c;就这张图&#xff0c;对&#xff0c;你没看错&#xff0c;就这几个组件&#xff0c;那是让我相当难受啊MyBati…

GraphQL:和EntityFramework更配哦

GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述&#xff0c;使得客户端能够准确地获得它需要的数据&#xff0c;而且没有任何冗余&#xff0c;也让 API 更容易地随着时间推移而演进&#xff0c…

算法设计与分析——回溯法——符号三角形问题

#include<iostream> using namespace std;class Triangle{public:void Backtrack(int t);int n;//第一行的符号个数 int half;//n*(n1)/4 int count;//当前—的个数 int **p;//符号三角形矩阵 long sum; //已找到的符号三角形数 };void Triangle::Backtrack(int t) {if(…

mysql 默认事务隔离级别_MySQL 事务隔离级别详解

个人公众号『码农札记』&#xff0c;欢迎关注&#xff0c;查看更多精彩文章。 简介&#xff1a; MySQL的事务隔离级别一共有四个&#xff0c;分别是读未提交、读已提交、可重复读以及可串行化。四个特性ACID原子性 &#xff08;Atomicity&#xff09;事务开始后所有操作&#x…

如何在 Asp.Net Core 中对请求进行限流

译文链接&#xff1a;https://www.infoworld.com/article/3442946/how-to-implement-rate-limiting-in-aspnet-core.html在应用程序开发时&#xff0c;或许你有这样的想法&#xff0c;控制用户的请求频率来防止一些用户的恶意攻击&#xff0c;具体的说就是&#xff1a;为了预防…

算法设计与分析——动态规划——石子合并问题

1.石子合并问题 在一个圆形操场的四周摆放着n堆石子。现要将石子有序地合并成一堆。规定每次只能选相邻的2堆石子合并成新的一堆&#xff0c;并将新的一堆石子数记为该次合并的得分。设计一个算法&#xff0c;计算出将n堆石子合并成一堆的最小得分和最大得分。 #include<st…

软件层面可以做到重启本地串口吗_手机关机还是重启好?get这几招,手机更流畅...

遇到手机卡顿&#xff0c;很多人都会不自觉的选择重启手机&#xff0c;还是不行&#xff0c;就关机等一会儿再开机&#xff0c;这样几次下来手机真的就顺畅多了。那么关机和重启到底有什么区别&#xff1f;还有哪些方法可以保持手机流畅&#xff1f;跟着小编来了解一下&#xf…

在传统行业做数字化转型之团队篇

【数字化转型】| 作者 / Edison Zhou这是EdisonTalk的第309篇原创内容在过去的两年时间里&#xff0c;我加入了一家传统行业的企业参与其数字化转型的过程&#xff0c;现在我将我的经历分享出来&#xff0c;本文是第四部分—团队篇&#xff0c;主要会介绍一下我所在的经济适用型…

回顾 | 进击吧! Blazor!系列

Blazor 是一个 Web UI 框架&#xff0c;可通过 WebAssembly 在任意浏览器中运行 .Net 。Blazor 旨在简化快速的单页面 .Net 浏览器应用的构建过程&#xff0c;它虽然使用了诸如 CSS 和 HTML 之类的 Web 技术&#xff0c;但它使用 C&#xff03;语言和 Razor 语法代替 JavaScrip…

算法设计与分析——回溯法——旅行售货员问题

#include<iostream> #include<bits/stdc.h> using namespace std; const int noEdge65535; class Traveling {public:void BackTrack(int i);int n; //图G的顶点数 int *x; //当前的解 int *bestx; // 当前的最优解 int **a; // 图G的临界矩阵 int cc; //…

sql不等于0怎么表示_数组真的只能从0开始吗?python表示不同意

背景早在linux操作系统诞生开始&#xff0c;c语言作为linux系统的编程语言主力&#xff0c;它为后续的其他高级编程语言(如c、java)提供了很多语言级的语义和协议规范。数组做为linux操作系统最基本的数据结构之一&#xff0c;便是其中的一项语言级高级特性&#xff0c;深入理解…

ASP.NET Core 5.0新增功能摘要

.NET5.0发布了大半个月&#xff0c;从.NET Core3.1的平滑迁移体验令人心旷神怡&#xff0c;改个targetframework就完成迁移&#xff0c;不要太轻松&#xff01;然而&#xff0c;ASP.NET Core5.0也有很多有意思的改变&#xff0c;这里为大家摘录一下&#xff01;内置swagger在 A…

算法设计与分析——回溯法——圆排列问题

#include<iostream> #include<math.h> using namespace std; class Circle {public:float Center(int t);void Compute(void );void BackTrack(int t);float min; //当前最优值 float *x; //当前圆排列圆心横坐标 float *r; //当前圆排列 float *result; /…

数字图像处理王伟强_深度学习主导下,还有必要学数字图像处理?

图像处理技术作为计算机视觉的基础&#xff0c;通过计算机对图像进行去除噪声增强、复原、分割、提取特征等处理的方法和技术。多用于目标检测与目标识别等领域。>>数字图像处理、CV、CG与AI四者的关系图<<随着计算机视觉的迅速发展&#xff0c;基于深度学习的研究…

算法设计与分析——贪心算法——单个出水口打水问题

1.打水问题&#xff1a;有n个人去水房排队打水&#xff0c;只有一个出水口&#xff0c;且出水口流速恒定。每个人因为盛水的工具不一&#xff0c;打水所需的时间也不一致&#xff0c;设第i个人所需要的打水时间为ti&#xff1b;问如何安排打水顺序使得所有人的平均等待时间最短…

在传统行业做数字化转型之最终篇

【数字化转型】| 作者 / Edison Zhou这是EdisonTalk的第310篇原创内容在过去的两年时间里&#xff0c;我加入了一家传统行业的企业参与其数字化转型的过程&#xff0c;现在我将我的经历分享出来&#xff0c;本文是最终篇&#xff0c;主要会回答中途一些网友的问题以及推荐一些参…

算法设计与分析——贪心算法——汽车加油问题

汽车加油问题&#xff1a;一辆汽车加满油后可行驶n公里&#xff0c;旅途中有若干个加油站&#xff0c;两加油站间距离不超n 公里&#xff0c;起点离第一个加油站距离及最后一个加油站离终点距离也不超过n公里。算法给出应在哪些加油站停靠加油&#xff0c;使沿途加油次数最少。…

.NET Conf China 2020志愿者招募火热开启!还有神秘惊喜等你来领…

距.NET Conf China 2020 2020 中国 .NET 开发者大会开幕还有14天作为.NET的组织者之一我们已经为这个活动准备铺垫了很久但为了保证活动方方面面的质量为了给现场&线下的小伙伴们带来更好的体验现在&#xff01;我们十分需要你们的帮助&#xff01;为更好地做好大会服务工作…

golang 修改nginx配置文件_「系统架构」Nginx调优,不可错过的几点

通常&#xff0c;Nginx在安装完后&#xff0c;不用更改任何配置信息&#xff0c;我们就可以直接运行它。但是&#xff0c;这显然不能满足我们的生产要求。在生产中&#xff0c;要让Nginx能正常高效地运行我们的应用&#xff0c;我们通常需要对Nginx的配置文件进行一些参数上的设…