5

有时我想为原始双打添加更多类型安全。经常出现的一个想法是在类型中添加单元信息。例如,

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

在像上面这样只有一个字段的情况下,JIT 是否能够在所有情况下优化掉这个抽象?与使用未包装双精度的类似代码相比,哪些情况(如果有)会导致生成额外的机器指令?

任何提及过早优化的内容都将被否决。我有兴趣了解基本事实。

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

// 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 是否不将一个结构保持一个值和未包装的值视为等效的任何情况,而不列出每个可能的代码片段作为它自己的问题

4

2 回答 2

5

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

其实际影响非常依赖于上下文。

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

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

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

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

这是否抵消了调用中的微小潜在收益取决于计算与调用的相对平衡以及传递了多少参数以及参数是什么类型、寄存器压力等。

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

于 2018-05-12T21:23:15.233 回答
1

我发现在打开优化的调试模式下运行十亿次 DoNaught 试验没有显着的性能差异。有时,双赢,有时,包装赢。

于 2017-07-13T17:05:46.527 回答