最近看到一个帖子,问的是怎么把自己定义的结构体转换成对应的byte数组,一般来说,都会想到用Marshal类来完成这个功能,其实还有一个方法也可以,那就是利用unsafe代码。
先定义假想的一个值类型:
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)
Code
1 [StructLayout(LayoutKind.Explicit, Size = 6)]
2 public struct MyStruct
3 {
4 [FieldOffset(0)]
5 public int Value;
6 [FieldOffset(0)]
7 public short Low;
8 [FieldOffset(2)]
9 public short Hi;
10 [FieldOffset(4)]
11 public short X;
12 }
然后,定义一个公用方法签名:Action<MyStruct, Stream>,这个是为了方便之后的几种不同方式做性能测试。
先来看看Marshal类是怎么做到的:
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)
Code
1 public static void ByMarshal(object p_StructObj, Stream stream)
2 {
3 int stuctSize = Marshal.SizeOf(p_StructObj);
4 byte[] stuctBytes = new byte[stuctSize];
5 IntPtr structPtr = Marshal.AllocHGlobal(stuctSize);
6 Marshal.StructureToPtr(p_StructObj, structPtr, false);
7 Marshal.Copy(structPtr, stuctBytes, 0, stuctSize);
8 Marshal.FreeHGlobal(structPtr);
9 stream.Write(stuctBytes, 0, stuctSize);
10 } 然后看看unsafe代码是如何做到的:
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)
Code
public unsafe static void ByUnsafe(MyStruct s, Stream stream)
{
MyStruct* ptr = &s;
for (int i = 0; i < sizeof(MyStruct); i++)
stream.WriteByte(*((byte*)ptr + i));
} 比较两者,可以发现,使用Marshal类出现了装箱,新建数组(因为签名定成了输出到流),分配堆空间,和释放对空间等,当然,实战中可以避免掉新建数组的代价,但是总体代价依然高于unsafe方法,当然,用Marshal可以很轻松的做到把任意值类型copy到数组里面,这一点上unsafe方法就吃亏了,因为unsafe方法必须要非常明确的支出结构体的具体类型。
先来看一个性能对比:
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)
Code
1 static void Main()
2 {
3 MyStruct s = new MyStruct();
4 s.Low = 5;
5 s.Hi = 10;
6 s.X = 1;
7 const int count = 1000000;
8 MemoryStream ms = new MemoryStream();
9 Stopwatch sw = new Stopwatch();
10 sw.Start();
11 for (int i = 0; i < count; i++)
12 ByUnsafe(s, ms);
13 sw.Stop();
14 Console.WriteLine(sw.ElapsedMilliseconds);
15 ms = new MemoryStream();
16 GC.Collect(2);
17 GC.WaitForPendingFinalizers();
18 sw.Reset();
19 sw.Start();
20 for (int i = 0; i < count; i++)
21 ByMarshal(s, ms);
22 sw.Stop();
23 Console.WriteLine(sw.ElapsedMilliseconds);
24 } 在我机器(型号比较老。。。)上的运行结果是:135,718
可以发现unsafe代码的性能优势非常明显,但是,如果不解决类型问题,unsafe的方式显然实用意义仍然小与Marshal方式,可能有人会想到用泛型,但是不幸的是泛型无法作用于unsafe代码,也就是说,T*是无法通过编译的,难道真的走投无路了吗?
别忘了,.net还有一个杀手锏,LCG——轻量级代码生成,就是凭借LCG,IronPython的性能才能如此出众。我们无法在运用泛型指针,但是我们可以用泛型方法来生成对应类型的非泛型unsafe代码,来实现一个:
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)
Code
1 public static Action<T, Stream> ByLcg<T>()
2 where T : struct
3 {
4 DynamicMethod dm = new DynamicMethod(string.Empty, typeof(void),
5 new Type[] { typeof(T), typeof(Stream) }, typeof(T));
6 var il = dm.GetILGenerator();
7 var ptr = il.DeclareLocal(typeof(T).MakePointerType());
8 var i = il.DeclareLocal(typeof(int));
9
10 var loopCondition = il.DefineLabel();
11 var loopBegin = il.DefineLabel();
12
13 il.Emit(OpCodes.Ldarga_S, 0);
14 il.Emit(OpCodes.Conv_U);
15 il.Emit(OpCodes.Stloc_0);
16 il.Emit(OpCodes.Ldc_I4_0);
17 il.Emit(OpCodes.Stloc_1);
18 il.Emit(OpCodes.Br_S, loopCondition);
19 il.MarkLabel(loopBegin);
20 il.Emit(OpCodes.Ldarg_1);
21 il.Emit(OpCodes.Ldloc_0);
22 il.Emit(OpCodes.Ldloc_1);
23 il.Emit(OpCodes.Add);
24 il.Emit(OpCodes.Ldind_U1);
25 il.Emit(OpCodes.Callvirt, typeof(Stream).GetMethod("WriteByte"));
26 il.Emit(OpCodes.Ldloc_1);
27 il.Emit(OpCodes.Ldc_I4_1);
28 il.Emit(OpCodes.Add);
29 il.Emit(OpCodes.Stloc_1);
30 il.MarkLabel(loopCondition);
31 il.Emit(OpCodes.Ldloc_1);
32 il.Emit(OpCodes.Sizeof, typeof(T));
33 il.Emit(OpCodes.Clt);
34 il.Emit(OpCodes.Brtrue_S, loopBegin);
35 il.Emit(OpCodes.Ret);
36
37 return (Action<T, Stream>)dm.CreateDelegate(typeof(Action<T, Stream>));
38 } 有点长,但是性能如何哪?来改造一下测试代码:
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)
Code
static void Main()
{
MyStruct s = new MyStruct();
s.Low = 5;
s.Hi = 10;
s.X = 1;
const int count = 1000000;
MemoryStream ms = new MemoryStream();
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < count; i++)
ByUnsafe(s, ms);
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
ms = new MemoryStream();
GC.Collect(2);
GC.WaitForPendingFinalizers();
sw.Reset();
sw.Start();
for (int i = 0; i < count; i++)
ByMarshal(s, ms);
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
ms = new MemoryStream();
GC.Collect(2);
GC.WaitForPendingFinalizers();
sw.Reset();
sw.Start();
var d = ByLcg<MyStruct>();
for (int i = 0; i < count; i++)
d(s, ms);
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
}
1 public static Action<T, Stream> ByLcg<T>()
2 where T : struct
3 {
4 DynamicMethod dm = new DynamicMethod(string.Empty, typeof(void),
5 new Type[] { typeof(T), typeof(Stream) }, typeof(T));
6 var il = dm.GetILGenerator();
7 var ptr = il.DeclareLocal(typeof(T).MakePointerType());
8 var i = il.DeclareLocal(typeof(int));
9
10 var loopCondition = il.DefineLabel();
11 var loopBegin = il.DefineLabel();
12
13 il.Emit(OpCodes.Ldarga_S, 0);
14 il.Emit(OpCodes.Conv_U);
15 il.Emit(OpCodes.Stloc_0);
16 il.Emit(OpCodes.Ldc_I4_0);
17 il.Emit(OpCodes.Stloc_1);
18 il.Emit(OpCodes.Br_S, loopCondition);
19 il.MarkLabel(loopBegin);
20 il.Emit(OpCodes.Ldarg_1);
21 il.Emit(OpCodes.Ldloc_0);
22 il.Emit(OpCodes.Ldloc_1);
23 il.Emit(OpCodes.Add);
24 il.Emit(OpCodes.Ldind_U1);
25 il.Emit(OpCodes.Callvirt, typeof(Stream).GetMethod("WriteByte"));
26 il.Emit(OpCodes.Ldloc_1);
27 il.Emit(OpCodes.Ldc_I4_1);
28 il.Emit(OpCodes.Add);
29 il.Emit(OpCodes.Stloc_1);
30 il.MarkLabel(loopCondition);
31 il.Emit(OpCodes.Ldloc_1);
32 il.Emit(OpCodes.Sizeof, typeof(T));
33 il.Emit(OpCodes.Clt);
34 il.Emit(OpCodes.Brtrue_S, loopBegin);
35 il.Emit(OpCodes.Ret);
36
37 return (Action<T, Stream>)dm.CreateDelegate(typeof(Action<T, Stream>));
38 } 看看结果如何:136,713,142
真的这么强大吗?LCG本身的代价哪?可以把count修改成1,再次运行,得到的结果是:0,0,14
也可以看到在循环次数太少的情况下,获得的性能优势不足以弥补LCG本身的消耗,那么多少是临界点哪?
我的机器上,经过多次试验,发现21000时,Marshal方法和LCG方法的时间消耗相等,也就是说,当次数小于21000次是,Marshal占了上风,单是如果,超过21000次时,LCG带来的性能优势,就足以弥补代码生成的损失了。(当然,前提是生成的代码,以委托的方式被缓存下来)
如果,无论次数多少,都要求最高性能的话,那就只有用unsafe的方法,把每一个需要用到的结构体都写一遍了,就是用人力换速度。。。