我正在开发一个引擎,我们可以在运行时动态复制大量属性。根据情况,我们可能会也可能不会修改属性值。它最初是用反射编写的,但由于性能问题,我们最近在 Reflection.Emit
中重新编写了它。重写完成,性能明显好很多,但现在代码正在与手写的 C#
进行基准测试。显然,为了公平竞争,用于基准测试的手写 C#
与 IL
具有“相似的功能”(您很快就会明白我的意思) .
一些 IL
引擎已经获得批准,因为它已经出色地通过了测试,并且与手写的 C#
几乎是 1:1。这告诉我:
调用动态方法没有任何开销
我们的总体概念和实现是正确的
基准测试正确
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#
.
您会注意到 I2
是 int
-> 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。 int
、long
等。出于某种原因,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.s
和 ldloc.s
,它们也采用本地索引的操作数.
还要确保您缓存(如果 C#
运行速度仅快两倍,则可能是这样)针对每个 Type
生成的方法。
关于C# EMIT IL 性能问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36961159/