【问题标题】:Is a struct wrapping a primitive value type a zero cost abstraction in C#?包装原始值类型的结构是 C# 中的零成本抽象吗?
【发布时间】:2017-07-12 16:47:51
【问题描述】:

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

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

【问题讨论】:

  • 你为什么不尝试做一些测试?
  • 如果你想知道两匹马哪一匹更快,你是在网上问陌生人,还是赛马?您想知道提议的计划的成本,然后运行该计划,很快您就会知道它的成本!
  • 抛开性能问题不谈,我建议使用AngleDegreesAngleRadiansAngleGradians 等类型是个坏主意。有一个Angle 类型,它具有AsDegreesAsRadians 等属性和工厂FromRadians 等属性。您的类型的实现细节就是:实现细节。通过将实现细节嵌入到类型的名称中,您限制了您的创新能力并降低了抽象级别。
  • 我觉得这个问题太笼统了。您基本上是在说“想想我可以用这种类型做的所有可能的事情,并向我展示 JIT 编译器以我想要的方式对其进行优化”。对于一个问题,这要求太多了。
  • @EricLippert 实际上,一个具有 AsDegrees/AsRadians 属性和工厂方法 FromRadians 的 Angle 结构正是我现在拥有的代码。为简单起见,我选择了 Angle,但有时在空间之间进行转换并不便宜,因此要求 API 用户明确而不是在幕后进行工作很有用。可怕的例子 - Vector3Cartesian 和 Vector3Polar

标签: c# performance cil ryujit


【解决方案1】:

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

实际影响取决于上下文。

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

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

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

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

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

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

【讨论】:

    【解决方案2】:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-06-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-07-26
      • 1970-01-01
      • 2022-10-14
      • 1970-01-01
      相关资源
      最近更新 更多