先谈一下我对Span的看法, span是指向任意连续内存空间的类型安全、内存安全的视图。

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

定语解读

  1. 指向任意连续内存空间: 支持托管堆,原生内存、堆栈, 这个可从Span的几个重载构造函数窥视一二。
  2. 类型安全: Span 是一个泛型
  3. 内存安全Span是一个readonly ref struct数据结构, 用于表征一段连续内存的关键属性被设置成只读readonly, 保证了所有的操作只能在这段内存内。
// 截取自Span源码,表征一段连续内存的关键属性 Pointer & Length 都只能从构造函数赋值
public readonly ref struct Span<T>
{
/// <summary>A byref or a native ptr.</summary>
internal readonly ByReference<T> _reference;
/// <summary>The number of elements this Span contains.</summary>
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<T>(ref MemoryMarshal.GetArrayDataReference(array));
_length = array.Length;
}
}
  1. 视图:操作结果会直接体现在底层的连续内存。

至此我们来看一个简单的用法, 利用span操作指向一段堆栈空间。

static  void  Main()
{ Span<byte> arraySpan = 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(Span<byte> array)
{
int arraySum = 0;
foreach (var value in array)
arraySum += value; return arraySum;
}
  • 此处Span 指向了特定的堆栈空间, Fill,Clear 等操作的效果直接体现到该段内存。
  • 注意Slice切片方法,内部实质是产生新的Span,也是一个新的视图,对新span的操作会体现到原始底层数据结构。
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<T> Slice(int start)
{
if ((uint)start > (uint)_length)
ThrowHelper.ThrowArgumentOutOfRangeException(); return new Span<T>(ref Unsafe.Add(ref _reference.Value, (nint)(uint)start /* force zero-extension */), _length - start);
}

从Slice切片源码,看到利用现有的ptr 和length,产生了新的操作视图,ptr的计算有赖于原ptr移动指针,但是依旧是作用在原始数据块上。

衍生技能点

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

  • readonly strcut :从C#7.2开始,你可以将readonly作用在struct上,指示该struct不可改变

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

如果想限制更新该连续内存空间的内容, C#提供了ReadOnlySpan<T>类型, 该类型强调该块内存只读,也就是不存在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<MemoryBenchmarkerDemo>();
}
} [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(' ');
ReadOnlySpan<char> span = s.AsSpan();
var num1 = int.Parse(span.Slice(0, position));
var num2 = int.Parse(span.Slice(position)); _= num1+ num2; }
}
}
}

解读:

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

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

  • ref struct:从C#7.2开始,ref可以作用在struct,指示该类型被分配在堆栈上,并且不能转义到托管堆

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

最后用一张图总结

最新文章

  1. 手把手教从零开始在GitHub上使用Hexo搭建博客教程(三)-使用Travis自动部署Hexo(1)
  2. Android随笔之——模拟按键操作的几种方式
  3. Datatables 在asp.net mvc中的使用
  4. GO RPC
  5. Linux设置FQDN
  6. nagios 完全配置手册
  7. C#进阶系列——使用Advanced Installer制作IIS安装包(一:配置IIS和Web.config)
  8. 如何定制 Calico 的 IP 池?- 每天5分钟玩转 Docker 容器技术(71)
  9. Again Prime? No Time.(uva10870+数论)
  10. CrackMe005-下篇 | 逆向破解分析 | 160个CrackMe(视频+图文)深度解析系列
  11. java中的基本数据类型一定存储在栈中吗?
  12. c++中关于用stringstream进行的类型转化
  13. vue脚手架 构建豆瓣App 第一天
  14. Kinect2.0相机标定
  15. CAS实现单点登录SSO执行原理探究超详细
  16. 廖雪峰Java6 IO编程-2input和output-5操作zip
  17. git clone远程branch和tag
  18. 最小树形图(hdu4009)
  19. 【Python】实现将testlink上的用例指定格式保存至Excel,用于修改上传
  20. MySQL修改max_allowed_packet

热门文章

  1. 学习Keepalived(二)
  2. 外部晶振的使用原因与内部RC振荡器的使用方法
  3. javascript新手实例1-DOM基本操作
  4. 字符串反转&amp;说反话
  5. 拼写检查-c++
  6. 常⽤的meta标签有哪些
  7. Java 实例 - 读取文件内容
  8. JavaScript 字符串(String)对象的方法
  9. String相关API-getBean()方法的使用
  10. LC-349