NativeBuferring,一种零分配的数据类型[下篇]
上文说到Unmanaged、BufferedBinary和BufferedString是NativeBuffering支持的三个基本数据类型,其实我们也可以说NativeBuffering只支持Unmanaged和IReadOnlyBufferedObject<T>两种类型,BufferedString、NativeBuffering和通过Source Generator生成的BufferedMessage类型,以及下面介绍的几种集合和字典类型,都实现了IReadOnlyBufferedObject<T>接口。
一、IReadOnlyBufferedObject<T>
二、集合
三、字典
四、为什么不直接返回接口?
一、IReadOnlyBufferedObject<T>
顾名思义,IReadOnlyBufferedObject<T>表示一个针对缓冲字节序列创建的只读数据类型。如下面的代码片段所示,该接口只定义了一个名为Parse的静态方法,意味着对于任何一个实现了该接口的类型,对应的实例都可以利用一个代表缓冲字节序列的NativeBuffer的对象进行创建。
public interface IReadOnlyBufferedObject<T> where T: IReadOnlyBufferedObject<T>
{
static abstract T Parse(NativeBuffer buffer);
} public unsafe readonly struct NativeBuffer
{
public byte[] Bytes { get; }
public void* Start { get; } public NativeBuffer(byte[] bytes, void* start)
{
Bytes = bytes ?? throw new ArgumentNullException(nameof(bytes));
Start = start;
} public NativeBuffer(byte[] bytes, int index = 0)
{
Bytes = bytes ?? throw new ArgumentNullException(nameof(bytes));
Start = Unsafe.AsPointer(ref bytes[index]);
}
}
由于IReadOnlyBufferedObject<T>是NativeBuffering支持的基础类型,而生成的BufferedMessage类型也实现了这个接口。通过这种“无限嵌套”的形式,我们可以定义一个具有任意结构的数据类型。比如我们具有如下这个表示联系人的Contact类型,我们需要利用它作为“源类型”生成对应BufferedMessage类型。
[BufferedMessageSource]
public partial class Contact
{
public Contact(string id, string name, Address address)
{
Id = id;
Name = name;
ShipAddress = address;
} public string Id { get; }
public string Name { get; }
public Address ShipAddress { get; }
} [BufferedMessageSource]
public partial class Address
{
public string Province { get; }
public string City { get; }
public string District { get; }
public string Street { get; }
public Address(string province, string city, string district, string street)
{
Province = province ?? throw new ArgumentNullException(nameof(province));
City = city ?? throw new ArgumentNullException(nameof(city));
District = district ?? throw new ArgumentNullException(nameof(district));
Street = street ?? throw new ArgumentNullException(nameof(street));
}
}
Contact具有Id、Name和ShipAddress 三个数据成员,ShipAddress 对应的Address又是一个复合类型,具有四个表示省、市、区和介绍的字符串类型成员。现在我们为Contact和Address这两个类型生成对应的ContactBufferedMessage和AddressBufferedMessage。
public unsafe readonly struct ContactBufferedMessage : IReadOnlyBufferedObject<ContactBufferedMessage>
{
public NativeBuffer Buffer { get; }
public ContactBufferedMessage(NativeBuffer buffer) => Buffer = buffer;
public static ContactBufferedMessage Parse(NativeBuffer buffer) => new ContactBufferedMessage(buffer);
public BufferedString Id => Buffer.ReadBufferedObjectField<BufferedString>(0);
public BufferedString Name => Buffer.ReadBufferedObjectField<BufferedString>(1);
public AddressBufferedMessage ShipAddress => Buffer.ReadBufferedObjectField<AddressBufferedMessage>(2);
} public unsafe readonly struct AddressBufferedMessage : IReadOnlyBufferedObject<AddressBufferedMessage>
{
public NativeBuffer Buffer { get; }
public AddressBufferedMessage(NativeBuffer buffer) => Buffer = buffer;
public static AddressBufferedMessage Parse(NativeBuffer buffer) => new AddressBufferedMessage(buffer);
public BufferedString Province => Buffer.ReadBufferedObjectField<BufferedString>(0);
public BufferedString City => Buffer.ReadBufferedObjectField<BufferedString>(1);
public BufferedString District => Buffer.ReadBufferedObjectField<BufferedString>(2);
public BufferedString Street => Buffer.ReadBufferedObjectField<BufferedString>(3);
}
如下的程序演示了如何将一个Contact对象转换成字节数组,然后利用这这段字节序列生成一个ContactBufferedMessage对象。给出的调试断言验证了Contact和ContactBufferedMessage对象承载了一样的数据,fixed关键字是为了将字节数组“固定住”。(源代码从这里下载)
using NativeBuffering;
using System.Diagnostics; var address = new Address("Jiangsu", "Suzhou", "Industory Park", "#328, Xinghu St");
var contact = new Contact("123456789", "John Doe", address);
var size = contact.CalculateSize();
var bytes = new byte[size];
var context = new BufferedObjectWriteContext(bytes);
contact.Write(context); unsafe
{
fixed (byte* _ = bytes)
{
var contactMessage = ContactBufferedMessage.Parse(new NativeBuffer(bytes));
Debug.Assert(contactMessage.Id == "123456789");
Debug.Assert(contactMessage.Name == "John Doe");
Debug.Assert(contactMessage.ShipAddress.Province == "Jiangsu");
Debug.Assert(contactMessage.ShipAddress.City == "Suzhou");
Debug.Assert(contactMessage.ShipAddress.District == "Industory Park");
Debug.Assert(contactMessage.ShipAddress.Street == "#328, Xinghu St");
}
}
二、集合
NativeBuffering同样支持集合。由于Unmanaged和IReadOnlyBufferedObject<T>是两种基本的数据类型,它们的根据区别在于:前者的长度有类型本身决定,是固定长度类型,后者则是可变长度类型。元素类型为Unmanaged和IReadOnlyBufferedObject<T>的集合分别通过ReadOnlyFixedLengthTypedList<T>和ReadOnlyVaraibleLengthTypedList<T>类型(结构体)表示,它们同样实现了IReadOnlyBufferedObject<T>接口。ReadOnlyFixedLengthTypedList<T>采用如下的字节布局:集合元素数量(4字节整数)+所有元素的字节内容(下图-上)。对于ReadOnlyVaraibleLengthTypedList<T>类型,我们会在前面为每个元素添加一个索引(4字节的整数),该索引指向目标元素在整个缓冲区的偏移量(下图-下)。
以如下所示的Entity为例,它具有两个数组类型的属性成员Collection1和Collection2,数组元素类型分别为Foobar和double,它们分别代表了上述的两种集合类型。
[BufferedMessageSource]
public partial class Entity
{
public Foobar[] Collection1 { get; }
public double[] Collection2 { get; }
public Entity(Foobar[] collection1, double[] collection2)
{
Collection1 = collection1;
Collection2 = collection2;
}
} [BufferedMessageSource]
public partial class Foobar
{
public int Foo { get; }
public string Bar { get; }
public Foobar(int foo, string bar)
{
Foo = foo;
Bar = bar;
}
}
NativeBuffering.Generator会将作为“源类型”的Entity和Foobar类型的生成对应的BufferedMessage类型(EntityBufferredMessage和FoobarBufferedMessage)。从EntityBufferredMessage类型的定义可以看出,两个集合属性的分别是ReadOnlyVariableLengthTypeList<FoobarBufferedMessage>和ReadOnlyFixedLengthTypedList<double>。
public unsafe readonly struct EntityBufferedMessage : IReadOnlyBufferedObject<EntityBufferedMessage>
{
public NativeBuffer Buffer { get; }
public EntityBufferedMessage(NativeBuffer buffer) => Buffer = buffer;
public static EntityBufferedMessage Parse(NativeBuffer buffer) => new EntityBufferedMessage(buffer);
public ReadOnlyVariableLengthTypeList<FoobarBufferedMessage> Collection1 => Buffer.ReadBufferedObjectCollectionField<FoobarBufferedMessage>(0);
public ReadOnlyFixedLengthTypedList<System.Double> Collection2 => Buffer.ReadUnmanagedCollectionField<System.Double>(1);
} public unsafe readonly struct FoobarBufferedMessage : IReadOnlyBufferedObject<FoobarBufferedMessage>
{
public NativeBuffer Buffer { get; }
public FoobarBufferedMessage(NativeBuffer buffer) => Buffer = buffer;
public static FoobarBufferedMessage Parse(NativeBuffer buffer) => new FoobarBufferedMessage(buffer);
public System.Int32 Foo => Buffer.ReadUnmanagedField<System.Int32>(0);
public BufferedString Bar => Buffer.ReadBufferedObjectField<BufferedString>(1);
}
两个集合类型都实现了IEnumerable<T>接口,还提供了索引。下面的代码演示了以索引的形式提取集合元素(源代码从这里下载)。
using NativeBuffering;
using System.Diagnostics; var entity = new Entity(
collection1: new Foobar[] { new Foobar(1, "foo"), new Foobar(2, "bar") },
collection2: new double[] { 1.1, 2.2 });
var bytes = new byte[entity.CalculateSize()];
var context = new BufferedObjectWriteContext(bytes);
entity.Write(context); unsafe
{
fixed (byte* p = bytes)
{
var entityMessage = EntityBufferedMessage.Parse(new NativeBuffer(bytes));
var foobar = entityMessage.Collection1[0];
Debug.Assert(foobar.Foo == 1);
Debug.Assert(foobar.Bar == "foo"); foobar = entityMessage.Collection1[1];
Debug.Assert(foobar.Foo == 2);
Debug.Assert(foobar.Bar == "bar"); Debug.Assert(entityMessage.Collection2[0] == 1.1);
Debug.Assert(entityMessage.Collection2[1] == 2.2);
}
}
三、字典
从数据的存储来看,字典就是键值对的集合,所以我们采用与集合一致的存储形式。NativeBuffering对集合的Key作了限制,要求其类型只能是Unmanaged和字符串(String/BufferredString)。按照Key和Value的类型组合,我们一共定义了四种类型的字典类型,它们分别是:
- ReadOnlyUnmanagedUnmanagedDictionary<TKey, TValue>:Key=Unmanaged; Value = Unmanaged
- ReadOnlyUnmanagedBufferedObjectDictionary<TKey, TValue>:Key=Unmanaged; Value = IReadOnlyBufferedObject<TValue>
- ReadOnlyStringUnmanagedDictionary<TValue>:Key=String/BufferredString; Value = Unmanaged
- ReadOnlyStringBufferedObjectDictionary<TValue>:Key=String/BufferredString; Value = IReadOnlyBufferedObject<TValue>
如果Key和Value的类型都是Unmanaged,键值对就是定长类型,所以我们会采用类似于ReadOnlyFixedLengthTypedList<T>的字节布局方式(下图-上),至于其他三种字典类型,则采用类似于ReadOnlyVaraibleLengthTypedList<T>的字节布局形式(下图-下)。
但是这仅仅解决了字段数据存储的问题,字典基于哈希检索定位的功能是没有办法实现的。这里我们不得不作出妥协,四种字典的索引均不能提供时间复杂度O(1)的哈希检索方式。为了在现有的数据结构上使针对Key的查找尽可能高效,在生成字节内容之前,我们会按照Key对键值对进行排序,这样我们至少可以采用二分法的形式进行检索,所以四种类型的字典的索引在根据指定的Key查找对应Value,对应的时间复杂度为Log(N)。如果字典包含的元素比较多,这样的查找方式不能满足我们的需求,我们可以I将它们转换成普通的Dictionary<TKey, TValue>类型,但是这就没法避免内存分配了。
我们照例编写一个简答的程序来演示针对字典的使用。我们定义了如下这个Entity作为“源类型”,它的四个属性对应的字典类型刚好对应上述四种键值对的组合。从生成的EntityBufferedMessage类型可以看出,四个成员的类型正好对应上述的四种字典类型。
[BufferedMessageSource]
public partial class Entity
{
public Dictionary<int, long> Dictionary1 { get; set; }
public Dictionary<int, string> Dictionary2 { get; set; }
public Dictionary<string, long> Dictionary3 { get; set; }
public Dictionary<string, string> Dictionary4 { get; set; }
} public unsafe readonly struct EntityBufferedMessage : IReadOnlyBufferedObject<EntityBufferedMessage>
{
public NativeBuffer Buffer { get; }
public EntityBufferedMessage(NativeBuffer buffer) => Buffer = buffer;
public static EntityBufferedMessage Parse(NativeBuffer buffer) => new EntityBufferedMessage(buffer);
public ReadOnlyUnmanagedUnmanagedDictionary<System.Int32, System.Int64> Dictionary1 => Buffer.ReadUnmanagedUnmanagedDictionaryField<System.Int32, System.Int64>(0);
public ReadOnlyUnmanagedBufferedObjectDictionary<System.Int32, BufferedString> Dictionary2 => Buffer.ReadUnmanagedBufferedObjectDictionaryField<System.Int32, BufferedString>(1);
public ReadOnlyStringUnmanagedDictionary<System.Int64> Dictionary3 => Buffer.ReadStringUnmanagedDictionaryField<System.Int64>(2);
public ReadOnlyStringBufferedObjectDictionary<BufferedString> Dictionary4 => Buffer.ReadStringBufferedObjectDictionaryField<BufferedString>(3);
}
如下的代码演示了基于四种字典类型基于“索引”的检索方式(源代码从这里下载)。
using NativeBuffering;
using System.Diagnostics; var entity = new Entity
{
Dictionary1 = new Dictionary<int, long> { { 1, 1 }, { 2, 2 }, { 3, 3 } },
Dictionary2 = new Dictionary<int, string> { { 1, "foo" }, { 2, "bar" }, { 3, "baz" } },
Dictionary3 = new Dictionary<string, long> { { "foo", 1 }, { "bar", 2 }, { "baz", 3 } },
Dictionary4 = new Dictionary<string, string> { { "a", "foo" }, { "b", "bar" }, { "c", "baz" } }
}; var bytes = new byte[entity.CalculateSize()];
var context = new BufferedObjectWriteContext(bytes);
entity.Write(context);
unsafe
{
fixed (void* _ = bytes)
{
var bufferedMessage = EntityBufferedMessage.Parse(new NativeBuffer(bytes)); ref var value1 = ref bufferedMessage.Dictionary1.AsRef(1);
Debug.Assert(value1 == 1);
ref var value2 = ref bufferedMessage.Dictionary3.AsRef("baz");
Debug.Assert(value2 == 3); var dictionary1 = bufferedMessage.Dictionary1;
Debug.Assert(dictionary1[1] == 1);
Debug.Assert(dictionary1[2] == 2);
Debug.Assert(dictionary1[3] == 3); var dictionary2 = bufferedMessage.Dictionary2;
Debug.Assert(dictionary2[1] == "foo");
Debug.Assert(dictionary2[2] == "bar");
Debug.Assert(dictionary2[3] == "baz"); var dictionary3 = bufferedMessage.Dictionary3;
Debug.Assert(dictionary3["foo"] == 1);
Debug.Assert(dictionary3["bar"] == 2);
Debug.Assert(dictionary3["baz"] == 3); var dictionary4 = bufferedMessage.Dictionary4;
Debug.Assert(dictionary4["a"] == "foo");
Debug.Assert(dictionary4["b"] == "bar");
Debug.Assert(dictionary4["c"] == "baz");
}
}
四、为什么不直接返回接口
针对集合,NativeBuffering提供了两种类型;针对字典,更是定义了四种类型,为什么不直接返回IList<T>/IDictionary<TKey,TValue>(或者IReadOnlyList<T>/IReadOnlyDictionary<TKey,TValue>)接口呢?这主要有两个原因,第一:为了尽可能地减少内存占用,我们将四种字典类型都定义成了结构体,如果使用接口的话会导致装箱;第二,四种字典类型的提供的API是有差异的,比如ReadOnlyFixedLengthTypedList<T> 和ReadOnlyUnmanagedUnmanagedDictionary<TKey, TValue>都提供了一个额外的AsRef方法,它直接返回值的引用(只读)。如果这个值被定义成一个成员较多的结构体,传引用的方式可以避免较多的拷贝。
public readonly unsafe struct ReadOnlyFixedLengthTypedList<T> : IReadOnlyList<T>, IReadOnlyBufferedObject<ReadOnlyFixedLengthTypedList<T>>
where T: unmanaged
{
public readonly ref T AsRef(int index);
...
} public unsafe readonly struct ReadOnlyUnmanagedUnmanagedDictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue>, IReadOnlyBufferedObject<ReadOnlyUnmanagedUnmanagedDictionary<TKey, TValue>>
where TKey : unmanaged, IComparable<TKey>
where TValue : unmanaged
{
public readonly ref TValue AsRef(TKey index) ;
...
}
NativeBuferring,一种零分配的数据类型[下篇]的更多相关文章
- 深入剖析Linux IO原理和几种零拷贝机制的实现
深入剖析Linux IO原理和几种零拷贝机制的实现 来源 https://zhuanlan.zhihu.com/p/83398714 零壹技术栈 公众号[零壹技术栈] 前言 零拷贝(Zero ...
- [转]详述DHCP服务器的三种IP分配方式
DHCP就是动态主机配置协议(Dynamic Host Configuration Protocol),它的目的就是为了减轻TCP/IP网络的规划.管理和维护的负担,解决IP地址空间缺乏问题.这种网络 ...
- Nginx upstream的5种权重分配方式【转】
原文地址:Nginx upstream的5种权重分配方式 1.轮询(默认) 每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除. 2.weight指定轮询几率,weig ...
- Rust之路(3)——数据类型 下篇
[未经书面同意,严禁转载] -- 2020-10-14 -- 架构是道,数据是术.道可道,非常道:术不名,不成术!道无常形,术却可循规. 学习与分析数据类型,最基本的方法就是搞清楚其存储原理,变量和对 ...
- js基础 三种弹出框 数据类型
总结:js三个组成部分ES:语法DOM:对象模型 => 通过js代码与页面文档(出现在body中的所有可视化标签)进行交互BOM:对象模型 => 通过js代码与浏览器自带功能进行交互 引入 ...
- C++三种内存分配方式
从静态存储区域分配:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在.例如全局变量,static变量.静态分配的区域的生命期是整个软件运行期,就是说从软件运行开始到软件终止退出.只 ...
- Nginx upstream的5种权重分配方式
.轮询(默认) 每个请求按时间顺序逐一分配到不同的后端服务器,后端服务器down掉,能自动剔除 .weight 指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况. upstre ...
- Nginx upstream的5种权重分配方式分享
Nginx负载均衡的分发方式有4种: 1.轮询,默认采取此方式,Nginx会按照请求时间的先后顺序进行轮询分发,若某台Web Server宕机,Nginx自动将其摘掉. 2.weight,权重,即轮询 ...
- Nginx upstream的5种权重分配方式(转)
出处:http://www.cnblogs.com/funsion/p/4003499.html?utm_source=tuicool 1.轮询(默认) 每个请求按时间顺序逐一分配到不同的后端服务器, ...
- JavaScript8种数据类型
一.开门见山 在ES5的时候,我们认知的数据类型确实是 6种:Number.String.Boolean.undefined.object.Null. ES6 中新增了一种 Symbol .这种类型的 ...
随机推荐
- Win Pycharm + Airtest + 夜神模拟器 实现APP自动化
前言: 前面已经讲过了Airtest的简单配置与使用了,相信大家已经对于操作Airtest没有什么问题了(#^.^#) 但是在Airtest IDE中编写代码是有局限性的,而且不能封装Airtest的 ...
- selenium web控件的交互进阶
Action ActionChains: 执行PC端的鼠标点击,双击,右键,拖曳等事件 TouchActions: 模拟PC和移动端的点击,滑动,拖曳,多点触控等多种手势操作 动作链接 ActionC ...
- 2023-04-02:设计一个仓库管理器,提供如下的方法: 1) void supply(String item, int num, int price) 名字叫item的商品,个数num,价格pri
2023-04-02:设计一个仓库管理器,提供如下的方法: void supply(String item, int num, int price) 名字叫item的商品,个数num,价格price. ...
- 2023-03-13:给定一个整数数组 A,坡是元组 (i, j),其中 i < j 且 A[i] <= A[j], 这样的坡的宽度为 j - i。 找出 A 中的坡的最大宽度,如果不存在,返回 0
2023-03-13:给定一个整数数组 A,坡是元组 (i, j),其中 i < j 且 A[i] <= A[j], 这样的坡的宽度为 j - i. 找出 A 中的坡的最大宽度,如果不存在 ...
- 2022-02-20:设计内存文件系统。 设计一个内存文件系统,模拟以下功能: ls: 以字符串的格式输入一个路径。如果它是一个文件的路径,那么函数返回一个列表,仅包含这个文件的名字。如果它是一个文件
2022-02-20:设计内存文件系统. 设计一个内存文件系统,模拟以下功能: ls: 以字符串的格式输入一个路径.如果它是一个文件的路径,那么函数返回一个列表,仅包含这个文件的名字.如果它是一个文件 ...
- 2021-12-18:找到字符串中所有字母异位词。 给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。 异位词 指由相同字母重排列形成
2021-12-18:找到字符串中所有字母异位词. 给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引.不考虑答案输出的顺序. 异位词 指由相同字母重排列形成 ...
- 2021-11-25:给定两个字符串s1和s2,返回在s1中有多少个子串等于s2。来自美团。
2021-11-25:给定两个字符串s1和s2,返回在s1中有多少个子串等于s2.来自美团. 答案2021-11-25: 改写kmp算法. next数组多求一位. 比如:str2 = aaaa, 那么 ...
- 【Python笔记】第二章Python基本图形绘制
嗨你好,我是AllenMi, 这是我学习北京理工大学的<Python语言程序设计>第二章笔记. 写笔记的目的一方面在于记录自己一步一步学习Python的内容, 另一方面也希望能够帮助到他人 ...
- Kubernetes GoRoutineMap工具包代码详解
1.概述 GoRoutineMap 定义了一种类型,可以运行具有名称的 goroutine 并跟踪它们的状态.它防止创建具有相同名称的多个goroutine,并且在上一个具有该名称的 goroutin ...
- ABP - 缓存模块(1)
1. 与 .NET Core 缓存的关系和差异 ABP 框架中的缓存系统核心包是 Volo.Abp.Caching ,而对于分布式缓存的支持,abp 官方提供了基于 Redis 的方案,需要安装 Vo ...