重新审视C# Span<T>数据结构
先谈一下我对Span的看法, span是指向任意连续内存空间的类型安全、内存安全的视图。
Span和Memory都是包装了可以在pipeline上使用的结构化数据的内存缓冲器,他们被设计用于在pipeline中高效传递数据。
定语解读
- 指向任意连续内存空间: 支持托管堆,原生内存、堆栈, 这个可从Span的几个重载构造函数窥视一二。
- 类型安全: Span 是一个泛型
- 内存安全: 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;
}
}
- 视图:操作结果会直接体现在底层的连续内存。
至此我们来看一个简单的用法, 利用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, 可以被存储在托管堆上, 按下不表。
最后用一张图总结
重新审视C# Span<T>数据结构的更多相关文章
- Redis学习笔记一:数据结构与对象
1. String(SDS) Redis使用自定义的一种字符串结构SDS来作为字符串的表示. 127.0.0.1:6379> set name liushijie OK 在如上操作中,name( ...
- C++数据结构之链式队列(Linked Queue)
C++数据结构之链式队列,实现的基本思想和链式栈的实现差不多,比较不同的一点也是需要注意的一点是,链式队列的指向指针有两个,一个是队头指针(front),一个是队尾指针(rear),注意指针的指向是从 ...
- 【数据结构】之二叉树的java实现
转自:http://blog.csdn.net/wuwenxiang91322/article/details/12231657 二叉树的定义: 二叉树是树形结构的一个重要类型.许多实际问题抽象出来的 ...
- JAVA学习笔记 -- 数据结构
一.数据结构的接口 在Java中全部类的鼻祖是Object类,可是全部有关数据结构处理的鼻祖就是Collection和Iterator接口,也就是集合与遍历. 1.Collection接口 Colle ...
- MySQL列:innodb的源代码的分析的基础数据结构
在过去的一年中的数据库相关的源代码分析.前段时间分析levelDB实施和BeansDB实现,数据库网络分析这两篇文章非常多.他们也比较深比较分析,所以没有必要重复很多劳力.MYSQL,当然主要还是数据 ...
- 深入浅出Redis-redis底层数据结构(下)
概述: 学习使用Redis,其实并不需要去研究其底层数据的实现.我们只需要了解他有哪些常用的数据类型,然后熟练使用,就可以很好的掌握Redis 这个工具了.但是这样的学习方法只适合Redis 的入门, ...
- Redis中的基本数据结构
Redis基础数据结构 基础数据结构 sds简单动态字符串 数据结构 typedef struct sdstr{ int len // 字符串分配的字节 int free // 未使用的字节数 cha ...
- Redis 基础数据结构与对象
Redis用到的底层数据结构有:简单动态字符串.双端链表.字典.压缩列表.整数集合.跳跃表等,Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包 ...
- Redis数据结构底层知识总结
Redis数据结构底层总结 本篇文章是基于作者黄建宏写的书Redis设计与实现而做的笔记 数据结构与对象 Redis中数据结构的底层实现包括以下对象: 对象 解释 简单动态字符串 字符串的底层实现 链 ...
随机推荐
- MariaDB数据库设置用户密码
SET PASSWORD [FOR user] = { PASSWORD('some password') | OLD_PASSWORD('some password') | 'encrypted p ...
- 一个 Redis 实例最多能存放多少的 keys?List、Set、 Sorted Set 他们最多能存放多少元素?
理论上 Redis 可以处理多达 232 的 keys,并且在实际中进行了测试,每个实 例至少存放了 2 亿 5 千万的 keys.我们正在测试一些较大的值.任何 list.set. 和 sorted ...
- 转:C++11常用新特性快速一览
转载至:https://blog.csdn.net/jiange_zh/article/details/79356417 1.nullptr nullptr 出现的目的是为了替代 NULL. 在某种意 ...
- @Qualifier 注解?
当有多个相同类型的 bean 却只有一个需要自动装配时,将@Qualifier 注解和 @Autowire 注解结合使用以消除这种混淆,指定需要装配的确切的 bean.
- 学习openstack(三)
一.OpenStack初探 1.1 OpenStack简介 OpenStack是一整套开源软件项目的综合,它允许企业或服务提供者建立.运行自己的云计算和存储设施.Rackspace与NASA是最初 ...
- IdentityServer4系列 | 混合模式
一.前言 在上一篇关于授权码模式中, 已经介绍了关于授权码的基本内容,认识到这是一个拥有更为安全的机制,但这个仍然存在局限,虽然在文中我们说到通过后端的方式去获取token,这种由web服务器和授权服 ...
- JS:数组中push对象,覆盖问题
发现将对象push进数组,后面的值会覆盖前面的值,最后输出的都是最后一次的值.其实这一切都是引用数据类型惹的祸.如果你也有类似问题,可以继续看下去哦.下面代码模拟:将json对象的每个键值对,单独搞成 ...
- Python:爬取中国各市的疫情数据并存储到数据库
import requests import pymysql import json def create(): # 连接数据库 db = pymysql.connect(host = 'localh ...
- M1芯片使用cocoapods 报错[!] Oh no, an error occurred
[解决方式] 命令行1(编译): sudo arch -x86_64 gem install ffi 命令行2(安装): arch -x86_64 pod install 原出处:https://gi ...
- Mybatis插入数据
对上文->Mybatis快速入门-<进行代码修改 1.在UserMapper.xml中添加插入操作 <!-- 插入操作--> <insert id="save& ...