有时我想在原始双打周围添加更多的类型安全。经常出现的一个想法是使用类型添加单位信息。例如
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 是否不将保存一个值的结构处理,而将解包的值视为等效,而不列出每个可能的代码片段作为它自己的问题
由于 ABI 要求,可能会有一些细微且可观察到的差异。例如,对于 Windows x64,结构包装的浮点数或双精度数将通过整数寄存器传递给被调用方,而浮点数和双精度数通过 XMM 寄存器传递(类似地用于返回(。最多可以通过寄存器传递 4 个整数和 4 个浮点数。
其实际影响非常依赖于上下文。
如果扩展示例以传递至少 5 个整数和结构或双精度参数的混合,则在结构包装的双大小写中,整数 arg 寄存器将更快用完,并且调用和访问被调用方中的尾随(非寄存器传递(参数将稍慢。但效果可能很微妙,因为第一个被叫方访问通常会将结果缓存回寄存器中。
同样,如果您传递至少 5 个双精度和结构包装的双精度的混合,则与将所有 args 作为双精度或将所有 args 作为结构包装的双精度传递相比,您可以在一次调用中放入更多的东西。因此,拥有一些结构包装的双精度和一些非结构包装的双精度可能会有一些小的优势。
因此,在隔离的情况下,如果寄存器中容纳更多参数,则纯调用开销和对 args 的原始访问会更低,这意味着如果存在许多其他双精度,则结构包装一些双精度会有所帮助,如果存在许多其他整数,则结构包装会有所帮助。
但是,如果调用方和被调用方都使用值进行计算并接收或传递它们,则会出现复杂情况 - 通常在这些情况下,结构包装最终会慢一些,因为值必须从整数寄存器移动到堆栈或(可能(浮点寄存器。
这是否会抵消调用中的小潜在增益取决于计算与调用的相对平衡,以及传递了多少参数以及参数的类型、寄存器压力等。
具有 HFA 结构传递规则的 ABI 往往更好地与此类事情隔离,因为它们可以在浮点寄存器中传递结构包装的浮点数。
我发现在启用优化的调试模式下运行十亿次 DoNaught 试验没有显着的性能差异。 有时,双赢,有时,包装纸赢了。