包含原始值类型的结构是否是C#中的零成本抽象?

时间:2017-07-12 16:47:52

标签: c# performance cil ryujit

有时我想在原始双打周围增加更多类型的安全性。出现的一个想法是添加类型的单位信息。例如,

struct AngleRadians {
  public readonly double Value;
  /* Constructor, casting operator to AngleDegrees, etc omitted for brevity... */
}

在如上所述的情况下,只有一个字段,JIT能否在所有情况下优化这种抽象?与使用未包装的double的类似代码相比,什么情况(如果有的话)将导致额外生成的机器指令?

任何关于过早优化的提及都将被低估。我对了解真相感兴趣。

编辑: 为了缩小问题的范围,这里有几个特别感兴趣的场景......

// 1. Is the value-copy constructor zero cost?
// Is...
var angleRadians = new AngleRadians(myDouble);
// The same as...
var myDouble2 = myDouble;

// 2. Is field access zero cost?
// Is...
var myDouble2 = angleRadians.Value;
// The same as...
var myDouble2 = myDouble;

// 3. Is function passing zero cost?
// Is calling...
static void DoNaught(AngleRadians angle){}
// The same as...
static void DoNaught(double angle){}
// (disregarding inlining reducing this to a noop

这些是我能想到的一些问题。当然,像@EricLippert这样出色的语言设计师可能会想到更多场景。因此,即使这些典型的用例是零成本,我仍然认为最好知道是否存在JIT没有处理持有一个值的结构,并且将未包装的值视为等价的情况。列出每个可能的代码段作为其自己的问题

2 个答案:

答案 0 :(得分:1)

由于ABI要求,可能存在一些轻微且可观察到的差异。例如,对于Windows x64,结构包装的float或double将通过整数寄存器传递给被调用者,而浮点数和双精度数通过XMM寄存器传递(类似于返回)。最多4个整数和4个浮点数可以通过寄存器传递。

这实际影响与背景有关。

如果扩展你的例子以传递至少5个整数和struct-or-double args的混合,你将在struct wrapped double case中更快地用完整数arg寄存器,并调用和访问尾随(非-register传递)被调用者中的args会稍微慢一些。但是效果可能很微妙,因为第一个被调用者访问通常会将结果缓存回寄存器中。

同样,如果你传递至少5个双打和结构包装双打的混合物,你可以在调用时在寄存器中放入更多东西,而不是将所有args作为双精度或所有args传递为struct wrapped double。因此,有一些结构包装双打和一些非结构包装双打可能有一些小优势。

因此,在隔离的情况下,如果更多的args适合寄存器,那么纯粹的调用开销和对args的原始访问会更低,这意味着如果存在许多其他的双精度数,则包装一些双精度的结构会有所帮助,而如果存在多个结构包装则不会有帮助许多其他整数。

但是如果调用者和被调用者都计算值并且还接收或传递它们,则会出现复杂情况 - 通常在这种情况下,结构包装最终会慢一些,因为必须将值从int寄存器移到堆栈或(可能)浮点寄存器。

这是否取消了呼叫的小额潜在收益取决于计算与呼叫的相对平衡以及传递了多少args以及args的类型,注册压力等。

具有HFA结构传递规则的ABI往往更好地与这种事物隔离,因为它们可以在浮点寄存器中传递struct wrapped浮点数。

答案 1 :(得分:0)

我发现,在启用优化的情况下,在调试模式下运行数十亿个DoNaught试验没有显着的性能差异。有时,双赢,有时,包装赢了。