使用动态程序集在 C# 中快速读取本机结构






4.77/5 (13投票s)
本文展示了如何生成动态方法以实现快速的字节到结构转换。
简介
本文演示了如何使用动态生成的代码将字节转换为用户定义的数据结构。
Sasha Goldshtein 写了一篇关于这个主题的优秀文章,分析了从字节数组读取用户定义的结构体的各种方法。本文基于他的工作,并提出了使用代码生成的更快、更通用的替代方案。附带的代码包括 Sasha 的原始代码和一个开源工具包,该工具包有助于代码生成。
背景
Sasha 的文章中展示的最快的解决方案是为非泛型类型使用 fixed
关键字:
static unsafe Packet ReadUsingPointer(byte[] data)
{
fixed (byte* packet = &data[0])
{
return *(Packet*)packet;
}
}
为了使其真正有用,我们需要一个泛型方法
static T Packet ReadUsingPointer<T>(byte[] data)
{
fixed (byte* packet = &data[0])
{
return *(T*)packet; // Would not compile
}
}
不幸的是,由于 C# 的限制,无法创建泛型方法 T ReadIntoStruct<T>(byte[] data)
,因此即使 T
仅限于值类型 (struct
),用泛型 T
替换 Packet
也根本无法编译。要编译,T
必须遵守 C# 语言规范 v3.0 的 §18.2 中规定的一组不同的要求:
非托管类型 是任何不是 引用类型 且在任何嵌套级别都不包含 引用类型 字段的类型。 换句话说,非托管类型 是以下类型之一
• sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal、 或 bool。
• 任何 enum-type。
• 任何 pointer-type。
• 任何仅包含 非托管类型 字段的用户定义的 struct-type。
请注意,即使您可以在结构体中使用 string
,但 string
不在列表中。 允许使用 非托管类型 的固定大小数组。
提出的解决方案是动态生成相同的方法,但用于给定类型,并使用泛型接口 ICall<T>
。 或者,还生成一个 static
方法来比较调用 static
方法和接口方法的成本。
为了避免
不满足 非托管类型 要求时出现任何奇怪的行为,我们必须根据所有规则递归地验证类型 T
T
- TypeExtensions.ThrowIfNotUnmanagedType()
。 我只是希望有一天对象 Type
会有一个简单的属性来检查,而不是我必须编写的所有代码,但现在它是 Type
对象上的一个扩展方法。
通用中间语言 (CIL) 相当复杂,但不需要深入理解即可完成方法生成。 首先,我使用 Reflector 查看为原型方法生成的 CIL。 然后,我改编了一个优秀的 OSS 库 .NET 的业务逻辑工具包 以发出与原型相同的 CIL,但用于不同的类型。 这篇文章 对如何使用工具包的 emit 功能进行了很好的介绍。 在我的代码中,我将所有辅助类都更改为扩展方法,使流程更加简化。
这是方法生成的样子。 请注意,将 ReadingStructureData.Packet
替换为另一项的类型。
var emit = methodBuilder.GetILGenerator();
// .locals init (
// [0] uint8& pinned packet,
var l0 = emit.DeclareLocal(typeof (byte).MakeByRefType(), true);
// [1] valuetype ReadingStructureData.Packet CS$1$0000,
var l1 = emit.DeclareLocal(itemType);
var L_0012 = emit.DefineLabel();
// because this code was taken from an instance method,
// "this" was the parameter 0, but was never used
emit
.ldarg(methodBuilder, param) //L_0000: ldarg.0
.ldc_i4_0() //L_0001: ldc.i4.0
.ldelema(typeof (byte)) //L_0002: ldelema uint8
.stloc(l0) //L_0007: stloc.0
.ldloc(l0) //L_0008: ldloc.0
.conv_i() //L_0009: conv.i
.ldobj(itemType) //L_000a: ldobj ReadingStructureData.Packet
.stloc(l1) //L_000f: stloc.1
.leave_s(L_0012) //L_0010: leave.s L_0012
.MarkLabelExt(L_0012) //L_0012:
.ldloc(l1) //ldloc.1
.ret() //L_0013: ret
;
Using the Code
该示例创建了两种方法 - 一种作为接口,需要一个对象的实例,另一种作为 static
方法的委托。
// Generate code
ICall<Packet> interfaceObj;
Func<byte[], Packet> staticDelegate;
WrapperFactory.Instance.CreateDynamicMethods(out interfaceObj, out staticDelegate);
var result = staticDelegate(sourceData); // Call static implementation (slower)
var result = interfaceObj.ReadItem(sourceData); // Interface implementation (faster)
性能研究
即使数字每次运行都会发生变化,总体结果是生成的代码的速度与原型代码的速度接近。 另请注意发出新代码所需的时间。 即使在包装多个类型时可以减少时间,但这仍然非常重要。
Non-Generic Solutions:
BinaryReader: 5,259.00
Pointer: 199.00
Generic Solutions:
MarshalSafe: 10,982.00
MarshalUnsafe: 6,944.00
C++/CLI: 467.00
Dynamically-generated solution:
Calling static prototype: 199.00
Calling interface prototype: 214.00
Creating dynamic methods: 14.00
Calling generated static: 213.00 (07% slower than static prototype)
Calling generated interface: 221.00 (03% slower than interface prototype)
关注点
即使 .NET 规范声明语句 fixed(byte *p = array)
和 fixed(byte *p = &array[0])
等效,IL 却显示了一个完全不同的故事。 第一条语句生成了明显更多的 IL 指令。 已在 Microsoft Connect 上创建了一个问题。 您可以在那里查看 IL 代码差异。
历史
- 2009 年 2 月 14 日 - 首次上传
- 2009 年 2 月 19 日 - 更新以删除外部代码依赖项