我正在开发一个引擎,我们在运行时动态地复制很多很多属性。根据具体情况,我们可能会或可能不会修改房产价值。它最初是用反射写的,但由于性能问题,我们最近在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
副本/ ToString
并取消注释其他5个属性,我会使用C#
返回1:1。
您会注意到I2
是int
- > 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。 int
,long
等。出于某种原因,double
导致了问题。
答案 0 :(得分:0)
跳过null
(brfalse)而不是双跳。根据您调用生成代码的方式(未在此处发布),您的基准测试可能有误3个原因:
答案 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.s
和ldloc.s
,它们也会获取本地索引的操作数。
同时确保缓存(如果C#
仅运行两倍),生成的方法可以为Type
生成的方法进行缓存。