山东泰润建设集团网站,福田网站建设设计公司哪家好,天元建设集团有限公司 刘洪顺,网站seo优化方案设计前言读完上篇《C#如何安全、高效地玩转任何种类的内存之Span的本质(一)》#xff0c;相信大家对span的本质应该非常清楚了。含着金钥匙出生的它#xff0c;从小就被寄予厚望要成为.NET下编写高性能应用程序的重要积木#xff0c;而且很多老前辈为了接纳它#xff0c;都纷纷… 前言读完上篇《C#如何安全、高效地玩转任何种类的内存之Span的本质(一)》相信大家对span的本质应该非常清楚了。含着金钥匙出生的它从小就被寄予厚望要成为.NET下编写高性能应用程序的重要积木而且很多老前辈为了接纳它都纷纷做出了改变比如String、Int、Array。现在它长大了已经成为.NET下发挥关键作用的新值类型和旗舰成员。那我们又该如何接纳它呢一句话熟悉它的脾气秉性让好钢用到刀刃上。脾气秉性 - 特点Slow vs Fast Span上篇博客介绍了span的本质主要涉及到三个字段如下public struct SpanT {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 SpanT {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的应用场景Span不能作为类的字段。class Impossible
{Spanbyte field;
}Span不能实现任何接口先来看一段C#伪代码struct StructTypeT : IEnumerableT { }
class SpanStructTypeSample
{static void Test(){var value new StructTypeint();Parse(value);}static void Parse(IEnumerableint collection) { }
}使用ILDasm查看生成的IL代码.method public hidebysig static void Test() cil managed // 调用Test方法
{// Code size 22 (0x16).maxstack 1.locals init (valuetype SpanTest.StructType1int32 V_0)IL_0000: nopIL_0001: ldloca.s V_0IL_0003: initobj valuetype SpanTest.StructType1int32IL_0009: ldloc.0IL_000a: box valuetype SpanTest.StructType1int32 // 装箱意味着被储存到托管堆上。IL_000f: call void SpanTest.SpanStructTypeSample::Parse(class [System.Runtime]System.Collections.Generic.IEnumerable1int32)IL_0014: nopIL_0015: ret
} // end of method SpanStructTypeSample::Test上面的代码很明确首先让自定义的值类型实现接口IEnumerable然后作为参数传递给Parse最后分析IL代码发现参数被装箱了意味着将被储存到托管堆上如果将来C#能专门定义只用于struct的接口那么就能扩展Stack-Only结构到此应用场景了一起期待吧。Span不能作为异步方法的参数首先async和await 是非常棒的语法糖不仅仅大大地简化了编写异步代码的难度而且还带来了代码的优雅度。同样先来看一段C#代码public async Task TestAsync(Spanbyte data) { }这样的用法也是禁止的编译时就会报错Parameter or local type Spanbyte cannot be declared in async method.。因为本质上async await 的内部是通过AsyncMethodBuilder来创建一个异步的状态机某一时刻可能会将方法参数储存到托管堆上。Span不能作为泛型类型的参数同样先来看一段C#代码FuncSpanbyte valueProvider () new Spanbyte(new byte[256]);
object value valueProvider.Invoke(); // 装箱这样的用法也是禁止的编译时会报错The type Spanbytemay not be used as a type argument.。同理spanbyte可以表示内存任意区域而实际使用时肯定需要类型化对象无法避免装箱。那么微软为什么不引入一种新的泛型约束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
{Spanbyte _memory new byte[1024];public void Resize(int newSize){_memory new byte[newSize]; // 因为这里无法保证原子更新}public byte this[int index] _memory[index]; // 所以这里可能的部分更新
}其实有两种办法可以解决这个问题直接处理 - 加锁即强制同步访问。间接处理 - 私有化字段即不给外面观察到部分更新的机会。如果这样就无法保证像数组一样的高性能因此不能给字段加锁也不能限制访问没意义另外对Span的访问和写入都是直接操作的内存如果_reference和_length出现不同步的情况还会导致内存安全问题。这也是为什么span只能存在于栈上即指针、数据、长度全都存于栈上而不是引用存在栈数据存在堆因为spanT不需要暂留必须快取快用快放否则就不要使用span。备注对于需要暂留到堆上的场景它的解决方案是MemoryT大家可以继续关注。.NET库的集成为了支持轻松高效地处理 {ReadOnly}Span 微软向.NET添加了数百个新成员和类型。目前大多是基于数组、字符串和基元类型的方法的重载 除此之外还包括一些专注于特定处理方面的全新类型比如System.IO.Pipelines。下面是一些比较常用的扩展基元类型伪代码short.Parse(ReadOnlySpanchar s);
int.Parse(ReadOnlySpanchar s);
long.Parse(ReadOnlySpanchar s);
DateTime.Parse(ReadOnlySpanchar s);
TimeSpan.Parse(ReadOnlySpanchar input);
Guid.Parse(ReadOnlySpanchar input);字符串public static ReadOnlySpanchar AsSpan(this string text, int start, int length);
public static ReadOnlySpanchar AsSpan(this string text, int start);
public static ReadOnlySpanchar AsSpan(this string text);
public static String CreateTState(int length, TState state, SpanActionchar, TState action);数组public static SpanT AsSpanT(this T[] array, int start);
public static SpanT AsSpanT(this T[] array);
public static SpanT AsSpanT(this ArraySegmentT segment, int start, int length);
public static SpanT AsSpanT(this ArraySegmentT segment, int start);
public static SpanT AsSpanT(this T[] array, int start, int length);Guidpublic static bool TryParse(ReadOnlySpanchar input, out Guid result);
public bool TryFormat(Spanchar destination, out int charsWritten, ReadOnlySpanchar format default (ReadOnlySpanchar));最后使用上面的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只能驻留到栈上完美解决了以下的问题更高效地内存访问快取快用快放的天然保障。更高效地GC跟踪。并发内存安全。备注正是由于Stack-Only这个特点在底层数据访问、转换以及同步处理方面Span性能非常出色。此外本篇还在上篇的基础上详细讲解span的脾气秉性以及每种特点下的非法应用场景一切都是为了大家能够在.NET 程序中使用span高效安全地访问内存希望大家能有所收获。下一篇可能会讲span的加强也可能会讲它在数据转换以及同步处理方面的应用比如Data Pipelines、Discontinuous Buffers、Buffer Pooling等也可能会讲MemoryT感兴趣请继续关注。最后如果有什么疑问和见解欢迎评论区交流。如果你觉得本篇文章对您有帮助的话感谢您的【推荐】。如果你对高性能编程感兴趣的话可以关注我我会定期的在博客分享我的学习心得。欢迎转载请在明显位置给出出处及链接。延伸阅读https://adamsitnik.com/Hardware-Counters-Diagnoser/#how-to-get-it-running-for-net-coremono-on-windowshttps://blogs.msdn.microsoft.com/dotnet/2017/10/16/ryujit-just-in-time-compiler-optimization-enhancementshttps://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Span.Fast.cshttps://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Span.cshttps://docs.microsoft.com/en-us/dotnet/csharp/write-safe-efficient-code