以前我一直有个疑惑——在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://blog.csdn.net/zyl910/article/details/6788417
再探C#类与结构体究竟谁快——考虑栈变量、栈分配、64位整数、密封类:http://blog.csdn.net/zyl910/article/details/6793908
三探C#类与结构体究竟谁快——MSIL(微软中间语言)解读:http://blog.csdn.net/zyl910/article/details/6817158
四探C#类与结构体究竟谁快——跨程序集(assembly)调用:http://blog.csdn.net/zyl910/article/details/6839868
目录——
C#类与结构体究竟谁快——各种函数调用模式速度评测:http://blog.csdn.net/zyl910/article/details/6788417
再探C#类与结构体究竟谁快——考虑栈变量、栈分配、64位整数、密封类:http://blog.csdn.net/zyl910/article/details/6793908
三探C#类与结构体究竟谁快——MSIL(微软中间语言)解读:http://blog.csdn.net/zyl910/article/details/6817158
四探C#类与结构体究竟谁快——跨程序集(assembly)调用:http://blog.csdn.net/zyl910/article/details/6839868