I have a WPF application that does a lot of matching across large datasets, and currently it uses C# and LINQ to match POCOs and display in a grid. As the number of datasets included has increased, and the volume of data has increased, I've been asked to look at performance issues. One of the assumptions that I was testing this evening was whether there's a substantive difference if we were to convert some of the code to C++ CLI. To that end I wrote a simple test that creates a List<>
with 5,000,000 items, and then does some simple matching. The basic object structure is:
public class CsClassWithProps
{
public CsClassWithProps()
{
CreateDate = DateTime.Now;
}
public long Id { get; set; }
public string Name { get; set; }
public DateTime CreateDate { get; set; }
}
One thing that I noticed was that on average, for the simple test of creating the list and then building a sub-list of all objects with an even ID, the C++/CLI code was about 8% slower on my development machine (64bit Win8, 8GB of RAM). For example, the case of a C# object being created and filtered took ~7 seconds, while the C++/CLI code took ~8 seconds on average. Curious as to why this would be, I used ILDASM to see what was happening under the covers, and was surprised to see that the C++/CLI code has extra steps in the constructor. First the test code:
static void CreateCppObjectWithMembers()
{
List<CppClassWithMembers> results = new List<CppClassWithMembers>();
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < Iterations; i++)
{
results.Add(new CppClassWithMembers() { Id = i, Name = string.Format("Name {0}", i) });
}
var halfResults = results.Where(x => x.Id % 2 == 0).ToList();
sw.Stop();
Console.WriteLine("Took {0} total seconds to execute", sw.Elapsed.TotalSeconds);
}
The C# class is above. The C++ class is defined as:
public ref class CppClassWithMembers
{
public:
long long Id;
System::DateTime CreateDateTime;
System::String^ Name;
CppClassWithMembers()
{
this->CreateDateTime = System::DateTime::Now;
}
};
When I extract the IL for both classes' constructors, this is what I get. First the C#:
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
// Code size 21 (0x15)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: nop
IL_0007: nop
IL_0008: ldarg.0
IL_0009: call valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now()
IL_000e: stfld valuetype [mscorlib]System.DateTime CsLibWithMembers.CsClassWithMembers::CreateDate
IL_0013: nop
IL_0014: ret
} // end of method CsClassWithMembers::.ctor
And then the C++:
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
// Code size 25 (0x19)
.maxstack 2
.locals ([0] valuetype [mscorlib]System.DateTime V_0)
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: call valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now()
IL_000b: stloc.0
IL_000c: ldarg.0
IL_000d: ldloc.0
IL_000e: box [mscorlib]System.DateTime
IL_0013: stfld class [mscorlib]System.ValueType modopt([mscorlib]System.DateTime) modopt([mscorlib]System.Runtime.CompilerServices.IsBoxed) CppLibWithMembers.CppClassWithMembers::CreateDateTime
IL_0018: ret
} // end of method CppClassWithMembers::.ctor
My question is: why is the C++ code using the local to store the value of the call from DateTime.Now
? Is there a C++-specific reason for this to happen, or is it just how they chose to implement the compiler?
I know already that there are many other ways to improve performance, and I know that I'm pretty far down the rabbit hole as it is, but I was curious to know if anyone could shed some light on this. It's been a long time since I've done C++, and with the advent of Windows 8, and Microsoft's renewed focus on C++, I thought it would be good to refresh, and that was also part of my motivation for this exercise, but the difference between the two compiler outputs caught my eye.
System::DateTime CreateDateTime;
This sounds like a trick question. The IL you posted most certainly won't be generated by the snippet you posted. Your actual declaration of the CreateDateTime member was:
System::DateTime^ CreateDateTime;
Clearly visible in the IL you posted. It produced the boxing conversion to convert the value type value to a reference object. This is a very common mistake in C++/CLI, much too easy to accidentally type the hat. One that the compiler really ought to generate a warning for, but doesn't. And yes, it bogs code down, the boxing conversion doesn't come for free.
Your attempt to speed code up by using C++/CLI is otherwise a lost cause. As long as you write managed code in C++/CLI, you'll get the same kind of IL that the C# compiler generates. The value of C++/CLI is its ability to very easily and cheaply call unmanaged code. That is however unlikely to produce good results either with code like this. The unmanaged code you call must be "substantive" so that the penalty you incur from switching from managed to unmanaged code execution is negligible. That cost hovers between a handful of CPU cycles for a simple transition that doesn't need any data conversion. To hundreds of cycles when you need to do things like pin arrays or convert strings.
A C++ version that is closer to what the C# compiler does (and gets rid of the expensive box
) would be this:
public ref class CppClassWithMembers
{
public:
long long Id;
System::DateTime CreateDateTime;
System::String^ Name;
CppClassWithMembers() : CreateDateTime(System::DateTime::Now) { }
};
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With