C++/CLI vs Explicit PInvoke Performance – Part I
When I have a C++ DLL with a bunch of C++ classes and I would want to pinvoke those C++ classes from C#, I have two choices, either using C++/CLI to write a managed wrapper or using our C# wrapper generator to generate a managed C# wrapper of the native C++ DLL. I will create both of the wrappers today.
There is actually a false assumption all over the web which assumes that calling into native C++ DLL from C# via C++/CLI wrapper is faster than simply explicitly pinvoking the C++ DLL. That assumption is wrong and explicit pinvoke actually performs better than C++/CLI when C++/CLI is used to write a managed wrapper of C++ DLL, which is used from .NET. I am going to write a few blogs to talk about the performance difference of C++/CLI wapper and the C# wrapper via explicit PInvoke generated by our PInvoke Interop SDK tools.
This is the first part of my blog comparing C++/CLI to the C# wrapper generated by our C# wrapper generator for C++ DLL.
Let’s write a C++ class with 2 methods we will call from C# via different wrappers. We are going to use a sqrt method in C++ DLL, in this example, the parameter is simple and the type is blittable type and no marshal is needed from C# to the native dll.
#ifdef LIBCALCULATOR_EXPORTS #define LIBCALCULATOR_API __declspec(dllexport) #else #define LIBCALCULATOR_API __declspec(dllimport) #endif public LIBCALCULATOR_API Calculator { public: Calculator() {}; ~Calculator {}; float sqrt(float x) { return ::sqrt(x); }; double sqrt(double x) { return ::sqrt(x); }; };
The implementation of a managed C++/CLI wrapper for native C++ DLL is very simple for our example although it will become very tedious and time-consuming if you want to wrap a large scale of exported C++ classes in a C++ DLL, such as type marshaling and maintaining all the method signatures, but that is a different story, we may would want to talk about this in the future.
#pragma once #include "Calculator.h" #include "msclr\marshal_cppstd.h" using namespace System; using namespace Runtime::InteropServices; using namespace msclr::interop; namespace CalculatorManagedWrapper { public ref class ManagedCalculator { Calculator* cal; public: ManagedCalculator() { cal = new Calculator(); } ~ManagedCalculator() { delete cal; } float sqrt(float x) { return cal->sqrt(x); } double sqrt(double x) { return cal->sqrt(x); } } }
Now, I will present the C# class for testing the wrapper, I also wrote a managed C# sqrt for comparison.
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 CalculatorManagedWrapper; public class SqrtFloatTestClass { 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 sqrt(float) of C#"); result = CSharpSqrt(iterationCount, ref timeUsed1); Console.WriteLine("Result = {0}", result); Console.WriteLine("Testing float sqrt(float) of Managed C++ Wrapper."); result = ManagedCppWrapper_Sqrt(iterationCount, ref timeUsed2); Console.WriteLine("Result = {0}", result); Console.WriteLine("Testing float sqrt(float) of C# Wrapper"); result = CSharpWrapper_Sqrt(iterationCount, ref timeUsed3); Console.WriteLine("Result = {0}", result); Console.WriteLine("Testing float sqrt(float) of C# Wrapper with Direct Call."); result = CSharpWrapperDirectCall_Sqrt(iterationCount, ref timeUsed4); Console.WriteLine("Result = {0}", result); Console.WriteLine("================================================================================="); Console.WriteLine("C# Sqrt - Average Time used: {0}ms", timeUsed1 / (i + 1)); Console.WriteLine("Managed C++ Wrapper Sqrt- 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 sqrt(float)============================="); Console.WriteLine(); Console.WriteLine("Total Iteration: {0}", iterationCount); Console.WriteLine(); Console.WriteLine("Method of float sqrt(float) Time Percentage"); Console.WriteLine(); Console.WriteLine("C# Managed sqrt {0} ms 1.0", (timeUsed1 / testCount).ToString("00.000")); Console.WriteLine("C++ PInvoke, Managed Wrapper of Native sqrt {0} ms {1}", (timeUsed2 / testCount).ToString("00.000"), (timeUsed2 / timeUsed1).ToString("0.000")); Console.WriteLine("Explicit PInvoke C# Wrapper of Native sqrt {0} ms {1}", (timeUsed3 / testCount).ToString("00.000"), (timeUsed3 / timeUsed1).ToString("0.000")); Console.WriteLine("Explicit PInvoke C# Wrapper with Direct Call of Native sqrt {0} ms {1}", (timeUsed4 / testCount).ToString("00.000"), (timeUsed4 / timeUsed1).ToString("0.000")); Console.WriteLine(); Console.WriteLine("======================================================================================="); } static float CSharpSqrt(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 = Sqrt(value + result); } DateTime end = DateTime.Now; timeUsed += (end - start).TotalMilliseconds; return result; } static float ManagedCppWrapper_Sqrt(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++) { result = cal.sqrt(value + result); } DateTime end = DateTime.Now; timeUsed += (end - start).TotalMilliseconds; return result; } static float CSharpWrapper_Sqrt(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 = cal.sqrt(value + result); } DateTime end = DateTime.Now; timeUsed += (end - start).TotalMilliseconds; return result; } static float CSharpWrapperDirectCall_Sqrt(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 = LibCalculatorWin32.Calculator_sqrt(cal.ptrObject, value + result); } DateTime end = DateTime.Now; timeUsed += (end - start).TotalMilliseconds; return result; } static float Sqrt(float x) { return (float)Math.Sqrt(x); } } }
Here is the result for calling float sqrt(float) from C# via all different kinds of wrappers.
The C# version did not perform much faster than the explicit Pinvoke C# wrapper whose method call is made directly on the PInvoke method without going through the C# wrapper class of Calculator. That is because the C# version of Math.Sqrt accepts a double parameter while we passed float type, type conversion happens when we call Sqrt, type conversion happens again when we get back the return value of double.
Calling sqrt directly via the pinvoke method is faster than calling sqrt method of the C# wrapper class of Calculator because there is less stack push and pop.
The C++/CLI wrapper presents the worst performance. I was not surprised because C++/CLI wrapper adds additional layer to make the call and the underlying mechanism of calling into the C++ DLL is still the same, P/Invoke, it is implicit though.
Here is testing class for double version of sqrt and its result for the method of double sqrt(double x).
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 CalculatorManagedWrapper; public class SqrtDoubleTestClass { public static void Test(int testCount, int iterationCount) { double timeUsed1 = 0, timeUsed2 = 0, timeUsed3 = 0, timeUsed4 = 0; int i = 0; for (; i < testCount; i++) { double result; Console.WriteLine("Testing double sqrt(double) of C#"); result = CSharpSqrt(iterationCount, ref timeUsed1); Console.WriteLine("Result = {0}", result); Console.WriteLine("Testing double sqrt(double) of Managed C++ Wrapper."); result = ManagedCppWrapper_Sqrt(iterationCount, ref timeUsed2); Console.WriteLine("Result = {0}", result); Console.WriteLine("Testing double sqrt(double) of C# Wrapper"); result = CSharpWrapper_Sqrt(iterationCount, ref timeUsed3); Console.WriteLine("Result = {0}", result); Console.WriteLine("Testing double sqrt(double) of C# Wrapper with Direct Call."); result = CSharpWrapperDirectCall_Sqrt(iterationCount, ref timeUsed4); Console.WriteLine("Result = {0}", result); Console.WriteLine("================================================================================="); Console.WriteLine("C# Sqrt - Average Time used: {0}ms", timeUsed1 / (i + 1)); Console.WriteLine("Managed C++ Wrapper Sqrt- 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 sqrt(double)============================"); Console.WriteLine(); Console.WriteLine("Total Iteration: {0}", iterationCount); Console.WriteLine(); Console.WriteLine("Method of double sqrt(double) Time Percentage"); Console.WriteLine(); Console.WriteLine("C# Managed sqrt {0} ms 1.0", (timeUsed1 / testCount).ToString("00.000")); Console.WriteLine("C++ PInvoke, Managed Wrapper of Native sqrt {0} ms {1}", (timeUsed2 / testCount).ToString("00.000"), (timeUsed2 / timeUsed1).ToString("0.000")); Console.WriteLine("Explicit PInvoke C# Wrapper of Native sqrt {0} ms {1}", (timeUsed3 / testCount).ToString("00.000"), (timeUsed3 / timeUsed1).ToString("0.000")); Console.WriteLine("Explicit PInvoke C# Wrapper with Direct Call of Native sqrt {0} ms {1}", (timeUsed4 / testCount).ToString("00.000"), (timeUsed4 / timeUsed1).ToString("0.000")); Console.WriteLine(); Console.WriteLine("========================================================================================"); } static double ManagedCppWrapper_Sqrt(int count, ref double timeUsed) { DateTime start = DateTime.Now; var cal = new ManagedCalculator(); double result = 0f; double fCount = (double)count; for (double value = 0; value < fCount; value++) { result = cal.sqrt(value + result); } DateTime end = DateTime.Now; timeUsed += (end - start).TotalMilliseconds; return result; } static double CSharpWrapper_Sqrt(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 = cal.sqrt(value + result); } DateTime end = DateTime.Now; timeUsed += (end - start).TotalMilliseconds; return result; } static double CSharpWrapperDirectCall_Sqrt(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 = LibCalculatorWin32.Calculator_sqrt(cal.CppPointer, value + result); } DateTime end = DateTime.Now; timeUsed += (end - start).TotalMilliseconds; return result; } static double CSharpSqrt(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 = Sqrt(value + result); } DateTime end = DateTime.Now; timeUsed += (end - start).TotalMilliseconds; return result; } static double Sqrt(double x) { return Math.Sqrt(x); } } }
The result of C++/CLI, PInvoke Performance
This time, the C# managed sqrt performs better than the float version because it does not need to convert the input parameter and convert back the return value any more, and conversion back and forth does take time.
And, unfortunately, the C++/CLI wrapper is again the worst performer.
To be fair to C++/CLI, I would mention that the
[SuppressUnmanagedCodeSecurity]
was added to the PInvoke declaration when building the C# Wrapper for the native C++ DLL.
[SuppressUnmanagedCodeSecurity] [DllImport("LibCalculator.dll", EntryPoint="#28", CallingConvention=CallingConvention.ThisCall)] public extern static float Calculator_sqrt(IntPtr thisObject, float x); [SuppressUnmanagedCodeSecurity] [DllImport("LibCalculator.dll", EntryPoint="#29", CallingConvention=CallingConvention.ThisCall)] public extern static double Calculator_sqrt(IntPtr thisObject, double x);
Stay tuned for the second part of the blog of C++/CLI vs PInvoke Performance.