以前我一直有个疑惑——在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