C#EMIT IL性能问题

时间:2016-04-30 22:59:13

标签: c# .net optimization compiler-construction cil

我正在开发一个引擎,我们在运行时动态地复制很多很多属性。根据具体情况,我们可能会或可能不会修改房产价值。它最初是用反射写的,但由于性能问题,我们最近在Reflection.Emit重新编写了它。重写是完整的,性能显然要好得多,但现在代码正在针对手写C#进行基准测试。显然,为了公平对抗,基准的手写C#具有类似的功能" (你会在一秒内看到我的意思)IL

部分IL引擎已经签署,因为它已经通过了飞行的颜色,并且与手写的C#几乎是1:1。这告诉我:

  1. 调用动态方法

  2. 没有任何开销
  3. 我们的一般概念和实施是正确的

  4. 基准测试是正确的

  5. IL和手写的C#正在以完全相同的方式进行测试,因此没有有趣的JIT业务正在进行(我不会想到)

  6. 我们期待IL比手写略慢,但到目前为止情况并非如此。在长轮中它可能会慢几毫秒,但你可以在IL中选择快捷方式,这样就可以弥补差异。

    在一个特殊情况下,它的速度要慢得多。慢了2倍。

    C#中,您有:

    class Source
    {
        public string S1 { get; set; }
        public int I1 { get; set; }
        public int I2 { get; set; }
        public double D1 { get; set; }
        public double D2 { get; set; }
        public double D3 { get; set; }
    }
    
    class Dest
    {
        public string S1 { get; set; }
        public int I1 { get; set; }
        public string I2 { get; set; }
        public double D1 { get; set; }
        public int D2 { get; set; }
        public string D3 { get; set; }
    }
    
    static Dest Test(Source s)
    {
        Dest d = new Dest();
    
        object o = s.D3;
    
        if (o != null)
            d.D3 = o.ToString();
    
        return d;
    }
    

    这就是我所说的类似功能。为了通用,当我们将属性复制到字符串时,我们首先将其打包然后调用Object.ToString()。本地,值类型调用ToString不同,因此上面的代码是苹果到苹果。

    如果我注释掉D3副本/ ToString并取消注释其他5个属性,我会使用C#返回1:1。

    您会注意到I2int - > string,但出于某种原因,该问题与double - >的问题不同。 string。我认为双ToString()一般来说更贵,但这笔费用也应该出现在C#代码中,但它并没有。

    我为D3副本发出的代码与我为I2副本发出的代码相同,为什么D3副本的开销很大?

    编辑:

    编译器发出:

    IL_0000: newobj instance void ConsoleApplication3.Dest::.ctor()
        IL_0005: ldarg.0
        IL_0006: callvirt instance float64 ConsoleApplication3.Source::get_D3()
        IL_000b: box [mscorlib]System.Double
        IL_0010: stloc.0
        IL_0011: dup
        IL_0012: ldloc.0
        IL_0013: brtrue.s IL_0018
    
        IL_0015: ldnull
        IL_0016: br.s IL_001e
    
        IL_0018: ldloc.0
        IL_0019: callvirt instance string [mscorlib]System.Object::ToString()
    
        IL_001e: callvirt instance void ConsoleApplication3.Dest::set_D3(string)
        IL_0023: ret
    

    我的代码的这一特定部分不会为Dest对象发出新内容,而是在其他地方完成。如上面C#所示,dup正在使用Dest对象。

    LocalBuilder localBuilderObject = generator.DeclareLocal(_typeOfObject);
    
    Label labelNull = generator.DefineLabel();
    Label labelNotNull = generator.DefineLabel();
    
    generator.Emit(OpCodes.Ldarg_0);
    generator.Emit(OpCodes.Callvirt, miGetter);
    generator.Emit(OpCodes.Box, typeSource);
    generator.Emit(OpCodes.Stloc_S, localBuilderObject);
    generator.Emit(OpCodes.Dup);
    generator.Emit(OpCodes.Ldloc_S, localBuilderObject);
    generator.Emit(OpCodes.Brtrue, labelNotNull);
    generator.Emit(OpCodes.Ldnull);
    generator.Emit(OpCodes.Br, labelNull);
    generator.MarkLabel(labelNotNull);
    generator.Emit(OpCodes.Ldloc_S, localBuilderObject);
    generator.Emit(OpCodes.Callvirt, _miToString);
    generator.MarkLabel(labelNull);
    generator.Emit(OpCodes.Callvirt,miSetter);
    

    正如我所提到的,我选择了类型,因此我可以通常调用Object::ToString()而不必担心值类型。参考类型也经历了这条路径。 C#代码的行为与此类似,仍然需要1/2的时间???

    整个周末我一直在搞乱这个问题。进一步测试显示其他值类型为1:1。 intlong等。出于某种原因,double导致了问题。

2 个答案:

答案 0 :(得分:0)

跳过null(brfalse)而不是双跳。根据您调用生成代码的方式(未在此处发布),您的基准测试可能有误3个原因:

  1. 你只能用委托来调用它(如果你打电话不在其他生成的代码中执行)
  2. 必须使用委托调用您的常规代码才能进行比较。
  3. 委托非静态方法比静态方法的委托构建更快(clr将推送null,在真正的处理之前跳转并弹出未使用的空值)。您必须使用第一个未使用的参数(引用类型)生成静态方法,并调用Delegate.CreateDelegate,并将目标明确指定为null,以防止它出现。

答案 1 :(得分:0)

正如您在C#编译代码中所看到的,使用了快速本地访问指令:

IL_000b: box [mscorlib]System.Double
IL_0010: stloc.0
IL_0011: dup
IL_0012: ldloc.0
...
IL_0018: ldloc.0

相反,在IL生成的代码中,您使用stloc.sldloc.s,它们也会获取本地索引的操作数。

同时确保缓存(如果C#仅运行两倍),生成的方法可以为Type生成的方法进行缓存。