第二个选项很少被开拓职员利用,由于用于非托管内存访问的API非常冗长。
Span<T>是C# 7.2中到达的一组值类型,它是来自不同来源的内存的无分配表示。Span<T>许可开拓职员以更方便的办法处理连续内存区域,确保内存和类型安全。
Ref返回

对付那些不密切关注C#措辞更新的人来说,理解Span<T>实现的第一步是理解C# 7.0中引入的ref返回值。
虽然大多数读者都熟习通过引用通报方法参数,但现在C#许可返回对值的引用,而不是值本身。
让我们来看看它是如何事情的。我们将为一组精彩的音乐家创建一个大略的包装,它既展示了传统的行为,又展示了新的ref返回特性。
publicclassArtistsStore{privatereadonlystring[]_artists=new[]{"Amenra","TheShadowRing","HiroshiYoshimura"};publicstringReturnSingleArtist(){return_artists[1];}publicrefstringReturnSingleArtistByRef(){returnref_artists[1];}publicstringAllAritsts=>string.Join(",",_artists);}
现在我们调用这些方法。
varstore=newArtistsStore();varartist=store.ReturnSingleArtist();artist="HenryCow";varallArtists=store.AllAritsts;//Amenra,TheShadowRing,HiroshiYoshimuraartist=store.ReturnSingleArtistByRef();artist="FrankZappa";allArtists=store.AllAritsts;//Amenra,TheShadowRing,HiroshiYoshimurarefvarartistReference=refstore.ReturnSingleArtistByRef();artistReference="ValentynSylvestrov";allArtists=store.AllAritsts;//Amenra,ValentynSylvestrov,HiroshiYoshimura
把稳,在第一个和第二个示例中,原始凑集没有被修正。在末了一个例子中,我们成功地改变了这个凑集的第二位艺术家。在本文后面的过程中您将看到,这个有用的特性将帮助我们以类似引用的办法操作位于堆栈上的数组。
Ref构造
我们知道,值类型可以在堆栈上分配。而且,它们并不一定依赖于利用值的高下文。为了确保值总是分配在堆栈上,C# 7.0中引入了ref struct的观点。Span<t>是一个ref struct,以是我们确定它总是分配在堆栈上。
Span实现
Span<T>是一个引用构造,它包含一个指向内存的指针和类似于以下内容的跨度长度。
publicreadonlyrefstructSpan<T>{privatereadonlyrefT_pointer;privatereadonlyint_length;publicrefTthis[intindex]=>ref_pointer+index;...}
把稳指针字段附近的ref润色符无法在.NET Core中的普通C#中声明此类布局,而是通过ByReference <T>实现。
因此,如您所见,索引是通过ref return实现的,它许可仅堆栈构造的引用类型类似于行为。
span限定
为了确保引用构造始终在堆栈上利用,它具有许多限定; 即,它们不能被装箱,它们不能被分配给工具类型,动态类型或任何接口类型的变量,它们不能是引用类型中的字段,并且它们不能在await和yield中利用 边界。 其余,对两个方法Equals和GetHashCode的调用将引发NotSupportedException。 Span<T>是一个引用构造。
利用Span代替字符串
重做现有代码库。
让我们研究一下将Linux权限转换为八进制表示形式的代码。 您可以在这里访问它。 这是原始代码。
internalclassSymbolicPermission{privatestructPermissionInfo{publicintValue{get;set;}publiccharSymbol{get;set;}}privateconstintBlockCount=3;privateconstintBlockLength=3;privateconstintMissingPermissionSymbol='-';privatereadonlystaticDictionary<int,PermissionInfo>Permissions=newDictionary<int,PermissionInfo>(){{0,newPermissionInfo{Symbol='r',Value=4}},{1,newPermissionInfo{Symbol='w',Value=2}},{2,newPermissionInfo{Symbol='x',Value=1}}};privatestring_value;privateSymbolicPermission(stringvalue){_value=value;}publicstaticSymbolicPermissionParse(stringinput){if(input.Length!=BlockCountBlockLength){thrownewArgumentException("inputshouldbeastring3blocksof3characterseach");}for(vari=0;i<input.Length;i++){TestCharForValidity(input,i);}returnnewSymbolicPermission(input);}publicintGetOctalRepresentation(){varres=0;for(vari=0;i<BlockCount;i++){varblock=GetBlock(i);res+=ConvertBlockToOctal(block)(int)Math.Pow(10,BlockCount-i-1);}returnres;}privatestaticvoidTestCharForValidity(stringinput,intposition){varindex=position%BlockLength;varexpectedPermission=Permissions[index];varsymbolToTest=input[position];if(symbolToTest!=expectedPermission.Symbol&&symbolToTest!=MissingPermissionSymbol){thrownewArgumentException($"invalidinputinposition{position}");}}privatestringGetBlock(intblockNumber){return_value.Substring(blockNumberBlockLength,BlockLength);}privateintConvertBlockToOctal(stringblock){varres=0;foreach(var(index,permission)inPermissions){varactualValue=block[index];if(actualValue==permission.Symbol){res+=permission.Value;}}returnres;}}publicstaticclassSymbolicUtils{publicstaticintSymbolicToOctal(stringinput){varpermission=SymbolicPermission.Parse(input);returnpermission.GetOctalRepresentation();}}
推理非常大略:string是char的数组,所以为什么不将其分配在堆栈上而不是堆上。
因此,我们的紧张目标是将SymbolicPermission的字段_value标记为ReadOnlySpan <char>而不是字符串。 为此,我们必须将SymbolicPermission声明为引用构造,由于字段或属性的类型不能为Span <T>,除非它是引用构造的实例。
internalrefstructSymbolicPermission{...privateReadOnlySpan<char>_value;}
现在,我们只需将触及范围内的每个字符串变动为ReadOnlySpan <char>,唯一感兴趣的一点是GetBlock方法,由于在这里,我们将Substring更换为Slice。
privateReadOnlySpan<char>GetBlock(intblockNumber){return_value.Slice(blockNumberBlockLength,BlockLength);}
评价
我们来衡量结果。
我们把稳到速率提高了50纳秒,大约提高了10%的性能。有人可能会说,50纳秒并不多,但它险些没有花费我们来实现它!
现在,我们来评估一下利用18块12个字符的权限的改进,看看我们是否能得到显著的改进。
如您所见,我们得到了0.5微秒或5%的性能改进。再一次,这看起来可能是一个适度的造诣。但是记住,这是很随意马虎实现的。
利用span而不是数组
让我们扩展其他类型的数组。 考虑一下ASP.NET Channels管道中的示例。 下面代码背后的缘故原由是,数据常常通过网络以块的形式到达,这意味着该数据段可能同时驻留在多个缓冲区中。 在示例中,此类数据被解析为int。
publicunsafestaticuintGetUInt32(thisReadableBufferbuffer){ReadOnlySpan<byte>textSpan;if(buffer.IsSingleSpan){//ifdatainsinglebuffer,it’seasytextSpan=buffer.First.Span;}elseif(buffer.Length<128){//else,considertempbufferonstackvardata=stackallocbyte[128];vardestination=newSpan<byte>(data,128);buffer.CopyTo(destination);textSpan=destination.Slice(0,buffer.Length);}else{//elsepaythecostofallocatinganarraytextSpan=newReadOnlySpan<byte>(buffer.ToArray());}uintvalue;//yettheactualparsingroutineisalwaysthesameandsimpleif(!Utf8Parser.TryParse(textSpan,outvalue)){thrownewInvalidOperationException();}returnvalue;}
让我们来剖析一下这里发生了什么。我们的目标是将字节序列解析为uint。
if(!Utf8Parser.TryParse(textSpan,outvalue)){thrownewInvalidOperationException();}returnvalue;
现在让我们看看如何将输入参数添补到textSpan中。 输入参数是一个缓冲区的实例,可以读取一系列连续的字节。
ReadableBuffer继续自ISequence <ReadOnlyMemory <byte >>,这基本上意味着它由多个内存段组成。
如果缓冲区由单个段组成,我们只利用第一个段的根本Span。
if(buffer.IsSingleSpan){textSpan=buffer.First.Span;}
否则,我们在堆栈上分配数据并基于它创建Span <byte>。
vardata=stackallocbyte[128];vardestination=newSpan<byte>(data,128);
然后,我们利用方法buffer.CopyTo(destination)遍历缓冲区的每个内存段,并将其复制到目标Span。 之后,我们只对缓冲区长度的跨度进行切片。
此示例向我们展示了新的Span <T> API,使我们能够以比到达之前更方便的办法利用在堆栈上手动分配的内存。
结论
Span <T>为stackalloc供应了一种安全且易于利用的替代方法,可轻松提高性能。 Span <T>在.NET Core 3.0代码库中得到了广泛的利用,与以前的版本相比,它使我们在性能上得到了改进。
在决定是否应利用Span <T>时,您可能会考虑以下几点:
如果您的方法接管数据数组且未变动其大小。 如果您不修正输入,则可以考虑ReadOnlySpan <T>。如果您的方法接管用于计数某些统计信息或实行语法剖析的字符串,则应接管ReadOnlySpan <char>。如果您的方法返回一小段数据,则可以在Span <T>的帮助下返回Span <T> buf = stackalloc T [size]。 请记住,T该当是一个值类型。