以前我一直有个疑惑——在C#中,究竟是类(class)比较快,还是结构体(struct)比较快?
当时没有深究。
最近我遇到一个难题,需要将一些运算大的指针操作代码给封装一下。原先为了性能,这些代码是以硬编码的形式混杂在算法逻辑之中,不但影响了算法逻辑的可读性,其本身的指针操作代码枯燥、难懂、易写错,不易维护。所以我希望将其封装一下,简化代码编写、提高可维护性,但同时要尽可能地保证性能。
由于那些指针操作代码很灵活,简单的封装不能解决问题,还需要用到接口(interface)以实现一些动态调用功能。
为了简化代码,还打算实现一些泛型方法。
本来还想因32位指针、64位指针的不同而构造泛型类,可惜发现C#不支持将int/long作为泛型类型约束,只好作罢。将设计改为——分别为32位指针、64位指针编写不同的类,它们实现同一个接口。
在C#中,有两类封装技术——
1.基于类(class)的封装。在基类中定义好操作方法,然后在派生类中实现操作方法。
2.基于结构体(struct)的封装。在接口中定义好操作方法,然后在结构体中实现该接口的操作方法。
我分别使用这两类封装技术编写测试代码,然后做性能测试。
经过反复思索,考虑 类、结构体、接口、泛型 的组合,我找出了15种函数调用模式——
硬编码
静态调用
调用派生类
调用结构体
调用基类
调用派生类的接口
调用结构体的接口
基类泛型调用派生类
基类泛型调用基类
接口泛型调用派生类
接口泛型调用结构体
接口泛型调用结构体引用
接口泛型调用基类
接口泛型调用派生类的接口
接口泛型调用结构体的接口
测试代码为——
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 | 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
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库