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