【发布时间】:2023-03-12 08:36:01
【问题描述】:
我正在开发一个引擎,我们可以在运行时动态复制大量属性。根据情况,我们可能会或可能不会沿途修改属性值。最初是用反射写的,但由于性能问题,我们最近在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 个属性,我将回到 1:1 与 C#。
您会注意到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() 而不必担心值类型。 Ref 类型也经过这条路径。 C# 代码的行为是这样的,但仍然需要 1/2 的时间???
我整个周末都在处理这个问题。进一步测试显示其他值类型为 1:1。 int、long 等。由于某种原因,double 引起了问题。
【问题讨论】:
-
我发现您的代码有点令人困惑:您加载 this 参数,获取字段值,将其装箱,然后将其存储在本地。那你复制什么?此外,请包括您发出的实际 IL 以及 C# 为其版本生成的实际 IL。
-
@500-InternalServerError,感谢您的回复。我已经更新了原始帖子以摆脱 IL 包装器(尽管它没有添加任何其他操作码或任何东西)。副本是 Dest 对象。我的代码中有一些差异:stloc_s/ldloc_s 而不是 stloc.0/ldloc.0 和分支与分支短裤。 Int/long 在 C# 性能方面是 1:1,所以它只是双倍的。
-
在这个级别的微基准测试中,页面上的代码对齐之类的愚蠢事情足以让你大吃一惊。您还必须深入了解抖动生成的实际代码,以查看是否存在任何实际差异;只是继续盯着 IL 是没有帮助的。
-
@JeroenMostert,2x 并不完全是微基准测试:)。在 5 分钟和 10 分钟之间产生差异 :)。
-
Hmmm.... 即使只是在循环中调用 someDouble.ToString() 所需的时间是 64 位的 2 倍。所以我想这就是问题所在。与 IL 无关。
标签: c# .net optimization compiler-construction cil