以前我一直有個疑惑——在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://115.com/file/aqz70zy3
摘自:zyl910的專欄