C#编译器和C++/CLI编译器输出之间的差异

本文关键字:编译器 之间 输出 CLI C++ | 更新日期: 2023-09-27 17:49:23

我有一个WPF应用程序,它在大型数据集之间进行大量匹配,目前它使用C#和LINQ来匹配POCO并在网格中显示。随着包含的数据集数量的增加和数据量的增加,我被要求研究性能问题。我今晚测试的一个假设是,如果我们将部分代码转换为C++CLI,是否会有实质性的差异。为此,我编写了一个简单的测试,创建了一个包含5000000个项目的List<>,然后进行一些简单的匹配。基本对象结构为:

public class CsClassWithProps
{
    public CsClassWithProps()
    {
        CreateDate = DateTime.Now;
    }
    public long Id { get; set; }
    public string Name { get; set; }
    public DateTime CreateDate { get; set; }
}

我注意到的一件事是,对于创建列表,然后构建一个包含偶数ID的所有对象的子列表的简单测试,在我的开发机器上(64位Win8,8GB RAM(,C++/CLI代码的速度平均慢了8%。例如,创建和筛选C#对象的情况大约需要7秒,而C++/CLI代码平均需要8秒。出于好奇,我使用ILDASM查看了背后发生的事情,并惊讶地发现C++/CLI代码在构造函数中有额外的步骤。首先测试代码:

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);
}

C#类在上面。C++类定义为:

public ref class CppClassWithMembers
{
public:
    long long Id;
    System::DateTime CreateDateTime;
    System::String^ Name;
    CppClassWithMembers()
    {
        this->CreateDateTime = System::DateTime::Now;
    }
};

当我为两个类的构造函数提取IL时,我得到的就是这个。首先是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

然后是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

我的问题是:为什么C++代码使用local来存储来自DateTime.Now的调用的值?发生这种情况是否有C++特定的原因,或者只是他们选择如何实现编译器?

我已经知道还有很多其他的方法可以提高成绩,我也知道我现在还很落后,但我很想知道是否有人能对此有所了解。我已经很久没有学习C++了,随着Windows 8的出现,以及微软对C++的重新关注,我认为刷新是件好事,这也是我进行这项练习的部分动机,但两种编译器输出之间的差异引起了我的注意。

C#编译器和C++/CLI编译器输出之间的差异

System::DateTime CreateDateTime;

这听起来像是一个恶作剧问题。你发布的IL肯定不会由你发布的片段生成。您对CreateDateTime成员的实际声明是:

System::DateTime^ CreateDateTime;

在你发布的IL中清晰可见。它生成装箱转换,将值类型值转换为引用对象。这是C++/CLI中非常常见的错误,很容易意外地键入帽子。编译器确实应该为其生成警告,但没有。是的,它让代码陷入困境,拳击转换不是免费的。

否则,您试图通过使用C++/CLI来加快代码速度是徒劳的。只要您在C++/CLI中编写托管代码,您就会得到与C#编译器生成的IL相同的IL。C++/CLI的价值在于它能够非常轻松、廉价地调用非托管代码。然而,这样的代码也不太可能产生好的结果。您调用的非托管代码必须是"实质性的",这样从托管代码执行切换到非托管代码执行所带来的损失可以忽略不计。对于一个不需要任何数据转换的简单转换,该成本徘徊在几个CPU周期之间。当你需要做像pin数组或转换字符串这样的事情时,要进行数百次循环。

一个更接近C#编译器的C++版本(并摆脱了昂贵的box(将是这样的:

public ref class CppClassWithMembers
{
public:
    long long Id;
    System::DateTime CreateDateTime;
    System::String^ Name;
    CppClassWithMembers() : CreateDateTime(System::DateTime::Now) { }
};