C#如何安全、高效地玩转任何种类的内存之Span的秉性特点(二)

前言

读完上篇《C#如何安全、高效地玩转任何种类的内存之Span的本质(一)》,相信大家对span的本质应该非常清楚了。含着金钥匙出生的它,从小就被寄予厚望要成为.NET下编写高性能应用程序的重要积木,而且很多老前辈为了接纳它,都纷纷做出了改变,比如String、Int、Array。现在,它长大了,已经成为.NET下发挥关键作用的新值类型和旗舰成员。

那我们又该如何接纳它呢?

一句话,熟悉它的脾气秉性,让好钢用到刀刃上

脾气秉性 - 特点

Slow vs Fast Span

上篇博客介绍了span的本质,主要涉及到三个字段,如下:

public struct Span<T> {internal IntPtr _byteOffset; // 偏移量internal object _reference;// 引用,可以看作当前对象的索引internal int _length;// 长度
}

当我们访问span表示的整体或部分内存时,内部的索引器通过计算(ref reference + byteOffset) + index * sizeOf(T)来正确直接地返回实际储存位置的引用,而不是通过复制内存来返回相对位置的副本,从而达到高性能,但是,现在我要告诉你,这种span被叫做slow span,为什么呢?因为C#7.2的新特性ref T支持在签名中直接返回引用(相当于直接整合了这个过程),这样就无需通过计算来确定指针开头及其起始偏移,从而真正拥有和访问数组一样高的效率,如下:

public struct Span<T> {internal ref T _reference;// 引用,本身已整合_byteOffset、_reference两者。internal int _length;// 长度
}

这种只包含两个字段的span就叫Fast span

在所有的.NET平台,Slow Span都是可得到的,但是目前只有.NET Core 2.X原生支持Fast span。

为了让大家更直观地了解这两种Span,下面来做两组基准测试

  • 不同运行时下Span进行10万次Get、Set的基准测试

    上图非常清楚了吧,从Mean(均值)指标可以看出差异还是比较大的(约60%),net framework时代追求生产力,而core时代追求高性能,所以还是早转core吧,并且新版本core还会进一步优化span,差距将会越来越大。

  • Span vs Array的基准测试

    不同运行时下,对Span和Array进行10万次Get、Set操作

    从上图Mean(均值)指标可以得出:

    • slow span,即运行时原生不支持,在性能上,它的Get、Set操作和数组差异50%左右。

    • fast span,即运行时原生支持,在性能上,它的Get、Set操作和数组相当。

看了上面测试,可能有的同学就会问了用Array就行了,如果总是操作整个数组,这是合适的,但如果想操作数组的一部分数据呢?按照以前的做法每次复制一份相对位置的副本给调用方,这就非常消耗性能的,那么如何支持对完整或部分数组的操作保持同样高的性能呢?答案就是span,没有之一。span不仅能用于访问数组和分离数组子集,还可引用来自内存任意区域的数据,比如本机代码、栈内存、托管内存。

基准测试示例源码参考

Stack-Only

分配一块栈内存是非常快速的,也无需手工释放,它会随着当前作用域而释放,比如方法执行结束时,就自动释放了,所以需要快取快用快放。Span虽然支持所有类型的内存,但决定安全、高效地操作各种内存的下限自然取决于最严苛的内存类型,即栈内存,好比木桶能装多少水,取决于最短的那块木板。此外,上一篇博客的动画非常清晰地演示了span的本质,每次都是通过整合内部指针为新的引用返回,而.NET运行时跟踪这些内部指针的成本非常高昂,所以将span约束为仅存在于栈上,从而隐式地限制了可以存在的内部指针数量。

备注:栈内存的容量非常小, ARM、x86 和 x64 计算机,默认堆栈大小为 1 MB。CLR和编译器会自动检测Stack-Only约束。

所以span必须是值类型,它不能被储存到堆上。

违背Stack-Only的应用场景

  1. Span不能作为类的字段

    class Impossible
    {Span<byte> field;
    }
  2. Span不能实现任何接口

    先来看一段C#(伪代码):

    struct StructType<T> : IEnumerable<T> { }
    class SpanStructTypeSample
    {static void Test(){var value = new StructType<int>();Parse(value);}static void Parse(IEnumerable<int> collection) { }
    }

    使用ILDasm查看生成的IL代码:

    .method public hidebysig static void  Test() cil managed // 调用Test方法
    {// Code size       22 (0x16).maxstack  1.locals init (valuetype SpanTest.StructType`1<int32> V_0)IL_0000:  nopIL_0001:  ldloca.s   V_0IL_0003:  initobj    valuetype SpanTest.StructType`1<int32>IL_0009:  ldloc.0IL_000a:  box        valuetype SpanTest.StructType`1<int32> // 装箱,意味着被储存到托管堆上。IL_000f:  call       void SpanTest.SpanStructTypeSample::Parse(class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32>)IL_0014:  nopIL_0015:  ret
    } // end of method SpanStructTypeSample::Test

    上面的代码很明确,首先让自定义的值类型实现接口IEnumerable,然后作为参数传递给Parse,最后分析IL代码发现参数被装箱了,意味着将被储存到托管堆上,如果将来C#能专门定义只用于struct的接口,那么就能扩展Stack-Only结构到此应用场景了,一起期待吧。

  3. Span不能作为异步方法的参数

    首先asyncawait 是非常棒的语法糖,不仅仅大大地简化了编写异步代码的难度,而且还带来了代码的优雅度。

    同样,先来看一段C#代码:

    public async Task TestAsync(Span<byte> data) { }

    这样的用法也是禁止的,编译时就会报错Parameter or local type Span<byte> cannot be declared in async method.。因为本质上,async & await 的内部是通过AsyncMethodBuilder来创建一个异步的状态机,某一时刻可能会将方法参数储存到托管堆上。

  4. Span不能作为泛型类型的参数

    同样,先来看一段C#代码:

    Func<Span<byte>> valueProvider = () => new Span<byte>(new byte[256]);
    object value = valueProvider.Invoke(); // 装箱

    这样的用法也是禁止的,编译时会报错The type Span<byte>may not be used as a type argument.。同理,span<byte>可以表示内存任意区域,而实际使用时肯定需要类型化对象,无法避免装箱。那么微软为什么不引入一种新的泛型约束:stackonly,而是决定禁止span作为泛型参数,因为这需要编译器检查所有的代码,可能还需要理解代码逻辑(因为有的类型需要运行时才能确定),不然是无法保证stackonly约束的,呵呵,目前看来是不现实的,不知人工智能能否解决这个问题。

Stack Tearing

阐述这个特点前,先简单说说计算机的字大小。

  • 计算机的字大小

    表示计算机中CPU的字长,32位CPU字长为32位,即4字节;64位CPU字长为64位,即8字节。CPU的字长决定了每次能够原子更新的连续内存块的大小

栈撕裂其实是多线程下的数据同步问题,当结构数据大于当前处理器的字大小时,都会面临这个问题。如前所述,span内部包含多个字段,这就意味着,一些处理器可能无法保证原子更新span_reference_length 字段,也就是说,多线程下_reference_length可能来自于两个不同的span。

internal class Buffer
{Span<byte> _memory = new byte[1024];public void Resize(int newSize){_memory = new byte[newSize]; // 因为这里无法保证原子更新}public byte this[int index] => _memory[index]; // 所以这里可能的部分更新
}

其实有两种办法可以解决这个问题:

  1. 直接处理 - 加锁,即强制同步访问。

  2. 间接处理 - 私有化字段,即不给外面观察到部分更新的机会。

如果这样,就无法保证像数组一样的高性能,因此不能给字段加锁,也不能限制访问(没意义),另外对Span的访问和写入都是直接操作的内存,如果_reference_length出现不同步的情况,还会导致内存安全问题。

这也是为什么span只能存在于栈上,即指针、数据、长度全都存于栈上,而不是引用存在栈,数据存在堆,因为span<T>不需要暂留,必须快取快用快放,否则就不要使用span。

备注:对于需要暂留到堆上的场景,它的解决方案是Memory<T>,大家可以继续关注。

.NET库的集成

为了支持轻松高效地处理 {ReadOnly}Span ,微软向.NET添加了数百个新成员和类型。目前大多是基于数组、字符串和基元类型的方法的重载 ,除此之外,还包括一些专注于特定处理方面的全新类型,比如:System.IO.Pipelines。

下面是一些比较常用的扩展:

  1. 基元类型(伪代码)

    short.Parse(ReadOnlySpan<char> s);
    int.Parse(ReadOnlySpan<char> s);
    long.Parse(ReadOnlySpan<char> s);
    DateTime.Parse(ReadOnlySpan<char> s);
    TimeSpan.Parse(ReadOnlySpan<char> input);
    Guid.Parse(ReadOnlySpan<char> input);
  2. 字符串

    public static ReadOnlySpan<char> AsSpan(this string text, int start, int length);
    public static ReadOnlySpan<char> AsSpan(this string text, int start);
    public static ReadOnlySpan<char> AsSpan(this string text);
    public static String Create<TState>(int length, TState state, SpanAction<char, TState> action);
  3. 数组

    public static Span<T> AsSpan<T>(this T[] array, int start);
    public static Span<T> AsSpan<T>(this T[] array);
    public static Span<T> AsSpan<T>(this ArraySegment<T> segment, int start, int length);
    public static Span<T> AsSpan<T>(this ArraySegment<T> segment, int start);
    public static Span<T> AsSpan<T>(this T[] array, int start, int length);
  4. Guid

    public static bool TryParse(ReadOnlySpan<char> input, out Guid result);
    public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format = default (ReadOnlySpan<char>));

最后使用上面的API演示一个官网的例子,解析字符串"123,456"中的数字:

以前的写法

var input = "123,456";
var commaPos = input.IndexOf(',');
var first = int.Parse(input.Substring(0, commaPos));// yes-Allocating, yes-Coping
var second = int.Parse(input.Substring(commaPos + 1));// yes-Allocating, yes-Coping

现在的写法

var input = "123,456";
var inputSpan = input.AsSpan();
var commaPos = input.IndexOf(',');
var first = int.Parse(inputSpan.Slice(0, commaPos));// no-Allocating, no-Coping
var second = int.Parse(inputSpan.Slice(commaPos + 1));// no-Allocating, no-Coping

当然还是有许多这样的方法,比如System.Random、System.Net.Socket、Utf8Formatter、Utf8Parser等,明白了它的脾气秉性,对于具体的应用场景大家可以先自行查阅资料,相信认真读完上篇、本篇的同学已经具备用好这把尖刀的能力了。

总结

综上所诉,通过限制Span只能驻留到栈上,完美解决了以下的问题:

  1. 更高效地内存访问,快取快用快放的天然保障

  2. 更高效地GC跟踪

  3. 并发内存安全

备注:正是由于Stack-Only这个特点,在底层数据访问、转换以及同步处理方面,Span性能非常出色。

此外,本篇还在上篇的基础上,详细讲解span的脾气秉性,以及每种特点下的非法应用场景,一切都是为了大家能够在.NET 程序中使用span高效安全地访问内存,希望大家能有所收获。下一篇可能会讲span的加强,也可能会讲它在数据转换以及同步处理方面的应用,比如:Data Pipelines、Discontinuous Buffers、Buffer Pooling等,也可能会讲Memory<T>,感兴趣请继续关注。

最后

如果有什么疑问和见解,欢迎评论区交流。
如果你觉得本篇文章对您有帮助的话,感谢您的【推荐】。
如果你对高性能编程感兴趣的话可以关注我,我会定期的在博客分享我的学习心得。
欢迎转载,请在明显位置给出出处及链接

延伸阅读

https://adamsitnik.com/Hardware-Counters-Diagnoser/#how-to-get-it-running-for-net-coremono-on-windows

https://blogs.msdn.microsoft.com/dotnet/2017/10/16/ryujit-just-in-time-compiler-optimization-enhancements

https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Span.Fast.cs

https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Span.cs

https://docs.microsoft.com/en-us/dotnet/csharp/write-safe-efficient-code

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

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

相关文章

word List 05

word List 05 如果存在什么问题&#xff0c;欢迎批评指正&#xff01;谢谢&#xff01;

《ASP.NET Core 微服务实战》-- 读书笔记(第6章)

第 6 章 事件溯源与 CQRS在本章&#xff0c;我们来了解一下随着云平台一同出现的设计模式我们先探讨事件溯源和命令查询职责分离&#xff08;CQRS&#xff09;背后的动机与哲学事件溯源简介事实由事件溯源而来我们大脑就是一种事件溯源系统&#xff0c;接收感官多种形式刺激&am…

数据结构----快速排序

数据结构----快速排序 原理&#xff1a;参考趣学数据结构 代码&#xff1a; #include<stdio.h> #include<stdlib.h> int quickSort(int a[], int l, int h) {//快速排序int i l, j h, p a[l];while (i < j) {while (i<j&&a[j]>p) {//从右往左…

编译调试 .NET Core 5.0 Preview 并分析 Span 的实现原理

很久没有写过 .NET Core 相关的文章了&#xff0c;目前关店在家休息所以有些时间写一篇新的????。这次的文章主要介绍如何在 Linux 上编译调试最新的 .NET Core 5.0 Preview 与简单分析 Span 的实现原理。微软从 .NET Core 5.0 开始把 GIT 仓库 coreclr 与 corefx 合并移动…

数据结构----归并排序

数据结构----归并排序 原理&#xff1a;参考趣学数据结构 代码&#xff1a; #include<stdio.h> #include<stdlib.h> #define N 100 void guiBingSort(int a[], int l, int h,int length) {//归并排序int mid (l h) / 2;int* b (int *)malloc(N*sizeof(int));i…

利用obfuscar对.NET应用进行混淆

背景发布客户端程序产品时&#xff0c;免不了会遇到一些怀有恶意或有强烈学习欲望的用户尝试对程序进行反编译。对于一些编译成本地指令的程序&#xff08;如C、C&#xff09;&#xff0c;编译后可读性低&#xff0c;反编译和破解成本较高&#xff0c;不需要对代码进行太高强度…

数据结构---基数排序

数据结构—基数排序 原理&#xff1a;参考趣学数据结构 代码&#xff1a; #include<stdio.h> #include<stdlib.h> int getNumberBit(int number) {//获取数字的位数int x number,count0;if (x 0)return 1;while (x ! 0) {count;x / 10;}return count; } int g…

C# 版本 疫情传播仿真程序

前言前一阵子看到有人制作了《疫情传播仿真程序》&#xff0c;是用 Java做的。里面根据多种实际情况&#xff0c;如居民移动意愿、医护能力、病毒传播能力&#xff0c;来模拟疫情的发展。看完之后&#xff0c;我暗暗称奇&#xff0c;特别是结合一些视频和照片&#xff0c;确实做…

jmeter 加密解密_使用Jmeter对SHA1加密接口进行性能测试

机会只留给那些有准备的人改变能改变的&#xff0c;接受不能改变的&#xff0c;就是进步性能测试过程中&#xff0c;有时候会遇到需要对信息头进行加密鉴权&#xff0c;下面我就来介绍如何针对SHA1加密鉴权开发性能测试脚本 1、首先了解原理&#xff0c;就是需要对如下三个参数…

word List 06

word List 06 如果存在什么问题&#xff0c;欢迎批评指正&#xff01;谢谢&#xff01;

面对疫情,在家办公的程序员如何突围

作者&#xff1a;陌北有棵树&#xff0c;架构师社区合伙人很多程序员朋友都已经开始了在家办公的生活&#xff0c;第一天办公&#xff0c;你的远程工具还流畅吗&#xff0c;视频会议换了几个软件&#xff1f;当然这些都是外在因素&#xff0c;尤其对于程序员来说&#xff0c;解…

[蓝桥杯2018初赛]全球变暖-dfs,bfs,连通块

解题思路: bfs:遍历所有未遍历过的陆地&#xff0c;通过bfs计算出当前位置连通陆地的数量cnt&#xff0c;以及被淹没陆地的数量bound,若cnt bound表示完整淹没的一个岛屿 dfs:将连通块全部标记&#xff0c;如果这个连通块全部都会淹没&#xff0c;则答案1&#xff0c;如果这个…

latex 参考文献显示问号_回「LaTeX 的罪与罚」

原文链接&#xff1a;LaTeX 的罪与罚 - 朴素的贝叶斯的文章 - 知乎作为 LaTeX 开发者&#xff0c;看到这种嘲讽自然是非常 angry 的。本来并不想趟这个混水&#xff0c;然而眼见着赞数一天天涨上去&#xff0c;还居然进了精华区&#xff0c;实在忍不住只好注册了贵乎来说几句。…

疫情之下,使用FRP实现内网穿透,远程连接公司电脑进行办公

当前情况下&#xff0c;经常会有需要到公司电脑进行一些操作&#xff0c;比如连接内网OA&#xff0c;数据库或者提交文档。为了减少外出&#xff0c;将使用frp进行内网穿透的方法进行一个说明。前提条件1. 一台拥有公网 IP 的设备(如果没有&#xff0c;服务器可以使用https://d…

ad中电容用什么封装_二极管在电路中到底做什么用的

所有的电子电路中基本上都会用到二极管&#xff0c;它的特性也是非常之多&#xff0c;最主要就是单方向导电性&#xff0c;(单向导电性的两根引脚之间的电阻分为正向电阻和反向电阻两种)。人们利用这些不同特性构成各种具体的应用电路&#xff0c;分析不同电路中的二极管工作原…

数据结构---邻接矩阵的DFS

数据结构—邻接矩阵的DFS 原理&#xff1a;参考趣学数据结构 代码&#xff1a; #include<stdio.h> #include<stdlib.h> #define N 100 #define elemType int //const int MAX_INT (1 << 31) - 1; //const int MAX_INT 0X7fffffff; #define INF (((uns…

.NET Core 如何判断程序是否在远程桌面(RDP)下运行

点击上方蓝字关注“汪宇杰博客”导语由于疫情的关系&#xff0c;很久没发文章了。今天终于稳定下来在家办公&#xff0c;抽空分享一个刚学会的技巧。最近在家办公的程序员可能避免不了要用远程桌面&#xff0c;那么问题来了&#xff0c;你的 .NET Core 程序有没有办法知道自己是…

[蓝桥杯2018初赛]方格计数-巧妙枚举,找规,数论

解题思路&#xff1a; 枚举第一象限的所有点&#xff0c;判断是否在圆内&#xff0c;最后结果*4 我们用下面的程序&#xff0c;来算一个半径为2的圆&#xff0c;其实我们第一象限算的就是那个绿点&#xff0c;然后类比到半径5000. 代码如下&#xff1a; #include <iostre…

ipa在线安装搭建_三种越狱工具安装方法

从 iOS 9.2 开始&#xff0c;苹果越狱进入了半越狱时代&#xff0c;也就是重启手机之后需要重新进入越狱工具激活越狱环境&#xff0c;以下是三种常用的越狱工具安装方法&#xff1a;方法一&#xff1a;自签名下载大胡子签名工具&#xff1a;Cydia Impactor下载地址&#xff1a…

《ASP.NET Core 微服务实战》-- 读书笔记(第7章)

第 7 章 开发 ASP.NET Core Web 应用ASP.NET Core 基础在本章&#xff0c;我们将从一个命令行应用开始&#xff0c;并且在不借助任何模板&#xff0c;脚手架和向导的情况下&#xff0c;最终得到一个功能完整的 Web 应用GitHub链接&#xff1a;https://github.com/microservices…