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