站在前人的肩膀上重新透视C# Span数据结构

先谈一下我对Span的站前看法, Span是肩膀结构指向任意连续内存空间的类型安全、内存安全的上重视C数据视图。

Span和Memory都是新透包装了可以在pipeline上使用的结构化数据的内存缓冲器,他们被设计用于在pipeline中高效传递数据。

定语解读

这里面许多定语,站前值得我们细细揣摩:

1. 指向任意连续内存空间:支持托管堆,肩膀结构原生内存、上重视C数据堆栈,新透 这个可从Span的站前几个重载构造函数窥视一二。

2. 类型安全:Span 是肩膀结构一个泛型。

3. 内存安全: Span[1]是上重视C数据一个readonly ref struct数据结构,用于表征一段连续内存的关键属性被设置成只读readonly, 保证了所有的新透操作只能在这段内存内。

// 截取自Span源码

public readonly ref struct Span

{

// 表征一段连续内存的站前关键属性 Pointer & Length 都只能从构造函数赋值

///

A byref or a native ptr.

internal readonly ByReference_reference;

///

The number of elements this Span contains.

private readonly int _length;

[MethodImpl(MethodImplOptions.AggressiveInlining)]

public Span(T[]? array)

{

if (array == null)

{

this = default;

return; // returns default

}

if (!typeof(T).IsValueType && array.GetType() != typeof(T[]))

ThrowHelper.ThrowArrayTypeMismatchException();

_reference = new ByReference(ref MemoryMarshal.GetArrayDataReference(array));

_length = array.Length;

}

}

4. 视图:操作结果会直接体现到底层的连续内存。

至此我们来看一个简单的肩膀结构用法, 利用span操作指向一段堆栈空间。上重视C数据

static void Main()

{

SpanarraySpan = stackalloc byte[100]; // 包含指针和Length的只读指针, 类似于go里面的切片

byte data = 0;

for (int ctr = 0; ctr < arraySpan.Length; ctr++)

arraySpan[ctr] = data++;

arraySpan.Fill(1);

var arraySum = Sum(arraySpan);

Console.WriteLine($"The sum is { arraySum}"); // 输出100

arraySpan.Clear();

var slice = arraySpan.Slice(0,50); // 因为是只读属性,云服务器提供商 内部New Span<>(), 产生新的切片

arraySum = Sum(slice);

Console.WriteLine($"The sum is { arraySum}"); // 输出0

}

[MethodImpl(MethodImplOptions.AggressiveInlining)]

static int Sum(Spanarray)

{

int arraySum = 0;

foreach (var value in array)

arraySum += value;

return arraySum;

}此处Span 指向了特定的堆栈空间, Fill,Clear 等操作的效果直接体现到该段内存。注意Slice切片方法,内部实质是产生新的Span,是一个新的视图,对新span的操作会体现到原始底层数据结构。 [MethodImpl(MethodImplOptions.AggressiveInlining)]

public SpanSlice(int start)

{

if ((uint)start > (uint)_length)

ThrowHelper.ThrowArgumentOutOfRangeException();

return new Span(ref Unsafe.Add(ref _reference.Value, (nint)(uint)start /* force zero-extension */), _length - start);

}

从Slice切片源码可以看到,实质是利用原ptr & length 产生包含新的ptr & length的操作视图, ptr其实是指针的移动,也就是定位新的数据块, 但是终归是在原始数据块内部。

衍生技能点

我们再细看Span的定义, 有几个关键词建议大家温故而知新。

1. readonly strcut[2]

从C#7.2开始,你可以将readonly作用在struct上,高防服务器指示该struct不可改变。

span 被定义为readonly struct,内部属性自然也是readonly,从上面的分析和实例看我们可以针对Span表征的特定连续内存空间做内容更新操作;

如果想限制更新该连续内存空间的内容, C#提供了ReadOnlySpan类型, 该类型强调该块内存只读,也就是不存在Span 拥有的Fill,Clear等方法。

一线码农大佬写了文章讲述[使用span对字符串求和]的姿势,大家都说使用span能高效操作内存,我们对该用例BenchmarkDotNet压测。

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

using System.Buffers;

using System.Runtime.CompilerServices;

using BenchmarkDotNet.Attributes;

using BenchmarkDotNet.Running;

namespace ConsoleApp3

{

public class Program

{

static void Main()

{

var summary = BenchmarkRunner.Run();

}

}

[MemoryDiagnoser,RankColumn]

public class MemoryBenchmarkerDemo

{

int NumberOfItems = 100000;

// 对字符串切割, 会产生字符串小对象

[Benchmark]

public void StringSplit()

{

for (int i = 0; i < NumberOfItems; i++)

{

var s = "97 3";

var arr = s.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries);

var num1 = int.Parse(arr[0]);

var num2 = int.Parse(arr[1]);

_ = num1 + num2;

}

}

// 对底层字符串切片

[Benchmark]

public void StringSlice()

{

for (int i = 0; i < NumberOfItems; i++)

{

var s = "97 3";

var position = s.IndexOf( );

ReadOnlySpanspan = s.AsSpan();

var num1 = int.Parse(span.Slice(0, position));

var num2 = int.Parse(span.Slice(position));

_= num1+ num2;

}

}

}

}

压测解读:

对字符串运行时切分,不会利用驻留池,于是case1会分配大量小对象;

case2对底层字符串切片,虽然会产生不同的透视对象Span, 但是实际引用了的原始内存块的偏移区间, 不存在分配新内存。

2. ref struct[3]

从C#7.2开始,ref可以作用在struct,指示该类型被分配在堆栈上,云南idc服务商并且不能转义到托管堆。

Span,ReadonlySpan 包装了对于任意连续内存快的透视操作,但是只能被存储堆栈上,不适用于一些场景,例如异步调用,.NET Core 2.1为此新增了Memory[4] , ReadOnlyMemory, 可以被存储在托管堆上,这个暂时按下不表。

最后用一张图总结, 本文成文,感谢[ yi念之间 ]大佬参与讨论。

引用链接

[1] Span: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Span.cs

[2] readonly strcut: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct#readonly-struct

[3] ref struct: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct

[4] Memory: https://docs.microsoft.com/en-us/dotnet/standard/memory-and-spans/memory-t-usage-guidelines

IT科技类资讯
上一篇:一站式全包!联想TruScale服务助力并行科技解决上云麻烦
下一篇:大唐资本利用数智化破局 并实现和客户双赢