NativeBuffering,进一步提升字符串的序列化性能
在《NativeBuffering,一种高性能、零内存分配的序列化解决方案[性能测试篇]》我比较了NativeBuffering和System.Text.Json两种序列化方式的性能,通过性能测试结果可以看出NativeBuffering具有非常明显的优势,有的方面的性能优势甚至是“碾压式”的,唯独针对字符串的序列化性能不够理想。我趁这个周末对此做了优化,解决了这块短板,接下来我们就来看看最新的性能测试结果和背后“加速”的原理。
一、新版的性能测试结果
我使用《NativeBuffering,一种高性能、零内存分配的序列化解决方案[性能测试篇]》提供的测试用例,选用的依然是如下这个Person类型,它的绝大部分数据成员都是字符串。
[BufferedMessageSource]
public partial class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string[] Hobbies { get; set; }
public string Address { get; set; }
public string PhoneNumber { get; set; }
public string Email { get; set; }
public string Gender { get; set; }
public string Nationality { get; set; }
public string Occupation { get; set; }
public string EducationLevel { get; set; }
public string MaritalStatus { get; set; }
public string SpouseName { get; set; }
public int NumberOfChildren { get; set; }
public string[] ChildrenNames { get; set; }
public string[] LanguagesSpoken { get; set; }
public bool HasPets { get; set; }
public string[] PetNames { get; set; } public static Person Instance = new Person
{
Name = "Bill",
Age = 30,
Hobbies = new string[] { "Reading", "Writing", "Coding" },
Address = "123 Main St.",
PhoneNumber = "555-555-5555",
Email = "bill@gmail.com",
Gender = "M",
Nationality = "China",
Occupation = "Software Engineer",
EducationLevel = "Bachelor's",
MaritalStatus = "Married",
SpouseName = "Jane",
NumberOfChildren = 2,
ChildrenNames = new string[] { "John", "Jill" },
LanguagesSpoken = new string[] { "English", "Chinese" },
HasPets = true,
PetNames = new string[] { "Fido", "Spot" }
};
}
这是采用的测试案例。Benchmark方法SerializeAsJson直接将静态字段Instance表示的Person对象序列化成JSON字符串,采用NativeBuffering的Benchmark方法SerializeAsNativeBuffering直接调用WriteTo扩展方法(通过Source Generator生成)对齐进行序列化,并利用一个ArraySegment<T>结构返回序列化结果。WriteTo方法具有一个类型为Func<int, byte[]>的参数,我们使用它来提供一个存放序列化结果的字节数组。作为Func<int, byte[]>输入参数的整数代表序列化结果的字节长度,这样我们才能确保提供的字节数组具有充足的存储空间。
[MemoryDiagnoser]
public class Benchmark
{
private static readonly Func<int, byte[]> _bufferFactory = ArrayPool<byte>.Shared.Rent; [Benchmark]
public string SerializeAsJson() => JsonSerializer.Serialize(Person.Instance); [Benchmark]
public void SerializeNativeBuffering()
{
var arraySegment = Person.Instance.WriteTo(_bufferFactory);
ArrayPool<byte>.Shared.Return(arraySegment.Array!);
}
}
这是上一个版本的测试结果,虽然NativeBuffering具有“零内存分配”的巨大优势,但是在耗时上会多一些。造成这个劣势的主要原因来源于针对字符串的编码,因为NativeBuffering在序列化过程需要涉及两次编码,一次是为了计算总的字节数,另一次才是生成序列化结果。

如果切换到目前最新版本(0.1.5),可以看出NativeBuffering的性能已经得到了极大的改善,并且明显优于JSON序列化的性能(对于JSON序列化,两次测试具体的耗时之所以具有加大的差异,是因为测试机器配置不同,12代和13代i7的差异)。而在内存分配层面,针对NativeBuffering的序列化依然是“零分配”。

二、背后的故事
接下来我们就来简单说明一下为什么NativeBuffering针对字符串的序列化明显优于JSON序列化,这要从BufferedString这个自定义的结构说起。如下所示的就是Source Generator为Person类型生成的BufferedMessage类型,可以看出它的原有的字符串类型的成员在此类型中全部转换成了BufferedString类型的只读属性。
public unsafe readonly struct PersonBufferedMessage : IReadOnlyBufferedObject<PersonBufferedMessage>
{
public static PersonBufferedMessage DefaultValue => throw new NotImplementedException();
public NativeBuffer Buffer { get; }
public PersonBufferedMessage(NativeBuffer buffer) => Buffer = buffer;
public static PersonBufferedMessage Parse(NativeBuffer buffer) => new PersonBufferedMessage(buffer);
public BufferedString Name => Buffer.ReadNonNullableBufferedObjectField<BufferedString>(0);
public System.Int32 Age => Buffer.ReadUnmanagedField<System.Int32>(1);
public ReadOnlyNonNullableBufferedObjectList<BufferedString> Hobbies => Buffer.ReadNonNullableBufferedObjectCollectionField<BufferedString>(2);
public BufferedString Address => Buffer.ReadNonNullableBufferedObjectField<BufferedString>(3);
public BufferedString PhoneNumber => Buffer.ReadNonNullableBufferedObjectField<BufferedString>(4);
public BufferedString Email => Buffer.ReadNonNullableBufferedObjectField<BufferedString>(5);
public BufferedString Gender => Buffer.ReadNonNullableBufferedObjectField<BufferedString>(6);
public BufferedString Nationality => Buffer.ReadNonNullableBufferedObjectField<BufferedString>(7);
public BufferedString Occupation => Buffer.ReadNonNullableBufferedObjectField<BufferedString>(8);
public BufferedString EducationLevel => Buffer.ReadNonNullableBufferedObjectField<BufferedString>(9);
public BufferedString MaritalStatus => Buffer.ReadNonNullableBufferedObjectField<BufferedString>(10);
public BufferedString SpouseName => Buffer.ReadNonNullableBufferedObjectField<BufferedString>(11);
public System.Int32 NumberOfChildren => Buffer.ReadUnmanagedField<System.Int32>(12);
public ReadOnlyNonNullableBufferedObjectList<BufferedString> ChildrenNames => Buffer.ReadNonNullableBufferedObjectCollectionField<BufferedString>(13);
public ReadOnlyNonNullableBufferedObjectList<BufferedString> LanguagesSpoken => Buffer.ReadNonNullableBufferedObjectCollectionField<BufferedString>(14);
public System.Boolean HasPets => Buffer.ReadUnmanagedField<System.Boolean>(15);
public ReadOnlyNonNullableBufferedObjectList<BufferedString> PetNames => Buffer.ReadNonNullableBufferedObjectCollectionField<BufferedString>(16);
}
BufferedString在NativeBuffering中用来表示字符串。如代码片段所示,BufferedString 同样实现了IReadOnlyBufferedObject<BufferedString>接口,以为着它也是对一段字节序列的封装。BufferedString提供了针对字符串类型的隐式转换,所以我们在编程的时候可以将它当成普通字符串来使用。
public unsafe readonly struct BufferedString : IReadOnlyBufferedObject<BufferedString>
{
public static BufferedString DefaultValue { get; }
static BufferedString()
{
var size = CalculateStringSize(string.Empty);
var bytes = new byte[size]; var context = BufferedObjectWriteContext.Create(bytes);
context.WriteString(string.Empty);
DefaultValue = new BufferedString(new NativeBuffer(bytes));
}
public BufferedString(NativeBuffer buffer) => _start = buffer.Start;
public BufferedString(void* start) => _start = start; [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BufferedString Parse(NativeBuffer buffer) => new(buffer); [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BufferedString Parse(void* start) => new(start); [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int CalculateSize(void* start) => Unsafe.Read<int>(start); [MethodImpl(MethodImplOptions.AggressiveInlining)]
public string AsString()
{
string v = default!;
Unsafe.Write(Unsafe.AsPointer(ref v), new IntPtr(Unsafe.Add<byte>(_start, IntPtr.Size * 2)));
return v;
} [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator string(BufferedString value) => value.AsString(); public override string ToString() => AsString(); [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int CalculateStringSize(string? value)
{
var byteCount = value is null ? 0 : Encoding.Unicode.GetByteCount(value);
var size = _headerByteCount + byteCount;
return Math.Max(IntPtr.Size * 3 + sizeof(int), size);
} private static readonly int _headerByteCount = sizeof(nint) + sizeof(nint) + sizeof(nint) + sizeof(int);
}
值得一提的是,BufferedString向String的类型转换是没有任何开销的,这一切源自它封装的这段字节序列的结构。我曾经在《你知道.NET的字符串在内存中是如何存储的吗?》中介绍过字符串对象自身在内存中的布局,而BufferedString封装的字节序列就是在这段内容加上前置的4/8个字节(x84为4字节,x64需要添加4字节Padding确保内存对齐)来表示总的字节数。当BufferedString转换成String类型时,只需要将返回的字符串变量指向TypeHandle部分的地址就可以了,这一点体现在上述的AsString方法上。

也正是因为NativeBuffering在序列化字符串的时候,生成的字节序列与字符串对象的内存布局一致,所以不在需要对字符串进行编码,直接按照如下所示的方式进行内存拷贝就可以了。这正是NativeBuffering针对字符串的序列化的性能得以提升的原因,不过整个序列化过程中还是需要计算字符串针对默认编码(Unicode)的字节长度。

NativeBuffering,进一步提升字符串的序列化性能的更多相关文章
- .net core Json字符串的序列化和反序列化通用类源码,并模拟了10万数据对比DataContractJsonSerializer和Newtonsoft性能
我们在开发中Json传输数据日益普遍,有很多关于Json字符串的序列化和反序列化的文章大多都告诉你怎么用,但是却不会告诉你用什么更高效.因为有太多选择,人们往往会陷入选择难题. 相比.NET Fram ...
- 痞子衡嵌入式:链接函数到8字节对齐地址或可进一步提升i.MXRT内核执行性能
大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家分享的是i.MXRT上进一步提升代码执行性能的经验. 今天跟大家聊的这个话题还是跟痞子衡最近这段时间参与的一个基于i.MXRT1170的大项目有 ...
- dotnet 6 使用 string.Create 提升字符串创建和拼接性能
本文告诉大家,在 dotnet 6 或更高版本的 dotnet 里,如何使用 string.Create 提升字符串创建和拼接的性能,减少拼接字符串时,需要额外申请的内存,从而减少内存回收压力 本文也 ...
- [转载]Hibernate如何提升数据库查询的性能
目录(?)[-] 数据库查询性能的提升也是涉及到开发中的各个阶段在开发中选用正确的查询方法无疑是最基础也最简单的 SQL语句的优化 使用正确的查询方法 使用正确的抓取策略 Hibernate的性能优化 ...
- 自适应查询执行:在运行时提升Spark SQL执行性能
前言 Catalyst是Spark SQL核心优化器,早期主要基于规则的优化器RBO,后期又引入基于代价进行优化的CBO.但是在这些版本中,Spark SQL执行计划一旦确定就不会改变.由于缺乏或者不 ...
- 用 JMH 检测 Lambdas 序列化性能
本文将介绍如何进行 Java Lambdas 序列化性能检测.Lambdas 的重要性以及 Lambdas 在分布式系统中的应用. Lambdas 表达式是 Java 8 中万众期待的新特性,其若干用 ...
- 智能SQL优化工具--SQL Optimizer for SQL Server(帮助提升数据库应用程序性能,最大程度地自动优化你的SQL语句 )
SQL Optimizer for SQL Server 帮助提升数据库应用程序性能,最大程度地自动优化你的SQL语句 SQL Optimizer for SQL Server 让 SQL Serve ...
- 怎样提升 RailS 应用的性能?
Is rails slow? 「铁路非常慢」,你或许听过这个笑话,那么我们的 Rails 框架呢? 假设说 Rails 慢,那么怎样提升 Rails APP 的性能就成了开发人员们最关注的问题. 或许 ...
- 各种Java序列化性能比较
转载:http://www.jdon.com/concurrent/serialization.html 这里比较Java对象序列化 XML JSON Kryo POF等序列化性能比较. 很多人以 ...
- 只为粗暴看一下ES6的字符串模板的性能
网上查找"ES6 字符串模板 +性能"5分钟无果遂写了一个暴力测试. 测试对象: +=方式,字符串累加计算方式 +s1+s2...+sn方式,即传统连加拼接字符串方式 s.push ...
随机推荐
- 国产化之x64平台安装银河麒麟操作系统
背景 某个项目需要实现基础软件全部国产化,其中操作系统指定银河麒麟v4,CPU使用飞腾处理器.飞腾处理器是ARMv8架构的,在之前的文章中介绍了使用QEMU模拟ARMv8架构安装银河麒麟操作系统的方式 ...
- typedef和define有什么区别
typedef 和define 都是替一个对象取一个别名,以此增强程序的可读性,区别如下: 使用不用 define 定义后面不用加分号,并且它的别名在对象的前面 typedef需要加分号,并且它的别名 ...
- salesforce零基础学习(一百二十九)Lead Convertion 有趣的经历
本篇参考:https://help.salesforce.com/s/articleView?id=000382564&type=1 Lead Convertion 是salesforce中s ...
- 2021-3-29 Enter按下事件
先在构造器中添加keydown事件 tBoxPsw.KeyDown += TBoxPsw_KeyDown; 在事件中添加按下enter按钮所触发的方法 private void TBoxPsw_Key ...
- 学习狂神Spring5_课堂笔记(更新中)
Spring 简介 spring,英文单词为春天,表示为软件行业带来了春天. 2002年,首次推出了Spring框架的雏形:interface21框架.官网为:www.interface21.io但是 ...
- opencv-python中 boundingRect(cnt)以及cv2.rectangle用法
矩形边框(Bounding Rectangle)是说,用一个最小的矩形,把找到的形状包起来.还有一个带旋转的矩形,面积会更小,效果见下图 首先介绍下cv2.boundingRect(img)这个函数 ...
- 《最新出炉》系列初窥篇-Python+Playwright自动化测试-11-playwright操作iframe-上篇
1.简介 原估计宏哥这里就不对iframe这个知识点做介绍和讲解了,因为前边的窗口切换就为这种网页处理提供了思路,另一个原因就是虽然iframe很强大,但是现在很少有网站用它了.但是还是有小伙伴或者童 ...
- 【RocketMQ】MQ消息发送总结
RocketMQ是通过DefaultMQProducer进行消息发送的,它实现了MQProducer接口,MQProducer接口中定义了消息发送的方法,方法主要分为三大类: send同步进行消息发送 ...
- 【Qt6】工具提示以及调色板设置
工具提示即 Tool Tip,当用户把鼠标移动到某个UI对象上并悬停片刻,就会出现一个"短小精悍"的窗口,显示一些说明性文本.一般就是功能描述,让用户知道这个XX是干啥用的. 在 ...
- 一次Java内存占用高的排查案例,解释了我对内存问题的所有疑问
原创:扣钉日记(微信公众号ID:codelogs),欢迎分享,非公众号转载保留此声明. 问题现象 7月25号,我们一服务的内存占用较高,约13G,容器总内存16G,占用约85%,触发了内存报警(阈值8 ...