C++/CLI 函式指針與 .NET 委託的性能
對於我的 C++/CLI 項目,我只是嘗試衡量 C++/CLI 函式指針與 .NET 委託的成本。
我的期望是,C++/CLI 函式指針比 .NET 委託更快。因此,我的測試分別計算了 5 秒內 .NET 委託和本機函式指針的呼叫次數。
結果
現在結果(並且仍然)令我震驚:
- .NET 委託: 在 5003 毫秒內執行 910M,結果為 152080413333030
- 函式指針: 在 5013 毫秒內執行 347M 次,結果為 57893422166551
這意味著,本機 C++/CLI 函式指針的使用幾乎比在 C++/CLI 程式碼中使用託管委託慢 3 倍。***怎麼可能?***在性能關鍵部分使用介面、委託或抽像類時,我應該使用託管構造嗎?
測試程式碼
連續呼叫的函式:
__int64 DoIt(int n, __int64 sum) { if ((n % 3) == 0) return sum + n; else return sum + 1; }呼叫該方法的程式碼嘗試使用所有參數以及返回值,因此沒有任何東西被優化掉(希望如此)。這是程式碼(用於 .NET 代表):
__int64 executions; __int64 result; System::Diagnostics::Stopwatch^ w = gcnew System::Diagnostics::Stopwatch(); System::Func<int, __int64, __int64>^ managedPtr = gcnew System::Func<int, __int64, __int64>(&DoIt); w->Restart(); executions = 0; result = 0; while (w->ElapsedMilliseconds < 5000) { for (int i=0; i < 1000000; i++) result += managedPtr(i, executions); executions++; } System::Console::WriteLine(".NET delegate: {0}M executions with result {2} in {1}ms", executions, w->ElapsedMilliseconds, result);與 .NET 委託呼叫類似,使用 C++ 函式指針:
typedef __int64 (* DoItMethod)(int n, __int64 sum); DoItMethod nativePtr = DoIt; w->Restart(); executions = 0; result = 0; while (w->ElapsedMilliseconds < 5000) { for (int i=0; i < 1000000; i++) result += nativePtr(i, executions); executions++; } System::Console::WriteLine("Function pointer: {0}M executions with result {2} in {1}ms", executions, w->ElapsedMilliseconds, result);附加資訊
- 使用 Visual Studio 2012 編譯
- .NET Framework 4.5 是目標
- 發布版本(執行計數與調試版本保持成比例)
- 呼叫約定是 __stdcall(當項目使用 CLR 支持編譯時不允許使用 __fastcall)
已完成所有測試:
- .NET 虛擬方法:在 5004 毫秒內執行 1025M,結果為 171358304166325
- .NET 委託:在 5003 毫秒內執行 910M,結果為 152080413333030
- 虛擬方法:在 5006 毫秒內執行 336M,結果為 56056335999888
- 函式指針:在 5013 毫秒內執行 347M 次,結果為 57893422166551
- 函式呼叫:在 5001 毫秒內執行 1459M,結果為 244230520832847
- 內聯函式:在 5000 毫秒內執行 1385M 次,結果為 231791984166205
對“DoIt”的直接呼叫在這裡由“函式呼叫”表示,它似乎被編譯器內聯,因為與對內聯函式的呼叫相比,執行計數沒有(顯著)差異。
對 C++ 虛方法的呼叫與函式指針一樣“慢”。託管類(引用類)的虛擬方法與 .NET 委託一樣快。
更新: 我挖得更深一點,似乎對於使用非託管函式的測試,每次呼叫 DoIt 函式時都會轉換到本機程式碼。因此,我將內部循環包裝到另一個我強制編譯非託管的函式中:
#pragma managed(push, off) __int64 TestCall(__int64* executions) { __int64 result = 0; for (int i=0; i < 1000000; i++) result += DoItNative(i, *executions); (*executions)++; return result; } #pragma managed(pop)另外我像這樣測試了 std::function :
#pragma managed(push, off) __int64 TestStdFunc(__int64* executions) { __int64 result = 0; std::function<__int64(int, __int64)> func(DoItNative); for (int i=0; i < 1000000; i++) result += func(i, *executions); (*executions)++; return result; } #pragma managed(pop)現在,新的結果是:
- 函式呼叫:在 5000 毫秒內執行 2946M 次,結果為 495340439997054
- std::function:在 5018 毫秒內執行 160M,結果為 26679519999840
std::function 有點令人失望。
您正在看到“雙重打擊”的成本。DoIt() 函式的核心問題是它被編譯為託管程式碼。委託呼叫非常快,通過委託從託管程式碼轉到託管程式碼並不復雜。函式指針很慢,但是編譯器會自動生成程式碼,首先從託管程式碼切換到非託管程式碼,然後通過函式指針進行呼叫。然後在一個存根中結束,該存根從非託管程式碼切換回託管程式碼並呼叫 DoIt()。
大概您真正要衡量的是對本機程式碼的呼叫。使用 #pragma 強制將 DoIt() 生成為機器程式碼,如下所示:
#pragma managed(push, off) __int64 DoIt(int n, __int64 sum) { if ((n % 3) == 0) return sum + n; else return sum + 1; } #pragma managed(pop)您現在將看到函式指針比委託更快