如何编写高性能的C#代码(四)字符串的另类骚操作

原文来自互联网,由长沙DotNET技术社区编译。如译文侵犯您的署名权或版权,请联系小编,小编将在24小时内删除。

作者介绍:

史蒂夫·戈登(Steve Gordon)是Microsoft MVP,Pluralsight的作者,布莱顿(英国西南部城市)的高级开发人员和社区负责人。

在本文中,我将继续有关编写高性能C#和.NET代码的系列文章[1]。这次,我将重点介绍String类型–

String.Create

-一种可用的新方法。.NET Core 2.1中首次引入该方法,目前计划将该方法发布后作为.NET Standard 2.1的一部分包含在内。

STRING.CREATE做什么?

String.Create方法支持有效创建需要在运行时构建或计算的字符串。在我进一步讨论之前,让我们花一点时间来介绍有关字符串的一些事实。

•在.NET中,字符串是一种流行的类型,用于表示文本数据。•字符串是引用类型,它们的数据存储在托管堆中。•根据设计,字符串是不可变的,这意味着一旦创建,就无法修改其数据。

从高性能的角度来看,这些事实的结合会导致字符串出现问题。从高层次上讲,我们编写高性能代码的目标通常是减少运行该代码的执行时间,并删除内存分配。

由于其不变性,对字符串进行操作通常会导致分配过多。如果要提取字符串的一部分,则会导致创建新字符串以及在旧字符串和新字符串占用的内存之间复制字符串数据。如果我们想将字符串转换为大写,这也会导致在堆上分配新的字符串。

如果我们想使用仅在运行时可用的数据以编程方式创建字符串,则也会出现问题。串联字符串也将导致分配和复制。对于长字符串,尤其是由许多组成部分组成的字符串,此成本可能会显着增加。

这并不意味着在适当的时候不应该使用字符串,但是在编写高度优化的代码时就成为一个问题。

在运行时构造字符串时使用的标准解决方案是使用StringBuilder,该StringBuilder使用附加了字符的内部缓冲区。当您在StringBuilder上调用build方法时,这将导致最终的字符串分配。

当串联多个元素时,StringBuilder通常比普通串联更有效(始终使用基准测试来验证您的方案)。

StringBuilder仍然需要字符的中间缓冲区,因此在那里要分配堆,并在从缓冲区构建字符串时再加上一个副本。

StringBuilder本身是一个类,因此使用其中的分配。

在ASP.NET Core团队已经在热路径上通过池化和共享StringBuilder的实例来解决这个分配成本问题,这是有意义的,例如在中间件等地方。

什么时候使用STRING.CREATE?

在日常开发过程中,不需要String.Create。它有一个特定的目的,即以高度优化的方式从某些现有数据实用地创建字符串,或者可能仅通过算法来创建字符串。

在这种情况下,主要的优化是帮助我们避免不必要的分配和数据复制。我们将在几分钟后看一个可行的示例,但在此之前,让我们考虑一些更通用的用例。

在用于ASP.NET Core的Kestrel Web服务器中,每个请求都会创建唯一的ID。在这种情况下,要求构建一个长度和格式已知的字符串,该字符串将唯一地标识请求。由于此操作每秒可能完成数千次,因此使其性能良好至关重要。String.Create允许在这种情况下有效地构造字符串。

STRING.CREATE如何工作?

String.Creates提供了一个非常短的窗口,允许我们从本质上打破字符串的不变性规则。这听起来有些吓人,但还不如我讲的那么糟糕。可能发生数据突变的窗口仅在返回对字符串的第一个引用之前。在此简短窗口之后,将无法修改现有字符串的数据。

在内部,String.Create在堆上分配适当的内存部分,以包含字符串数据的char数组。为此,该方法将字符串所需的长度作为第一个参数。这是一个重要的限制,您必须知道或能够预先计算出要创建的字符串的确切字符长度。

这是Create方法的签名:

 public static string Create<TState> (int length, TState state, System.Buffers.SpanAction<char,TState> action);

该方法采用第二个参数,这是构造字符串所需的一般状态。一会儿我们来专门介绍这个状态。 

最后,create方法接受一个委托,该委托应在分配的堆内存上进行操作以设置最终的字符串数据。在这种情况下,参数是SpanAction,它在System.Buffers中定义。

由于Span 类型不能用作泛型类型参数,因此不能使用标准的Action委托。相反,SpanAction支持采用将用作内部Span 的类型的类型。在这种情况下,我们正在处理字符。

SpanAction委托就是魔力所在。在分配了字符串所需的char []内存之后,然后可以使用我们传递的委托来填充该数组中的字符。委托完成后,将返回内部使用该数组的字符串,并已正确设置其值。

让我们考虑一下不使用此方法即可构建字符串的最低分配方式之一。我们可能会使用临时char数组作为缓冲区来构建字符串数据,然后将该数组传递给字符串的构造函数。这基本上就是StringBuilder为我们所做的。这种方法将导致两种分配,一种分配给缓冲区,另一种分配给字符串。所涉及的阵列之间也会发生一些内存复制。

可能是这样的:

 using System;namespace StringCreateSample{class Program{private const char spaceSeparator = ' '; // space separator characterstatic void Main(){// Our source data (state) which will be composed into the final string.var context = new ContextData{FirstString = "Hello",SecondString = ".NET",ThirdString = "friends."};var length = context.FirstString.Length + 1 + context.SecondString.Length + 1 + context.ThirdString.Length;var buffer = new char[length]; // allocationvar position = 0;for (var i = 0; i < context.FirstString.Length; i++){buffer[i] = context.FirstString[i];position++;}buffer[position++] = spaceSeparator;for (var i = 0; i < context.SecondString.Length; i++){buffer[position++] = context.SecondString[i];}buffer[position++] = spaceSeparator;for (var i = 0; i < context.ThirdString.Length; i++){buffer[position++] = context.ThirdString[i];}Console.WriteLine(new string(buffer)); // string allocation + copy}}internal struct ContextData{public string FirstString { get; set; }public string SecondString { get; set; }public string ThirdString { get; set; }}}

另一个选择是使用不安全的代码,或者在.NET Core 2.1及更高版本中,我们可以使用Span 支持来安全地使用小的堆栈分配缓冲区,而不是堆分配的数组。

只要缓冲区的大小不是太大,这将是一个不错的选择,并且我们将仅针对最后一个字符串进行一次堆分配。

但是,将需要一个副本来将数据从堆栈内存中移到字符串堆内存中。这具有很小的执行时间成本。 我们的示例Main方法中为实现此目的所做的更改如下所示:

static void Main(){// Our source data (state) which will be composed into the final string.var context = new ContextData{FirstString = "Hello",SecondString = ".NET",ThirdString = "friends."};var length = context.FirstString.Length + 1 + context.SecondString.Length + 1 + context.ThirdString.Length;// In real-world code we should ensure we don't try to allocate too much on the stack!// Ignoring that risk for this example.Span<char> buffer = stackalloc char[length]; // DOES NOT heap allocatevar position = 0;for (var i = 0; i < context.FirstString.Length; i++){buffer[i] = context.FirstString[i];position++;}buffer[position++] = spaceSeparator;for (var i = 0; i < context.SecondString.Length; i++){buffer[position++] = context.SecondString[i];}buffer[position++] = spaceSeparator;for (var i = 0; i < context.ThirdString.Length; i++){buffer[position++] = context.ThirdString[i];}Console.WriteLine(new string(buffer)); // string allocation + copy from stack memory}

回到String.Create,我们现在可以了解这如何为我们提供最佳性能。通过避免对字符进行预缓冲(即使该字符在堆栈中),这意味着用于构造字符串的逻辑将直接作用于该字符串将引用的存储器的最终区域。

正确完成后,我们可以以编程方式构建字符串,而无需中间分配,并且具有很高的性能。 在SpanAction中,我们可以通过字符串占用的内存访问Span 。我们可以通过Span修改该内存,将其切成适当的位置并将字符写入基础数组。

传入的状态将允许我们使用现有数据来构建字符串。您可能已经在想一个重要的问题。为什么将状态直接传递给Create方法?为什么我们不能仅仅从委托代码中引用我们需要的数据?

原因是,如果我们捕获变量,则后一种方法将导致关闭。编译器将必须生成一个类来处理此问题,这是我们在此处要避免的堆分配。

另外,这里的关闭将防止委托的缓存,这本身就是我们无法承受的性能损失。相反,Create方法接受状态作为参数,以避免委托形成闭包。

解释起来有点复杂,但是这里的要点是确保状态中需要包含为创建字符串而需要访问的所有对象。

如果要传递多个对象,建议的模式是使用ValueTuple[2]。由于这是一个结构,因此它不会分配任何内容,一旦进入委托,您就可以对其进行解构以获取组成部分。

使用STRING.CREATE的快速示例

在深入研究真实示例之前,让我们快速看一下如何使用String.Create。

using System;namespace StringCreateSample{class Program{private const char spaceSeparator = ' '; // space separator characterstatic void Main(){// Our source data (state) which will be composed into the final string.var context = new ContextData{FirstString = "Hello",SecondString = ".NET",ThirdString = "friends."};var length = context.FirstString.Length + 1 + context.SecondString.Length + 1 + context.ThirdString.Length;var myString = string.Create(length, context, (chars, state) =>{// NOTE: We don't access the context variable in this delegate since // it would cause a closure and allocation.// Instead we access the state parameter.// will track our position within the string data we are populatingvar position = 0;// copy the first string data to index 0 of the Span<char>state.FirstString.AsSpan().CopyTo(chars);position += state.FirstString.Length; // update the position// add a space in the current position and increement position by 1chars[position++] = spaceSeparator;// copy the second string data to a slice at current positionstate.SecondString.AsSpan().CopyTo(chars.Slice(position)); position += state.SecondString.Length; // update the position// add a space in the current position and increement position by 1chars[position++] = spaceSeparator;// copy the third string data to a slice at current positionstate.ThirdString.AsSpan().CopyTo(chars.Slice(position)); });Console.WriteLine(myString);}}internal struct ContextData{public string FirstString { get; set; }public string SecondString { get; set; }public string ThirdString { get; set; }}}

这段代码中的注释逐步说明了正在发生的事情。 从上面我们可以看出一个结论,我们有一个ContextData对象,其中包含三个我们要用来构建最终字符串的字符串。

首先,我们计算最终字符串所需的长度,该长度包括组成部分及其之间的间距。

我们将长度传递给string.Create并将上下文作为状态参数传递。

最后,我们定义SpanAction委托的代码,该代码切片为基础Span ,以将组件部分复制到最终字符串中的正确位置。所有这些都是通过为字符串所需的内存分配单个堆来实现的。

如何使用STRING.CREATE –一个真实的例子

现在,让我们根据我遇到的实际情况看一个可行的示例。请注意,这仍然是演示代码。它基于我的生产要求,但是我已经对其进行了简化,以便我们可以专注于特定技术。我有把握地确定它可以进一步优化!

在我的演讲“Turbocharged: Writing High-Performance C# and .NET Code”中,我讨论了一个服务示例,其中,从AWS SQS队列中读取消息后,我需要将消息正文存储到S3存储桶中。

将内容存储到S3中时,我们必须为对象提供唯一的键。因此,此服务必须计算在上载对象时传递到AWS开发工具包的密钥。在我们的案例中,这种情况每天发生1800万次,因此即使是很小的性能提升也会对规模产生重大影响。

密钥由传入消息中的八个元素组成。最终键中仅允许使用小写字母,数字和下划线,并且任何空格都应转换为下划线。构造字符串的第一种方法是使用数组固定组成部分,然后将各个片段连接在一起以形成最终字符串。我不会在这篇文章中显示所有代码,但是您可以在我的GitHub repo中[3]查看一个示例[4]

第二次迭代使用堆栈分配的字符数组作为缓冲区,以形成字符串的最终数据。通过在该内存上使用Span ,我便能够将各种元素复制到堆栈分配的缓冲区中。在Span 上调用ToString导致创建了对象键的最终字符串。再次,我不会在这里显示该代码,因为它很长。如果您想签出,也可以在我的仓库中[5]找到。

在最后的迭代中,我利用了String.Create,这意味着我可以避免将内存从堆栈分配的缓冲区复制到字符串的堆内存中。如果您想浏览该代码,也可以在我的GitHub repo中找到[6]

请记住,这些样本尚未完全优化,其设计目的是演示某些特定技术而非完整的优化。在我的案例中,String.Create在运行的基准测试中仅稍快一些。将来,我将对此进行更深入的探讨。这是我比较这两种方法的基准结果。

图片

在大多数情况下,String.Create方法的速度要快几纳秒,但是在某些基准测试运行中,它的速度要慢几纳秒。潜在地,我可以对转换逻辑进行一些进一步的优化,从而可以解决这一问题。从逻辑上讲,将数据从堆栈内存复制到字符串堆内存所需的工作较少,应该会更有效率,但是对于您的实际情况而言,它始终值得测试。

为了对此进行研究,我对纯String.Create和stackalloc创建进行了一些基准测试。对于较短的字符串,stackalloc似乎只快一点。这是一个基准测试,在此基准下,我使用两种方法将10个字符的短字符串组合在一起。在这种情况下,每个测试中组合的字符串数中的计数。只有五个项目,根本没有太多。到组合100个字符串时,使用String.Create带来的性能提升更加明显。

图片

如果您对String.Create的另一个示例用例感兴趣,我已经在ASP.NET Core基于代码的地方确定了String.Create应该改善性能的地方。我提出了一个GitHub问题来[7]证明这一点,并希望参与创建PR以提出最终优化方案。

字符串创建最佳实践

这篇文章中已经有很多信息可以解释一个方法。最后,让我们回顾最重要的几点。

•String.Create提供了一种高性能,低分配的方法来以编程方式创建字符串。•与所有性能优化一样,对原始解决方案进行基准测试,并确保所做的更改具有积极作用。•避免闭包,并确保不要在SpanAction委托中捕获外部变量。•使用ValueTuples可以为状态传递多个对象。

STRING.CREATE的局限性

与您可能熟悉的其他一些创建新字符串的方法相比,使用String.Create涉及更多。我不建议在每个地方都使用此功能,但是在性能较高的应用程序中,它可能会提供一些有价值的收益。

您可能遇到的最大限制是,您必须事先知道(或能够计算)所需字符串的确切长度。您可能需要访问所有组成状态对象的长度,以便计算最终字符串的长度。在某些情况下,构建字符串时有很多条件逻辑,仅知道部件的长度可能还不够。

摘要

String.Create在高性能方案中很有用。一旦了解了它的运行规则,就可以直接使用它。因此,如果您正在优化应用程序中的热路径,那么它是一个值得记住的工具,并且在解析和生成字符串(通常是其主要功能的一部分)的应用程序中可能会获得重大收益。

谢谢阅读!如果您想了解有关高性能.NET和C#代码的更多信息,可以在此处[8]查看我的完整博客文章系列。

References

[1] 有关编写高性能C#和.NET代码的系列文章: https://www.stevejgordon.co.uk/writing-high-performance-csharp-and-dotnet-code
[2] ValueTuple: https://blogs.msdn.microsoft.com/mazhou/2017/05/26/c-7-series-part-1-value-tuples/
[3] 在我的GitHub repo中: https://github.com/stevejgordon/TurbochargedDemos/blob/master/src/1%20-%20ObjectKeyBuilderDemo/S3ObjectKeyGenerator.cs
[4] 一个示例: https://github.com/stevejgordon/TurbochargedDemos/blob/master/src/1%20-%20ObjectKeyBuilderDemo/S3ObjectKeyGenerator.cs
[5] 可以在我的仓库中: https://github.com/stevejgordon/TurbochargedDemos/blob/master/src/1%20-%20ObjectKeyBuilderDemo/S3ObjectKeyGeneratorNew.cs
[6] GitHub repo中找到: https://github.com/stevejgordon/TurbochargedDemos/blob/master/src/1%20-%20ObjectKeyBuilderDemo/S3ObjectKeyGeneratorNewV2.cs
[7] 提出了一个GitHub问题: https://github.com/aspnet/AspNetCore/issues/10290
[8] 在此处: https://www.stevejgordon.co.uk/writing-high-performance-csharp-and-dotnet-code

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/311110.shtml

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

相关文章

statusbar 尺寸 显示图标_移动端页面设计规范尺寸大起底 - 椰树飘香

移动端尺寸繁多&#xff0c;包括IOS和安卓&#xff0c;尺寸多达十余种&#xff0c;所以移动页面尺寸的适配一直是前端和设计的头疼。今天来总结一下当前市场上的一些移动端尺寸&#xff0c;方便设计师和前端去考虑适配。但是最好还是针对自己的产品做调查&#xff0c;根据数据去…

新建项目上传gitee(码云)教程

登录码云 新建一个仓库后&#xff0c;复制HTTPS地址&#xff1a; 本地项目操作 打开需要上传gitee的项目文件夹&#xff0c;并打开Git Bash窗口 本地仓库初始化&#xff1a; git init添加到暂存区 git add .提交到本地仓库 git commit -m "first commit"关联到…

OpenSilver: 通过WebAssembly 复活Silverlight

本月早些时候&#xff0c;Userware发布了第一个版本的OpenSilver&#xff0c;微软Silverlight 的开源重新实现。OpenSilver 通过WebAssembly 实现无需任何其他插件在 浏览器上运行。OpenSilver 的当前版本可作为"技术预览"版本提供&#xff0c;它涵盖了大约 60% 的原…

db2有主键时默认hash分区_MySQL分区表最佳实践

前言&#xff1a;分区是一种表的设计模式&#xff0c;通俗地讲表分区是将一大表&#xff0c;根据条件分割成若干个小表。但是对于应用程序来讲&#xff0c;分区的表和没有分区的表是一样的。换句话来讲&#xff0c;分区对于应用是透明的&#xff0c;只是数据库对于数据的重新整…

程序员过关斩将-- 喷一喷坑爹的面向UI编程

点击上方“蓝字”关注我们菜菜哥&#xff0c;求你个事呗&#xff1f;说来听听&#xff0c;假装你男朋友可不干不是哦&#xff0c;是正经事。前几天一个项目UI改了&#xff0c;好多人跟着加班修改&#xff0c;怎么样尽量避免这种情况呢&#xff1f;UI修改顶多和客户端开发人员关…

python二维散点分布图_深入理解皮尔逊相关系数amp;python代码

1.常见理解误区&#xff08;1&#xff09;计算出变量A和变量B的皮尔逊相关系数为0&#xff0c;不代表A和B之间没有相关性&#xff0c;只能说明A和B之间不存在线性相关关系。例&#xff1a;温度和冰淇淋销量之间的散点图像如下&#xff0c;可以发现大致成二次函数图像&#xff0…

hdu4911 Inversion-归并排序

解题思路&#xff1a; 如果原序列的逆序对数大于交换次数&#xff0c;那么最少的逆序对数量就是原序列逆序对-交换次数。 如果原序列的逆序对数小于等于交换次数&#xff0c;那么最少的逆序对数量为0&#xff0c;因为交换次数超过逆序对数&#xff0c;可以把这些逆序对全部消除…

【.net core】电商平台升级之微服务架构应用实战

一、前言这篇文章本来是继续分享IdentityServer4 的相关文章&#xff0c;由于之前有博友问我关于微服务相关的问题&#xff0c;我就先跳过IdentityServer4的分享&#xff0c;进行微服务相关的技术学习和分享。微服务在我的分享目录里面是放到四月份开始系列文章分享的&#xff…

c语言将一个已知头结点的单链表逆序_C语言实现常用数据结构:静态链表数组实现(第5篇)...

「今天是学习C语言第 148 天」纸上学来终觉浅&#xff0c;绝知此事要躬行。—— 陆游「冬夜读书示子聿」# 静态链表使用数组实现&#xff0c;利用数组下标代替指针&#xff0c;从而实现数据结点之间的先后关系。实现要点&#xff1a;1.数组下标为0的位置为头结点&#xff0c;指…

集成平台集群任务动态分派

源宝导读&#xff1a;MIP集成平台是为了解决企业大量异构系统之间快速、稳定集成的需要&#xff0c;助力企业数字化转型&#xff0c;明源云自主研发的平台系统。本文将对"事件任务分派"场景的架构设计以及实践成果进行分享。背景MIP集成平台是为了解决企业大量异构系…

dotcpp1115 DNA-打印图案

题目描述 小强从小就喜欢生命科学&#xff0c;他总是好奇花草鸟兽从哪里来的。终于&#xff0c; 小强上中学了&#xff0c;接触到了神圣的名词–DNA.它有一个双螺旋的结构。这让一根筋的小强抓破头皮&#xff0c;“要是能画出来就好了” 小强喊道。现在就请你帮助他吧 输入 输…

akb48_AKB48里历史——六年的终结

注&#xff1a;这是2012年发行的一本在BUBUKA连载的基础上补充了一些内容的粉丝公式教科书&#xff0c;从里面找了部分内容翻译了一下&#xff0c;节选的内容主要说的是2011年的事情&#xff0c;以当时作者的视角&#xff0c;是AKB48第一次新老粉丝换代的时期。前田敦子和大岛优…

[头脑风暴] 解读Docker Bridge网络模型

背景这几天在研究Kubernetes&#xff0c; 遇到一个有意思的nodejs镜像&#xff1a;luksa/kubia# 不带端口映射启动容器 docker run -it -d luksa/kubia # 连接到默认的Bridge网桥&#xff0c;容器IP是 172.17.0.2之后&#xff0c;在宿主机使用容器IP和8080 端口可访问该容器…

kodi pvr 不能安装_「家庭影音串流」电视最强播放器KODI使用方法

本文作者&#xff1a;空翻的帕兹文章适用电脑手机等全平台设备&#xff0c;在用户没有nas的情况下如何使用串流电视最强播放器KODIKodi是由XBMC基金會開發的開源媒體播放器&#xff0c;原名XBMC(最後一個以XBMC命名的版本是13.2「Gotham」&#xff0c;14.0 「Helix」是第一個以…

docker部署flask项目

项目本地运行 1.到github或者自己创建一个flask项目&#xff0c;确保在本地是可以运行成功的 2.上传到自己的代码仓库 服务器部署 1.安装docker yum install docker -y2.配置加速器 DaoCloud加速器采用自主研发的智能路由及缓存技术&#xff0c;并引入了现金的协议层优化…

.NET Core开发实战(第24课:文件提供程序:让你可以将文件放在任何地方)--学习笔记...

24 | 文件提供程序&#xff1a;让你可以将文件放在任何地方文件提供程序核心类型&#xff1a;1、IFileProvider2、IFileInfo3、IDirectoryContentsIFileProvider 是访问各种各样文件提供程序的接口通过这样子抽象的定义&#xff0c;让我们与具体的抽象文件的读取的代码进行了隔…

连接mysql数据库_解决Navicat连接MySQL数据库报错问题

今天在用Navicat连接另外一台主机上的MySQL时报错&#xff1a;Host is not allowed to connect to this MySQL server默认安装的mysql无法远程连接是因为MySQL默认配置了不支持远程连接引起的。解决方法&#xff1a;一、本地主机上登录root用户找到mysql.exe所在路径&#xff0…

洛谷T172098 子串-substr

代码如下&#xff1a; #include <iostream> #include <cstring> using namespace std;int main() {int cnt;cin >> cnt;string a, b;while (cnt--) {int n, m;cin >> n >> m;cin >> a;cin >> b;int ans 0 ;for (int i 0; i < …

在Ocelot中使用自定义的中间件(二)

在上文中《在Ocelot中使用自定义的中间件&#xff08;一&#xff09;》&#xff0c;我介绍了如何在Ocelot中使用自定义的中间件来修改下游服务的response body。今天&#xff0c;我们再扩展一下设计&#xff0c;让我们自己设计的中间件变得更为通用&#xff0c;使其能够应用在不…

机器学习理论引导 电子版_机器学习理论篇1:机器学习的数学基础(2)

本节主要就是讲述的机器学习的数学基础&#xff0c;提到数学基础&#xff0c;可能一眼就会是满眼的枯燥、没意思&#xff0c;但是成就英雄的路上注定了孤独&#xff0c;要想要真正的在学术上有所突破就必须挨得住寂寞&#xff0c;受得住孤独&#xff0c;才能真正的走进熟悉直到…