I have demonstrated that C++/CLI vs P/Invoke Performance difference in Part I by using all different wrappers to pinvoke C++ method of sqrt of a native C++ DLL. Since I already included C# version of sqrt in the comparison, I will include comparison of C# version of any native c++ method I used in the future blogs, I will try to optimize every piece of C#, C++ and C++/CLI code as possible as I can, otherwise, it won’t be accurate at all. The C# wrapper used to pinvoke C++ DLL was generated by our PInvoke Interop SDK – A C# Wrapper Generator for C++ DLL.
I added the following methods to the original C++ Calculator class. I choose a simplified implementation(it could overflow depending on the parameters passed in) of hypot because the implementation itself is not that important to our test.
float hypot(float x, float y) { return ::sqrt(x * x + y * y); } double hypot(double x, double y) { return ::sqrt(x * x + y * y); }
When I built the C++ native DLL, I tried to optimize it as possible as I can, So I optimized the dll build for maximize speed and favor fast code.
And I also enabled SIMD Extensions 2, I tried and it did show it improved the performance.
C++/CLI wrapper code is still very simple, but I had to write the method signature one more time. This is why I do not prefer C++/CLI, I have to repeat every method signature I define in the C++ class even it does not require marshaling.
float hypot(float x, float y) { return cal->hypot(x, y); } double hypot(double x, double y) { return cal->hypot(x, y); }
The C# Wrapper methods code are presented below.
[HandleProcessCorruptedStateExceptions] [SuppressUnmanagedCodeSecurity] [DllImport("LibCalculator.dll", EntryPoint="#32", CallingConvention=CallingConvention.ThisCall)] public extern static double Calculator_hypot(IntPtr thisObject, double x, double y); [HandleProcessCorruptedStateExceptions] [SuppressUnmanagedCodeSecurity] [DllImport("LibCalculator.dll", EntryPoint="#31", CallingConvention=CallingConvention.ThisCall)] public extern static float Calculator_hypot(IntPtr thisObject, float x, float y); public double hypot(double x, double y) { return LibCalculatorWin32.Calculator_hypot(this.ptrObject, x, y); } public float hypot(float x, float y) { return LibCalculatorWin32.Calculator_hypot(this.ptrObject, x, y); }
Now, the complete C# testing code.
namespace NSpeech.Win32.NLibCalculator.Test { using System; using System.Collections.Generic; using System.Linq; using System.Text; using NSpeech.Win32.StdLib; using NSpeech.Win32.NLibCalculator; using System.Runtime.InteropServices; using SampleManagedWrapper; public class HypotFloatTestClass { public static void Test(int testCount, int iterationCount) { double timeUsed1 = 0, timeUsed2 = 0, timeUsed3 = 0, timeUsed4 = 0; for (int i = 0; i < testCount; i++) { float result; Console.WriteLine("Testing float hypot(float) of C#"); result = CSharp_Hypot(iterationCount, ref timeUsed1); Console.WriteLine("Result = {0}", result); Console.WriteLine("Testing float hypot(float) of Managed C++ Wrapper."); result = ManagedCppWrapper_Hypot(iterationCount, ref timeUsed2); Console.WriteLine("Result = {0}", result); Console.WriteLine("Testing float hypot(float) of C# Wrapper"); result = CSharpWrapper_Hypot(iterationCount, ref timeUsed3); Console.WriteLine("Result = {0}", result); Console.WriteLine("Testing float hypot(float) of C# Wrapper with Direct Call."); result = CSharpWrapperDirectCall_Hypot(iterationCount, ref timeUsed4); Console.WriteLine("Result = {0}", result); Console.WriteLine("================================================================================="); Console.WriteLine("C# Hypot - Average Time used: {0}ms", timeUsed1 / (i + 1)); Console.WriteLine("Managed C++ Wrapper Hypot- Average Time used: {0}ms", timeUsed2 / (i + 1)); Console.WriteLine("PInvoke Wrapper - Average Time used: {0}ms", timeUsed3 / (i + 1)); Console.WriteLine("PInvoke Wrapper Direct Call - Average Time used: {0}ms", timeUsed4 / (i + 1)); Console.WriteLine("================================================================================="); Console.WriteLine(); } Console.WriteLine(); Console.WriteLine(); Console.WriteLine("=========================Final Result of float hypot(float)============================="); Console.WriteLine(); Console.WriteLine("Total Iteration: {0}", iterationCount); Console.WriteLine(); Console.WriteLine("Method of float hypot(float) Time Percentage"); Console.WriteLine(); Console.WriteLine("C# Managed hypot {0} ms 1.0", (timeUsed1 / testCount).ToString("00.000")); Console.WriteLine("C++ PInvoke, Managed Wrapper of Native hypot {0} ms {1}", (timeUsed2 / testCount).ToString("00.000"), (timeUsed2 / timeUsed1).ToString("0.000")); Console.WriteLine("Explicit PInvoke C# Wrapper of Native hypot {0} ms {1}", (timeUsed3 / testCount).ToString("00.000"), (timeUsed3 / timeUsed1).ToString("0.000")); Console.WriteLine("Explicit PInvoke C# Wrapper with Direct Call of Native hypot {0} ms {1}", (timeUsed4 / testCount).ToString("00.000"), (timeUsed4 / timeUsed1).ToString("0.000")); Console.WriteLine(); Console.WriteLine("========================================================================================"); } static float CSharp_Hypot(int count, ref double timeUsed) { DateTime start = DateTime.Now; var cal = new Calculator(); float result = 0f; float fCount = (float)count; for (float value = 0; value < fCount; value++) { result = (float)Math.Sqrt(value * value + result * result); } DateTime end = DateTime.Now; timeUsed += (end - start).TotalMilliseconds; return result; } static float ManagedCppWrapper_Hypot(int count, ref double timeUsed) { DateTime start = DateTime.Now; var cal = new ManagedCalculator(); float result = 0f; float fCount = (float)count; for (float value = 0; value < fCount; value++) { // PInvoke Managed C++/CLI class method result = cal.hypot(value, result); } DateTime end = DateTime.Now; timeUsed += (end - start).TotalMilliseconds; return result; } static float CSharpWrapper_Hypot(int count, ref double timeUsed) { DateTime start = DateTime.Now; var cal = new Calculator(); float result = 0f; float fCount = (float)count; for (float value = 0; value < fCount; value++) { // PInvoke C++ class method result = cal.hypot(value, result); } DateTime end = DateTime.Now; timeUsed += (end - start).TotalMilliseconds; return result; } static float CSharpWrapperDirectCall_Hypot(int count, ref double timeUsed) { DateTime start = DateTime.Now; var cal = new Calculator(); float result = 0f; float fCount = (float)count; for (float value = 0; value < fCount; value++) { // PInvoke C++ class method without using the Calculator class wrapper result = LibCalculatorWin32.Calculator_hypot1(cal.ptrObject, value, result); } DateTime end = DateTime.Now; timeUsed += (end - start).TotalMilliseconds; return result; } } }
and then the testing code for double version Hypot.
namespace NSpeech.Win32.NLibCalculator.Test { using System; using System.Collections.Generic; using System.Linq; using System.Text; using NSpeech.Win32.StdLib; using NSpeech.Win32.NLibCalculator; using System.Runtime.InteropServices; using SampleManagedWrapper; public class HypotDoubleTestClass { public static void Test(int testCount, int iterationCount) { double timeUsed1 = 0, timeUsed2 = 0, timeUsed3 = 0, timeUsed4 = 0; for (int i = 0; i < testCount; i++) { double result; Console.WriteLine("Testing double hypot(double) of C#"); result = CSharp_Hypot(iterationCount, ref timeUsed1); Console.WriteLine("Result = {0}", result); Console.WriteLine("Testing double hypot(double) of Managed C++ Wrapper."); result = ManagedCppWrapper_Hypot(iterationCount, ref timeUsed2); Console.WriteLine("Result = {0}", result); Console.WriteLine("Testing double hypot(double) of C# Wrapper"); result = CSharpWrapper_Hypot(iterationCount, ref timeUsed3); Console.WriteLine("Result = {0}", result); Console.WriteLine("Testing double hypot(double) of C# Wrapper with Direct Call."); result = CSharpWrapperDirectCall_Hypot(iterationCount, ref timeUsed4); Console.WriteLine("Result = {0}", result); Console.WriteLine("================================================================================="); Console.WriteLine("C# Hypot - Average Time used: {0}ms", timeUsed1 / (i + 1)); Console.WriteLine("Managed C++ Wrapper Hypot- Average Time used: {0}ms", timeUsed2 / (i + 1)); Console.WriteLine("PInvoke Wrapper - Average Time used: {0}ms", timeUsed3 / (i + 1)); Console.WriteLine("PInvoke Wrapper Direct Call - Average Time used: {0}ms", timeUsed4 / (i + 1)); Console.WriteLine("================================================================================="); Console.WriteLine(); } Console.WriteLine(); Console.WriteLine(); Console.WriteLine("=========================Final Result of double hypot(double)============================="); Console.WriteLine(); Console.WriteLine("Total Iteration: {0}", iterationCount); Console.WriteLine(); Console.WriteLine("Method of double hypot(double) Time Percentage"); Console.WriteLine(); Console.WriteLine("C# Managed hypot {0} ms 1.0", (timeUsed1 / testCount).ToString("00.000")); Console.WriteLine("C++ PInvoke, Managed Wrapper of Native hypot {0} ms {1}", (timeUsed2 / testCount).ToString("00.000"), (timeUsed2 / timeUsed1).ToString("0.000")); Console.WriteLine("Explicit PInvoke C# Wrapper of Native hypot {0} ms {1}", (timeUsed3 / testCount).ToString("00.000"), (timeUsed3 / timeUsed1).ToString("0.000")); Console.WriteLine("Explicit PInvoke C# Wrapper with Direct Call of Native hypot {0} ms {1}", (timeUsed4 / testCount).ToString("00.000"), (timeUsed4 / timeUsed1).ToString("0.000")); Console.WriteLine(); Console.WriteLine("=========================================================================================="); } static double CSharp_Hypot(int count, ref double timeUsed) { DateTime start = DateTime.Now; var cal = new Calculator(); double result = 0f; double fCount = (double)count; for (double value = 0; value < fCount; value++) { result = Math.Sqrt(value * value + result * result); } DateTime end = DateTime.Now; timeUsed += (end - start).TotalMilliseconds; return result; } static double ManagedCppWrapper_Hypot(int count, ref double timeUsed) { DateTime start = DateTime.Now; // C++/CLI managed wrapper class of native class, Calculator var cal = new ManagedCalculator(); double result = 0f; double fCount = (double)count; for (double value = 0; value < fCount; value++) { // PInvoke C++/CLI class method of hypot result = cal.hypot(value, result); } DateTime end = DateTime.Now; timeUsed += (end - start).TotalMilliseconds; return result; } static double CSharpWrapper_Hypot(int count, ref double timeUsed) { DateTime start = DateTime.Now; // C# Wrapper class of the native class, Calculator. var cal = new Calculator(); double result = 0f; double fCount = (double)count; for (double value = 0; value < fCount; value++) { // PInvoke C++ Class method of hypot. result = cal.hypot(value, result); } DateTime end = DateTime.Now; timeUsed += (end - start).TotalMilliseconds; return result; } static double CSharpWrapperDirectCall_Hypot(int count, ref double timeUsed) { DateTime start = DateTime.Now; // C# Wrapper class of the native class, Calculator. var cal = new Calculator(); double result = 0f; double fCount = (double)count; for (double value = 0; value < fCount; value++) { // PInvoke C++ Class method of hypot without using the wrapper class method. result = LibCalculatorWin32.Calculator_hypot(cal.ptrObject, value, result); } DateTime end = DateTime.Now; timeUsed += (end - start).TotalMilliseconds; return result; } } }
Now, let’s look at the result of calling the two versions of Hypot methods.
Float Version of Hypot
Doube version of Hypot
The result is what I expected, C++/CLI is the worst performer in both case. The C# version of hypot is the best since there is not much math operation involved in this test and the time to setup the thunk is significant, and we had to push two float or double parameter to the stack, it did cost more CPU time for .NET framework to setup the calling stack. Our C# wrapper class performed very close to the C# version of hypot when the direct call to the P/Invoke method is used, that is the fastest way to pinvoke C++ class method. If you define the hypot as static method, the performance is so close to the C# version and it actually could be better if the math operation is more complicated, which means we can probably build a C# Math library wrapper on top of any existing fast-performance C++ library and the performance could be better than a C# version of Math library.
Stay tuned, I will present you C# vs C++/CLI vs PInvoke Performance – Part IV.