14

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

public class CsClassWithProps
{
    public CsClassWithProps()
    {
        CreateDate = DateTime.Now;
    }

    public long Id { get; set; }
    public string Name { get; set; }
    public DateTime CreateDate { get; set; }
}

我注意到的一件事是,平均而言,对于创建列表然后构建具有偶数 ID 的所有对象的子列表的简单测试,C++/CLI 代码在我的开发机器(64 位 Win8 , 8GB 内存)。例如,创建和过滤 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++ 代码使用本地来存储调用的值 from DateTime.Now?发生这种情况是否有特定于 C++ 的原因,或者仅仅是他们选择实现编译器的方式?

我已经知道还有许多其他方法可以提高性能,而且我知道我离兔子洞还很远,但我很想知道是否有人可以对此有所了解。自从我做 C++ 以来已经很久了,随着 Windows 8 的出现,以及微软对 C++ 的重新关注,我认为刷新一下会很好,这也是我进行这个练习的动机的一部分,但是两个编译器输出之间的差异引起了我的注意。

4

2 回答 2

6
System::DateTime CreateDateTime;

这听起来像是一个技巧问题。您发布的 IL 肯定不会由您发布的代码段生成。您对 CreateDateTime 成员的实际声明是:

System::DateTime^ CreateDateTime;

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

否则,您尝试使用 C++/CLI 加快代码速度是徒劳的。只要您在 C++/CLI 中编写托管代码,您就会获得与 C# 编译器生成的相同类型的 IL。C++/CLI 的价值在于它能够非常轻松且廉价地调用非托管代码。然而,使用这样的代码不太可能产生好的结果。您调用的非托管代码必须是“实质性的”,以便您从托管代码执行切换到非托管代码执行所产生的惩罚可以忽略不计。对于不需要任何数据转换的简单转换,该成本徘徊在几个 CPU 周期之间。当您需要执行诸如引脚数组或转换字符串之类的事情时,需要数百个周期。

于 2012-12-27T19:10:40.313 回答
2

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

public ref class CppClassWithMembers
{
public:
    long long Id;
    System::DateTime CreateDateTime;
    System::String^ Name;

    CppClassWithMembers() : CreateDateTime(System::DateTime::Now) { }
};
于 2012-12-27T18:16:45.217 回答