以前我一直有个疑惑——在C#中,究竟是类(class)比较快,还是结构体(struct)比较快?
当时没有深究。
最近我遇到一个难题,需要将一些运算大的指针操作代码给封装一下。原先为了性能,这些代码是以硬编码的形式混杂在算法逻辑之中,不但影响了算法逻辑的可读性,其本身的指针操作代码枯燥、难懂、易写错,不易维护。所以我希望将其封装一下,简化代码编写、提高可维护性,但同时要尽可能地保证性能。
由于那些指针操作代码很灵活,简单的封装不能解决问题,还需要用到接口(interface)以实现一些动态调用功能。
为了简化代码,还打算实现一些泛型方法。
本来还想因32位指针、64位指针的不同而构造泛型类,可惜发现C#不支持将int/long作为泛型类型约束,只好作罢。将设计改为——分别为32位指针、64位指针编写不同的类,它们实现同一个接口。
在C#中,有两类封装技术——
1.基于类(class)的封装。在基类中定义好操作方法,然后在派生类中实现操作方法。
2.基于结构体(struct)的封装。在接口中定义好操作方法,然后在结构体中实现该接口的操作方法。
我分别使用这两类封装技术编写测试代码,然后做性能测试。
经过反复思索,考虑 类、结构体、接口、泛型 的组合,我找出了15种函数调用模式——
硬编码
静态调用
调用派生类
调用结构体
调用基类
调用派生类的接口
调用结构体的接口
基类泛型调用派生类
基类泛型调用基类
接口泛型调用派生类
接口泛型调用结构体
接口泛型调用结构体引用
接口泛型调用基类
接口泛型调用派生类的接口
接口泛型调用结构体的接口
测试代码为——
using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;namespace TryPointerCall
{/// <summary>/// 指针操作接口/// </summary>public interface IPointerCall{/// <summary>/// 指针操作/// </summary>/// <param name="p">源指针</param>/// <returns>修改后指针</returns>unsafe byte* Ptr(byte* p);}#region 非泛型/// <summary>/// [非泛型] 指针操作基类/// </summary>public abstract class PointerCall : IPointerCall{public abstract unsafe byte* Ptr(byte* p);}/// <summary>/// [非泛型] 指针操作派生类: 指针+偏移/// </summary>public class PointerCallAdd : PointerCall{/// <summary>/// 偏移值/// </summary>public int Offset = 0;public override unsafe byte* Ptr(byte* p){return unchecked(p + Offset);}}/// <summary>/// [非泛型] 指针操作结构体: 指针+偏移/// </summary>public struct SPointerCallAdd : IPointerCall{/// <summary>/// 偏移值/// </summary>public int Offset;public unsafe byte* Ptr(byte* p){return unchecked(p + Offset);}}#endregion#region 泛型// !!! C#不支持将整数类型作为泛型约束 !!!//public abstract class GenPointerCall<T> : IPointerCall where T: int, long//{// public abstract unsafe byte* Ptr(byte* p);// void d()// {// }//}#endregion#region 全部测试/// <summary>/// 指针操作的一些常用函数/// </summary>public static class PointerCallTool{private const int CountLoop = 200000000; // 循环次数/// <summary>/// 调用指针操作/// </summary>/// <typeparam name="T">具有IPointerCall接口的类型。</typeparam>/// <param name="ptrcall">调用者</param>/// <param name="p">源指针</param>/// <returns>修改后指针</returns>public static unsafe byte* CallPtr<T>(T ptrcall, byte* p) where T : IPointerCall{return ptrcall.Ptr(p);}public static unsafe byte* CallClassPtr<T>(T ptrcall, byte* p) where T : PointerCall{return ptrcall.Ptr(p);}public static unsafe byte* CallRefPtr<T>(ref T ptrcall, byte* p) where T : IPointerCall{return ptrcall.Ptr(p);}// C#不允许将特定的结构体作为泛型约束。所以对于结构体只能采用上面那个方法,通过IPointerCall接口进行约束,可能会造成性能下降。//public static unsafe byte* SCallPtr<T>(T ptrcall, byte* p) where T : SPointerCallAdd//{// return ptrcall.Ptr(p);//}private static int TryIt_Static_Offset;private static unsafe byte* TryIt_Static_Ptr(byte* p){return unchecked(p + TryIt_Static_Offset);}/// <summary>/// 执行测试 - 静态调用/// </summary>/// <param name="sOut">文本输出</param>private static unsafe void TryIt_Static(StringBuilder sOut){TryIt_Static_Offset = 1;// == 性能测试 ==byte* p = null;Stopwatch sw = new Stopwatch();int i;unchecked{#region 测试// 硬编码sw.Reset();sw.Start();for (i = 0; i < CountLoop; ++i){p = p + TryIt_Static_Offset;}sw.Stop();sOut.AppendLine(string.Format("硬编码:\t{0}", sw.ElapsedMilliseconds));// 静态调用sw.Reset();sw.Start();for (i = 0; i < CountLoop; ++i){p = TryIt_Static_Ptr(p);}sw.Stop();sOut.AppendLine(string.Format("静态调用:\t{0}", sw.ElapsedMilliseconds));#endregion // 测试}}/// <summary>/// 执行测试 - 非泛型/// </summary>/// <param name="sOut">文本输出</param>private static unsafe void TryIt_NoGen(StringBuilder sOut){// 创建PointerCallAdd pca = new PointerCallAdd();SPointerCallAdd spca;pca.Offset = 1;spca.Offset = 1;// 转型PointerCall pca_base = pca;IPointerCall pca_itf = pca;IPointerCall spca_itf = spca;// == 性能测试 ==byte* p = null;Stopwatch sw = new Stopwatch();int i;unchecked{#region 调用#region 直接调用// 调用派生类sw.Reset();sw.Start();for (i = 0; i < CountLoop; ++i){p = pca.Ptr(p);}sw.Stop();sOut.AppendLine(string.Format("调用派生类:\t{0}", sw.ElapsedMilliseconds));// 调用结构体sw.Reset();sw.Start();for (i = 0; i < CountLoop; ++i){p = spca.Ptr(p);}sw.Stop();sOut.AppendLine(string.Format("调用结构体:\t{0}", sw.ElapsedMilliseconds));#endregion // 直接调用#region 间接调用// 调用基类sw.Reset();sw.Start();for (i = 0; i < CountLoop; ++i){p = pca_base.Ptr(p);}sw.Stop();sOut.AppendLine(string.Format("调用基类:\t{0}", sw.ElapsedMilliseconds));// 调用派生类的接口sw.Reset();sw.Start();for (i = 0; i < CountLoop; ++i){p = pca_itf.Ptr(p);}sw.Stop();sOut.AppendLine(string.Format("调用派生类的接口:\t{0}", sw.ElapsedMilliseconds));// 调用结构体的接口sw.Reset();sw.Start();for (i = 0; i < CountLoop; ++i){p = spca_itf.Ptr(p);}sw.Stop();sOut.AppendLine(string.Format("调用结构体的接口:\t{0}", sw.ElapsedMilliseconds));#endregion // 间接调用#endregion // 调用#region 泛型调用#region 泛型基类约束// 基类泛型调用派生类sw.Reset();sw.Start();for (i = 0; i < CountLoop; ++i){p = CallClassPtr(pca, p);}sw.Stop();sOut.AppendLine(string.Format("基类泛型调用派生类:\t{0}", sw.ElapsedMilliseconds));// 基类泛型调用基类sw.Reset();sw.Start();for (i = 0; i < CountLoop; ++i){p = CallClassPtr(pca_base, p);}sw.Stop();sOut.AppendLine(string.Format("基类泛型调用基类:\t{0}", sw.ElapsedMilliseconds));#endregion // 泛型基类约束#region 泛型接口约束 - 直接调用// 接口泛型调用派生类sw.Reset();sw.Start();for (i = 0; i < CountLoop; ++i){p = CallPtr(pca, p);}sw.Stop();sOut.AppendLine(string.Format("接口泛型调用派生类:\t{0}", sw.ElapsedMilliseconds));// 接口泛型调用结构体sw.Reset();sw.Start();for (i = 0; i < CountLoop; ++i){p = CallPtr(spca, p);}sw.Stop();sOut.AppendLine(string.Format("接口泛型调用结构体:\t{0}", sw.ElapsedMilliseconds));// 接口泛型调用结构体引用sw.Reset();sw.Start();for (i = 0; i < CountLoop; ++i){p = CallRefPtr(ref spca, p);}sw.Stop();sOut.AppendLine(string.Format("接口泛型调用结构体引用:\t{0}", sw.ElapsedMilliseconds));#endregion // 直接调用#region 间接调用// 接口泛型调用基类sw.Reset();sw.Start();for (i = 0; i < CountLoop; ++i){p = CallPtr(pca_base, p);}sw.Stop();sOut.AppendLine(string.Format("接口泛型调用基类:\t{0}", sw.ElapsedMilliseconds));// 接口泛型调用派生类的接口sw.Reset();sw.Start();for (i = 0; i < CountLoop; ++i){p = CallPtr(pca_itf, p);}sw.Stop();sOut.AppendLine(string.Format("接口泛型调用派生类的接口:\t{0}", sw.ElapsedMilliseconds));// 接口泛型调用结构体的接口sw.Reset();sw.Start();for (i = 0; i < CountLoop; ++i){p = CallPtr(spca_itf, p);}sw.Stop();sOut.AppendLine(string.Format("接口泛型调用结构体的接口:\t{0}", sw.ElapsedMilliseconds));#endregion // 间接调用#endregion // 泛型调用}}/// <summary>/// 执行测试 - 泛型/// </summary>/// <param name="sOut">文本输出</param>private static unsafe void TryIt_Gen(StringBuilder sOut){// !!! C#不支持将整数类型作为泛型约束 !!!}/// <summary>/// 执行测试/// </summary>public static string TryIt(){StringBuilder sOut = new StringBuilder();sOut.AppendLine("== PointerCallTool.TryIt() ==");TryIt_Static(sOut);TryIt_NoGen(sOut);TryIt_Gen(sOut);sOut.AppendLine();return sOut.ToString();}}
#endregion}
编译器——
VS2005:Visual Studio 2005 SP1。
VS2010:Visual Studio 2010 SP1。
采用上述编译器编译为Release版程序,最大速度优化。
机器A——
HP CQ42-153TX
处理器:Intel Core i5-430M(2.26GHz, Turbo 2.53GHz, 3MB L3)
内存容量:2GB (DDR3-1066)
机器B——
DELL Latitude E6320
处理器:Intel i3-2310M(2.1GHz, 3MB L3)
内存容量:4GB (DDR3-1333,双通道)
测试环境——
A_2005:机器A,VS2005,Window 7 32位。
A_2010:机器A,VS2010,Window 7 32位。
B_2005:机器B,VS2005,Window 7 64位(x64)。
B_2010:机器B,VS2010,Window 7 64位(x64)。
B_2010xp:机器B,VS2010,Window XP SP3 32位。
测试结果(单位:毫秒)——
模式 | A_2005 | A_2010 | B_2005 | B_2010 | B_2010xp |
硬编码 | 163 | 162 | 23 | 24 | 95 |
静态调用 | 162 | 161 | 23 | 23 | 95 |
调用派生类 | 570 | 487 | 456 | 487 | 606 |
调用结构体 | 162 | 160 | 95 | 620 | 100 |
调用基类 | 565 | 571 | 453 | 513 | 874 |
调用派生类的接口 | 810 | 728 | 779 | 708 | 929 |
调用结构体的接口 | 1052 | 1055 | 1175 | 1175 | 1267 |
基类泛型调用派生类 | 975 | 568 | 1055 | 1148 | 671 |
基类泛型调用基类 | 984 | 569 | 1055 | 1152 | 671 |
接口泛型调用派生类 | 1383 | 729 | 1346 | 1531 | 1062 |
接口泛型调用结构体 | 566 | 162 | 767 | 1149 | 107 |
接口泛型调用结构体引用 | 487 | 164 | 752 | 816 | 100 |
接口泛型调用基类 | 1378 | 812 | 1337 | 1535 | 1072 |
接口泛型调用派生类的接口 | 1376 | 810 | 1338 | 1533 | 1102 |
接口泛型调用结构体的接口 | 1542 | 1133 | 2486 | 2013 | 1365 |
结果分析——
先看第1列数据(A_2005),发现“静态调用”、“调用结构体”与“硬编码”的时间几乎一致,很可能做了函数展开优化。其次最快的是“接口泛型调用结构体引用”,比“接口泛型调用结构体”快了16%。但是“接口泛型调用结构体的接口”最慢,“调用结构体的接口”也比较慢。其他的基于类的调用模式的速度排在中间。而且发现泛型方法速度较慢。
然后看第2列数据(A_2010),发现“接口泛型调用结构体”、“接口泛型调用结构体引用”也与“硬编码”的时间几乎一致,表示它们也是做了函数展开优化的,看来在VS2010中不需要使用ref优化结构体参数。“调用结构体的接口”、“接口泛型调用结构体的接口”两个都成了垫底。泛型方法的速度有了很大的提高,几乎与非泛型调用速度相当。
再看第3列数据(B_2005),并与第1列(A_2005)进行比较,发现“静态调用”与“硬编码”的时间几乎一致,而“调用结构体”要慢一些。“接口泛型调用结构体”、“接口泛型调用结构体引用”比较慢,排在了“调用基类”、“调用派生类”的后面。可能是64位环境(x64)的特点吧。
再看第4列数据(B_2010),并与第3列(B_2005)进行比较,发现大部分变慢了,尤其是结构体相关的,难道VS2010的x64性能还不如VS2005?我将平台改为“x64”又编译了一次,结果依旧。
再看第5列数据(B_2010xp),发现32位WinXP下的大部分项目比64位Win7下要快,真诡异。而且发现“静态调用”、“调用结构体”与“硬编码”的时间几乎一致,看来“调用结构体”一直是被函数展开优化的,而64位下的静态调用有着更深层次的优化,所以比不过。
我觉得在要求性能的情况下,使用结构体封装指针操作比较好,因为直接调用时会做函数展开优化,大多数情况下与硬编码的性能一致。在遇到需要一些灵活功能时,可考虑采用“接口泛型调用结构体引用”的方式,速度有所下降。接口方式最慢,尽可能不用。一定要用接口的话,应优先选择非泛型版。
(完)
测试程序exe——
http://115.com/file/dn6hvcm3
http://download.csdn.net/detail/zyl910/3614511
源代码下载——
http://115.com/file/aqz70zy3
http://download.csdn.net/detail/zyl910/3614514
目录——
C#类与结构体究竟谁快——各种函数调用模式速度评测:http://www.cnblogs.com/zyl910/archive/2011/09/19/2186623.html
再探C#类与结构体究竟谁快——考虑栈变量、栈分配、64位整数、密封类:http://www.cnblogs.com/zyl910/archive/2011/09/20/2186622.html
三探C#类与结构体究竟谁快——MSIL(微软中间语言)解读:http://www.cnblogs.com/zyl910/archive/2011/09/24/2189403.html
四探C#类与结构体究竟谁快——跨程序集(assembly)调用:http://www.cnblogs.com/zyl910/archive/2011/10/01/2197844.html