C# EMIT IL 性能问题

标签 c# .net optimization compiler-construction cil

我正在开发一个引擎,我们可以在运行时动态复制大量属性。根据情况,我们可能会也可能不会修改属性值。它最初是用反射编写的,但由于性能问题,我们最近在 Reflection.Emit 中重新编写了它。重写完成,性能明显好很多,但现在代码正在与手写的 C# 进行基准测试。显然,为了公平竞争,用于基准测试的手写 C#IL 具有“相似的功能”(您很快就会明白我的意思) .

一些 IL 引擎已经获得批准,因为它已经出色地通过了测试,并且与手写的 C# 几乎是 1:1。这告诉我:

  1. 调用动态方法没有任何开销

  2. 我们的总体概念和实现是正确的

  3. 基准测试正确

  4. IL 和手写的 C# 正在以完全相同的方式进行测试,因此不会发生有趣的 JIT 业务(我不认为)

我们原本期望 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 copy/ToString 并取消注释其他 5 个属性,我将回到 1:1 与 C#.

您会注意到 I2int -> string,但由于某种原因,那个没有同样的问题与 double -> string 一样。我知道 double 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 对象发出新的,这是在其他地方完成的。 dup 正在欺骗 Dest 对象,如上面的 C# 所示。

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 导致了问题。

最佳答案

正如您在 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 生成的方法。

关于C# EMIT IL 性能问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36961159/

相关文章:

java - 优化变量声明原语与空对象

c# - 输出缓存不起作用

c# - AutoMapper 版本 9.0.0 - IMappingOperationOptions 上没有 ConfigureMap() 方法

c# - 返回任务的命名方法的公认模式是什么?

c# - 将 ReflectionOnlyType 与 Type 进行比较的推荐方法是什么?

sql - Oracle优化器会在同一SELECT中使用多个提示吗?

c# - 不必要的大括号会降低性能吗?

c# - C# SSH .Net 库中的 Sudo 命令

c# - 谷歌客户端登录授权和谷歌音乐

c# - AOP Postsharp,记录变量值