Quantcast
Channel: xInterop C++ .NET Bridge
Viewing all articles
Browse latest Browse all 31

C# vs C++/CLI vs PInvoke Performance – Part II

$
0
0

C++/CLI vs Explicit PInvoke Performance – Part II

Today, I am going to continue the comparison of C++/CLI vs Explict PInvoke performance. It is the second part of the blog, you may want to read the first part if you have not already.

string or String is commonly used in .NET, if you know nothing about C++/CLI, you would expect it to be able to convert or marshal a .NET string to c string such as char* transparently with no data copy at all, but you will be disappointed to see the a different way of how C++/CLI actually bridges string between managed world and the native world. We will be talking about this later.

In order to test the performance of using the C++/CLI managed wrapper and a C# wrapper generated by our .NET PInvoke Interop SDK – A C# Wrapper Generator for C++ DLL, we will need to create a C++ class inside a native C++ DLL. We are going to use a sample C++ DLL from now on. We are going to write a single method IndexOf in a C++ class named StringHelper, the method is used to find the index of a key string in a value string. Here is the implementation of the C++ class.

#ifndef __STRING_HELPER_H__
#define __STRING_HELPER_H__

#pragma once

#include <string>

class SAMPLE_API StringHelper
{

public:

	StringHelper() {};

	~StringHelper() {};

	int IndexOf(const char* szValue, const char* szKey)
	{
		if (szValue == NULL || szKey == NULL)
			return -1;

		const char* p = strstr(szValue, szKey);
		if (p != NULL)
		{
			return (int)(p - szValue);
		}
		else
		{
			return -1;
		}
	};
};

#endif

In the preceding example of IndexOf method, we used strstr to find the pointer of the key string value, we did not use std::string::find method to find the index of the key string because std::string:find is far less efficient than strstr, and then it would undermine the efficiency of the Explicit P/Invoke when we do the performance comparison of C++/CLI vs Explicit P/Invoke.

Once again, let’s write the C++/CLI wrapper class first, it is a simple one because we have only one class with one method we will be using, if there are many of them, it is tedious, time-consuming, hard-to-maintain, because everything you want from the original C++ class, you will need to wrap them in C++/CLI, it has no such intelligence allowing you to automatically find the methods and wrap them for you.

#pragma once

#include "StringHelper.h"

#include "msclr\marshal_cppstd.h"

using namespace System;
using namespace Runtime::InteropServices;
using namespace msclr::interop;

namespace SampleManagedWrapper {

	public ref class ManagedStringHelper
	{
		StringHelper* pStringHelper;

	public:

		ManagedStringHelper()
		{
			pStringHelper = new StringHelper();
		}

		~ManagedStringHelper()
		{
			delete pStringHelper;
		}

		int IndexOf(String^ value, String^ key)
		{
			marshal_context context;
			return pStringHelper->IndexOf(context.marshal_as<const char*>(value), context.marshal_as<const char*>(key));
		}
	};
}

Remember I stated that the string conversion is not transparent, you would have to manually convert the .NET string to C++ const char*, which is being shown in the preceding code. That is where it performs so much worse than Explicit P/Invoke, I will show you the result later. In order to use a string parameter in C++/CLI to call into the native C++ DLL, we are responsible for converting it to const char* by using a class called marshal_context, I appreciate such a type conversion class is provided by C++/CLI, the issue with this kind of type conversion is it slows down the whole process of calling the native C++ classes, which makes me unwilling to use C++/CLI, not mentioning that I still get to maintain the code whenever I change the pubic interfaces of my C++ classes.

It took me only less than 5 seconds to use our PInvoke Interop SDK Tools to generate a C# wrapper for the sample C++ DLL. A new class named StringHelper is ready for me to use in C# code. I will show you the P/Invoke signature and the wrapper code.

[SuppressUnmanagedCodeSecurity]
[DllImport("Sample.dll", EntryPoint="#29", CallingConvention=CallingConvention.ThisCall)]
public extern static int StringHelper_IndexOf(IntPtr thisObject, string szValue, string szKey);

public int IndexOf(string szValue, string szKey)
{
    // this.ptrObject is the C++ pointer pointing to the C++ object.
    return StringHelper_IndexOf(this.ptrObject, szValue, szKey);
}

Now, let’s write the C# code to test the performance of C++/CLI and Explicit PInvoke. I would also want to know the performance difference of the C# wrapper for the native version of IndexOf and the C# version of String.IndexOf. Here is the complete test class.

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 StringHelperTestClass
    {
        public static void Test(int testCount, int iterationCount)
        {
            double timeUsed1 = 0, timeUsed2 = 0, timeUsed3 = 0, timeUsed4 = 0;
            for (int i = 0; i < testCount; i++)
            {
                int result;
                Console.WriteLine("Testing int IndexOf(string, string) of C#");
                result = CSharp_IndexOf(iterationCount, ref timeUsed1);
                Console.WriteLine("Result = {0}", result);

                Console.WriteLine("Testing int IndexOf(string, string) of Managed C++ Wrapper.");
                result = ManagedCppWrapper_IndexOf(iterationCount, ref timeUsed2);
                Console.WriteLine("Result = {0}", result);

                Console.WriteLine("Testing int IndexOf(string, string) of C# Wrapper");
                result = CSharpWrapper_IndexOf(iterationCount, ref timeUsed3);
                Console.WriteLine("Result = {0}", result);

                Console.WriteLine("Testing int IndexOf(string, string) of C# Wrapper with Direct Call.");
                result = CSharpWrapperDirectCall_IndexOf(iterationCount, ref timeUsed4);
                Console.WriteLine("Result = {0}", result);

                Console.WriteLine("=================================================================================");
                Console.WriteLine("C# -  Average Time used:                              {0}ms", timeUsed1 / (i + 1));
                Console.WriteLine("Managed C++ Wrapper - 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 int IndexOf(string, string)=============================");
            Console.WriteLine();
            Console.WriteLine("Total Iteration: {0}", iterationCount);
            Console.WriteLine();
            Console.WriteLine("Method of int IndexOf(string, string)                    Time         Percentage");
            Console.WriteLine();
            Console.WriteLine("C# Manged Implementation                                 {0} ms       1.0", (timeUsed1 / testCount).ToString("000.000"));
            Console.WriteLine("C++ PInvoke, Managed Wrapper                             {0} ms       {1}", (timeUsed2 / testCount).ToString("000.000"), (timeUsed2 / timeUsed1).ToString("0.000"));
            Console.WriteLine("Explicit PInvoke C# Wrapper                              {0} ms       {1}", (timeUsed3 / testCount).ToString("000.000"), (timeUsed3 / timeUsed1).ToString("0.000"));
            Console.WriteLine("Explicit PInvoke C# Wrapper with Direct Call             {0} ms       {1}", (timeUsed4 / testCount).ToString("000.000"), (timeUsed4 / timeUsed1).ToString("0.000"));
            Console.WriteLine();
            Console.WriteLine("=======================================================================================");
        }

        static int CSharp_IndexOf(int count, ref double timeUsed)
        {
            DateTime start = DateTime.Now;                        
            string value = "123456789099765432112334567789899765655444";
            int result = 0;
            for (int i = 0; i < count; i++)
            {
                result += value.IndexOf(i.ToString());
            }

            DateTime end = DateTime.Now;
            timeUsed += (end - start).TotalMilliseconds;
            return result;
        }

        static int ManagedCppWrapper_IndexOf(int count, ref double timeUsed)
        {
            DateTime start = DateTime.Now;
            var stringHelper = new ManagedStringHelper();
            string value = "123456789099765432112334567789899765655444";
            int result = 0;
            for (int i = 0; i < count; i++)
            {                
                result += stringHelper.IndexOf(value, i.ToString());
            }

            DateTime end = DateTime.Now;
            timeUsed += (end - start).TotalMilliseconds;
            return result;
        }

        static int CSharpWrapper_IndexOf(int count, ref double timeUsed)
        {
            DateTime start = DateTime.Now;
            var stringHelper = new StringHelper();
            string value = "123456789099765432112334567789899765655444";
            int result = 0;
            for (int i = 0; i < count; i++)
            {
                result += stringHelper.IndexOf(value, i.ToString());
            }

            DateTime end = DateTime.Now;
            timeUsed += (end - start).TotalMilliseconds;
            return result;
        }

        static int CSharpWrapperDirectCall_IndexOf(int count, ref double timeUsed)
        {
            DateTime start = DateTime.Now;
            var stringHelper = new StringHelper();
            string value = "123456789099765432112334567789899765655444";
            int result = 0;
            for (int i = 0; i < count; i++)
            {                
                result += LibCalculatorWin32.StringHelper_IndexOf(stringHelper.ptrObject, value, i.ToString());
            }

            DateTime end = DateTime.Now;
            timeUsed += (end - start).TotalMilliseconds;
            return result;
        }
    }

I was surprised to see the testing result, i ran the testing program a few times with different iteration numbers, but they all gave me similar result. Just like what you can see the following picture, the wrapper class generated by our PInvoke Interop SDK performs very close to the C# version of String.IndexOf.

The C# wrapper generated by our PInvoke Interop SDK Tools is 3 times faster than C++/CLI wrapper, it is so close to the C# version of String.IndexOf. From my experience of working with so many C++ DLL, I would say there is not much string copy at all when passing a string value to C++ DLL using Explicit P/Invoke when the C++ method accepts char* as parameter, I don’t know the details of .NET implementation though.

C++/CLI wrapper is once again the worst performer when passing string(char* type) from C# to the C++ DLL.

Stay tuned, we will be comparing the performance when using std::string in the C++ class next time. I hope the C++/CLI wrapper for C++ DLL could perform better.

 


Viewing all articles
Browse latest Browse all 31

Latest Images

Trending Articles



Latest Images