编写高性能的C#代码(三)使用SPAN

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

作者介绍:

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

编写高性能的C#代码(三)使用SPAN 

这篇文章继续了我有关编写高性能C#代码的系列文章[1]。在本文中,我们将通过介绍Span 类型从上两篇文章继续,并通过将其转换为基于Span的版本来重构一些现有代码。我们将使用Benchmark.NET比较这些方法并验证我们的更改是否改进了代码。

如果您想遵循示例代码,可以在GitHub上找到[2]

什么是SPAN ?

Span是C#7.2引入的一种新类型,在.NET Core 2.1运行时中受支持。现有的.NET Standard 1.0运行时都有一个.NET Standard实现,但是在.NET Core中,我将重点介绍运行时更改,以支持可能的最佳版本,也称为“fast span”。

Span提供对内存连续区域的类型安全访问。该内存可以位于堆,堆栈上,甚至可以由非托管内存组成。Span具有相关的类型ReadOnlySpan ,该类型提供内存中数据的只读视图。ReadOnlySpan可用于查看不可变类型(例如字符串)占用的内存。我更喜欢将Span视为进入某些现有内存的窗口,而不管其分配在何处。

图片

在上图中,Span 引用一些已经分配的连续内存。现在,我们在该内存上有了一个窗口。

Span 被定义为引用结构,这意味着它仅限于仅在堆栈上分配。这减少了一些潜在的用例,例如将其存储为类中的字段或在异步方法中使用它。这些限制可以通过使用类似的新型Memory来解决,我们将在以后的文章中介绍它。引用结构设计的主要原因是要确保在使用Span时,我们不会引起其他堆分配。这是它支持高性能代码路径中如此高度优化的用例的原因之一。

我将避免为这篇文章过多地介绍实现细节(毕竟这是一篇介绍),而将重点放在一个示例中,我们可能在哪里使用它以及它如何影响我们的基准。

如果您想阅读有关Span的更多详细信息,我建议以下链接:

•Span 结构[3]•有关Span的所有信息:探索新的.NET主体[4]•C#7.2:了解Span[5]•Span By Adam Sitnik[6]

加快现有代码的速度并减少分配

一篇文章中,我们对一些代码进行了基准测试,这些代码用于从全名字符串中“解析”姓氏。通过Benchmark.NET,我们确定该方法需要125.8 ns的时间运行,并且每次运行分配160个字节。

在使用基于Span 的方法进行重构之前,我希望这是一个公平的竞争,因此我将首先不使用Span 来优化代码。这有望成为一个很好的例子,因为它着重指出,即使不使用Span之类的新功能,也可以通过对正在执行的工作进行一些思考来优化现有代码。

当前代码在任何空格上分割字符串,这将组成一个字符串数组。如果考虑到这一点,我们将分配一个数组,在使用名称“ Steve J Gordon”的情况下,这样做时将分配三个较小的字符串“ Steve”,“ J”和“ Gordon”。正如我们在基准测试中所看到的那样,这会导致分配160个字节。

对于查找姓氏的要求,我们不在乎存储名称的所有部分,而只是存储我们希望是姓氏的最后一部分。请注意,在此示例中,我忽略了多词姓氏等情况!

让我们向NameParser添加另一个方法,该方法而不是拆分字符串,而是获取最后一个空格字符的索引,并使用该方法获取代表姓氏的子字符串。

public string GetLastNameUsingSubstring(string fullName)
    {
        var lastSpaceIndex = fullName.LastIndexOf(" ", StringComparison.Ordinal);




        return lastSpaceIndex == -1
            ? string.Empty
            : fullName.Substring(lastSpaceIndex + 1);
    }

首先,我们获取全名字符串中最后一次出现空格的索引。如果它为-1,则找不到任何空格,因此我们将返回一个空字符串作为默认结果。如果找到索引,则使用Substring方法提取姓氏并将其返回。

我们稍后会将该版本包含在我们的基准测试中。但是,实际上,值得在进行代码改进的每个迭代时对其进行测试,以验证您是在改进方面还是使它们变得更糟。

使用SPAN 

让我们看看这次如何使用Span 重新编写此代码。在高性能需求旺盛的场景中,我们既要提高速度又要减少代码中的内存分配。

 public ReadOnlySpan<char> GetLastNameWithSpan(ReadOnlySpan<char> fullName)
    {
        var lastSpaceIndex = fullName.LastIndexOf(' ');




        return lastSpaceIndex == -1 
            ? ReadOnlySpan<char>.Empty 
            : fullName.Slice(lastSpaceIndex + 1);
    }

首先要注意的是,方法参数“ fullName”现在的类型为ReadOnlySpan 。某些类型(例如字符串)可以隐式转换为chars的ReadOnlySpan,因此此方法签名可以正常工作。现在,返回类型也是ReadOnlySpan。

首先,以与上面的优化代码非常相似的方式,我们寻找空格字符的最后一个索引。

同样,如果其值为-1,则我们找不到空格,并且将返回空的ReadOnlySpan 结果。

如果找到空格字符,我们现在可以使用Span的一种功能,即“切片”(Slice)。

切片是一项非常强大的操作,我们可以将现有的Span和“切片”放到更紧密的窗口中。切片时,我们为切片指定起始位置的索引,并为切片指定终止位置的长度。省略长度会导致从起始位置到Span结束的切片。

切片是一种低成本的操作,因为我们不复制任何内容,而只是创建一个新的Span,该Span表示一个进入现有内存范围子集的窗口。

图片

在上图中,我们可以创建原始Span的Slice来查看其中的5个元素,而无需分配原始内存的任何其他副本。

在新的基于Span的代码中,我们从空格字符后的索引处开始获取fullName的一部分。由于我们未指定长度,因此此切片将运行到现有Span的末尾。

对Span进行切片后,会在切片的部分上产生一个新的Span,然后将其作为方法的结果返回。

至此,我们有两个潜在的改进代码版本,一个使用Substring,另一个使用Span 。让我们更新基准并比较结果。

衡量改进基准

添加两个新基准后,基准类现在如下所示:

[RankColumn]
    [Orderer(SummaryOrderPolicy.FastestToSlowest)]
    [MemoryDiagnoser]
    public class NameParserBenchmarks
    {
        private const string FullName = "Steve J Gordon";
        private static readonly NameParser Parser = new NameParser();




        [Benchmark(Baseline = true)]
        public void GetLastName()
        {
            Parser.GetLastName(FullName);
        }




        [Benchmark]
        public void GetLastNameUsingSubstring()
        {
            Parser.GetLastNameUsingSubstring(FullName);
        }




        [Benchmark]
        public void GetLastNameWithSpan()
        {
            Parser.GetLastNameWithSpan(FullName);
        }
    }

我们定义了三个基准,每个基准在NameParser中采用不同的方法。运行基准测试在我的计算机上给出以下结果…

图片

此列表中的最后一项是我们原始的GetLastName方法。因为我们要求获得排名结果,并且此方法运行的最慢,所以它在最后显示出来。

这次大约花了125ns的时间运行,当然仍然分配了160个字节。

第二快的是我们尝试在不使用Span 的情况下改进代码的情况,该代码使用Substring。此代码比原始方法快大约3倍。重要的是,我们现在将分配减少到只有40个字节。这说明了我们在调用子字符串时要分配的姓氏字符串。

总的赢家是基于Span 的方法。这比我们的原始代码快10倍,比基于子字符串的方法快2.8倍。

这里真正重要的是,因为我们要对Span进行切片以查找姓氏的位置,并且还返回Span作为方法的输出,所以我们永远不会分配新的字符串。通过已分配的内存状态(现在为空)可以明显看出这一点。

图片

对于单个调用,节省的160个字节(或子字符串方法节省40个字节)并不庞大,但是在特定场景下上,节省的费用加起来了。

如果此代码需要在我维护的每天处理约2000万条消息的数据处理服务中运行,那么我们每天将节省3.2 GB的分配。这些可能是短暂的分配,但是即使如此,它们仍将导致垃圾回收。根据估算的Gen 0 / 1k操作数(译者注,是指0代回收,每次回收1k字节),原始代码每天将触发2,000个操作,共506个GC。

这是CPU时间和暂停时间,我们可以通过避免分配任何资源来帮助减少时间。

摘要

在本文中,我们研究了新的Span 类型,并使用它重构了一些代码以实现最佳性能。最初,Span听起来可能有点复杂,但正如我希望我已经展示的那样,在本示例中使用它非常简单。

谢谢阅读!

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

References

[1] 有关编写高性能C#代码的系列文章: https://www.stevejgordon.co.uk/motivations-for-writing-high-performance-csharp-code
[2] 示例代码,可以在GitHub上找到: https://github.com/stevejgordon/BenchmarkAndSpanExample
[3] Span 结构: https://docs.microsoft.com/en-us/dotnet/api/system.span-1?view=netcore-2.2
[4] 有关Span的所有信息:探索新的.NET主体: https://msdn.microsoft.com/en-us/magazine/mt814808.aspx
[5] C#7.2:了解Span: https://channel9.msdn.com/Events/Connect/2017/T125
[6] Span By Adam Sitnik: https://adamsitnik.com/Span/
[7] 在此处: https://www.stevejgordon.co.uk/writing-high-performance-csharp-and-dotnet-code

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 好的,在Unity中使用高性能的Socket代码,你可以使用Unity的System.Net.Sockets命名空间。下面是一个简单的例子,展示了如何使用Socket连接到远程服务器并发送和接收数据: ``` using UnityEngine; using System.Net.Sockets; using System.Text; public class SocketExample : MonoBehaviour { private Socket _socket; private byte[] _buffer = new byte[1024]; void Start() { // 创建Socket _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // 连接到远程服务器 _socket.Connect("www.example.com", 80); // 发送数据 string data = "Hello World!"; byte[] buffer = Encoding.UTF8.GetBytes(data); _socket.Send(buffer); // 接收数据 int recv = _socket.Receive(_buffer); string recvData = Encoding.UTF8.GetString(_buffer, 0, recv); Debug.Log("Received: " + recvData); } void OnApplicationQuit() { // 关闭Socket _socket.Close(); } } ``` 希望这个例子能帮到你! ### 回答2: 使用Unity编写高性能的socket代码需要注意以下几点: 1. 选择合适的库:Unity自带支持socket通信的API,但是性能不高。可以选择使用方库如DotNetty,它是C#高性能异步网络编程框架,在Unity中也可以使用。 2. 使用异步编程模型:使用async/await关键字可以让代码以异步的方式执行,充分利用IO等待时间。在处理网络数据时,可以使用这种模型来提高性能。 3. 采用事件驱动的方式:使用事件驱动模型可以减少线程等待时间,提高并发性能。可以使用事件回调的方式处理网络数据的接收和发送。 4. 使用TCP连接池:为了减少频繁创建和销毁的开销,可以使用TCP连接池来管理连接。连接池可以复用已经建立的连接,减少建立连接的时间和资源消耗。 5. 使用数据缓冲区:在处理网络数据时,可以使用缓冲区来存储接收和发送的数据,避免频繁的内存分配和复制操作。 6. 设置合理的超时时间:在建立连接、接收和发送数据时,可以设置合理的超时时间,避免因为网络不稳定或者异常情况导致程序阻塞。 7. 合理处理异常:在网络通信中,可能会遇到各种异常情况,如连接断开、超时等。合理处理这些异常可以保证程序的稳定性和可靠性。 使用这些技巧,可以编写高性能的socket代码。但是需要根据具体的业务需求和环境来进行优化和调整,以获得最佳的性能表现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值