【问题标题】:Performance penalty: denormalized numbers versus branch mis-predictions性能损失:非规范化数字与分支错误预测
【发布时间】:2020-07-13 03:27:37
【问题描述】:

对于那些已经测量过或对此类注意事项有深入了解的人,假设您必须执行以下操作(只是为示例选择任何一个)浮点运算符:

float calc(float y, float z)
{ return sqrt(y * y + z * z) / 100; }

yz 可能是非正规数,让我们假设两种可能的情况,其中只有 y、只是 z 或两者,以完全随机的方式,可以是非正规数

  • 50% 的时间

现在假设我想避免处理非正规数的性能损失,我只想将它们视为 0,然后我通过以下方式更改那段代码:

float calc(float y, float z)
{
   bool yzero = y < 1e-37;
   bool zzero = z < 1e-37;
   bool all_zero = yzero and zzero;
   bool some_zero = yzero != zzero;

   if (all_zero)
      return 0f;

   float ret;

   if (!some_zero) ret = sqrt(y * y + z * z);
   else if (yzero) ret = z;
   else if (zzero) ret = y;

   return ret / 100;
}

更糟糕的是,分支错误预测的性能损失(对于 50% 或

为了正确解释前一段代码中哪些操作可以是正常的或非正常的,我还想就以下密切相关的问题获得一些单行但完全可选的答案:

float x = 0f; // Will x be just 0 or maybe some number like 1e-40;
float y = 0.; // I assume the conversion is just thin-air here and the compiler will see just a 0.
0; // Is "exact zero" a normal or a denormal number?
float z = x / 1; // Will this "no-op" (x == 0) cause z be something like 1e-40 and thus denormal?
float zz = x / c; // What about a "no-op" operating against any compiler-time constant?
bool yzero = y < 1e-37; // Have comparisions any performance penalty when y is denormal or they don't?

【问题讨论】:

  • 唯一真正的答案是测量。
  • 在什么 CPU 上? IIRC,AMD CPU 对低于正常值的输入/结果没有惩罚,而现代英特尔 CPU(Sandybridge 系列)无需微码辅助即可处理低于正常值操作数的部分但不是所有 FP 操作(超过 100 个周期,而大约 10 到 20 个周期分支小姐)。有关一些信息,请参阅Agner Fog's microarch PDF;他笼统地提到了这一点,但没有完全详细地细分。不幸的是,我不认为 uops.info 测试正常与不正常。
  • 您的示例函数将导致不准确的结果,早于yz 低于正常值(只要任一变量平方为零)。除此之外,您的问题需要更多上下文(例如,什么平台,您关心吞吐量还是延迟?)
  • 我不知道任何非 x86 微体系结构的详细信息,例如 ARM cortex-a76 或任何 RISC-V,因此只能选择一些可能相关的随机示例。在简单的有序管道与现代 x86 等深度 OoO exec CPU 之间,错误预测的惩罚也有很大差异。真正的误判惩罚也取决于周围的代码。
  • 您可以使用ret = std::hypot(y, z); 而不是ret = sqrt(y * y + z * z);,这样可以避免下溢和上溢

标签: c++ x86 floating-point micro-optimization branch-prediction


【解决方案1】:

在包括 x86 在内的许多 ISA 中都有免费的硬件支持,请参见下面的回复:FTZ / DAZ。当您使用 -ffast-math 或等效项进行编译时,大多数编译器会在启动期间设置这些标志。

另请注意,在某些情况下,您的代码无法避免惩罚(在有任何惩罚的硬件上):y * yz * z 对于小但规范化的 yz 可能低于正常值/强>。 (Good catch, @chtz)。 y*y 的指数是y 指数的两倍,更负或更正。对于23 explicit mantissa bits in a float,这大约是 12 个指数值,它们是次正规值的平方根,不会一直下​​溢到 0

平方次正规总是给0 下溢;我不知道,对于乘法,次正规输入可能比次正规输出受到惩罚的可能性更小。 是否有低于正常值的惩罚会因一个微架构中的操作而异,例如加/减、乘和除。

此外,任何负数 yz 都会被视为 0,这可能是一个错误,除非您的输入已知为非负数。

如果结果差异如此之大,x86 微架构将是我的主要用例

是的,处罚(或没有处罚)差别很大。

从历史上看(P6 系列),英特尔过去总是采用非常慢的微码辅助来处理不正常的结果和不正常的输入,包括比较。现代英特尔 CPU(Sandybridge 系列)无需微码辅助即可处理次正规操作数上的部分但不是全部 FP 操作。 (性能事件fp_assists.any

微码辅助就像一个异常,会刷新乱序的管道,在 SnB 系列上需要超过 160 个周期,而分支未命中则需要大约 10 到 20 个周期。 以及现代 CPU 上的 branch misses have "fast recovery"。真正的分支未命中惩罚取决于周围的代码;例如如果分支条件真的很晚才准备好,它可能会导致丢弃大量后来的独立工作。但是,如果您期望它经常发生,那么微码辅助可能仍然会更糟。

请注意,您可以使用整数运算检查次正规:只需检查指数字段是否为全零(尾数为非零:0.0 的全零编码在技术上是次正规的特殊情况) . 因此您可以使用andps/pcmpeqd/andps 等整数 SIMD 操作手动刷新为零@

Agner Fog's microarch PDF 有一些信息;他笼统地提到了这一点,但没有对每个 uarch 进行完全详细的细分。不幸的是,我不认为 https://uops.info/ 测试正常与不正常。

Knight's Landing (KNL) 对除法只有低于正常值的惩罚,而不是 add / mul。与 GPU 一样,他们采用了一种优先考虑吞吐量而不是延迟的方法,并且在其 FPU 中有足够的流水线阶段来处理硬件中的次规范,相当于无分支。尽管这可能意味着每个 FP 操作的延迟更高。

除非设置了 FTZ,否则 AMD Bulldozer / Piledriver 对“低于正常或下溢”的结果有约 175 个周期的惩罚。 Agner 没有提到次正常的输入。压路机/挖掘机没有任何处罚。

AMD Ryzen(来自 Agner Fog 的 microarch pdf)

产生次等结果的浮点运算需要额外的几个时钟周期。这 当乘法或除法下溢为零时也是如此。这远远小于 推土机和打桩机的高额罚款。清零时没有惩罚 mode 和 denormals-are-zero 模式都打开了。

相比之下,英特尔 Sandybridge 系列(至少 Skylake)不会对一直下溢至 0.0 的结果进行处罚。

Intel Silvermont (Atom)来自 Agner Fog 的 microarch pdf

具有次正规数作为输入或输出或产生下溢的操作采取 大约 160 个时钟周期,除非清零模式和非规范化为零 模式都使用了。

这将包括比较。


我不知道任何非 x86 微架构的详细信息,例如 ARM cortex-a76 或任何 RISC-V,因此我无法挑选一些可能​​也相关的随机示例。在简单的有序管道与现代 x86 等深度 OoO exec CPU 之间,错误预测的惩罚也有很大差异。真正的误判惩罚也取决于周围的代码。


现在假设我想避免处理非正规数的性能损失,我只想将它们视为 0

那么你应该设置你的 FPU 来免费为你做这件事,消除所有不正常的惩罚的可能性。

一些/大多数(?)现代 FPU(包括 x86 SSE 但不包括旧版 x87)允许您免费将次正规(又名非正规)视为零,因此只有当您希望 some 有这种行为时才会出现此问题em> 函数,但不是全部,在同一个线程中。并且切换太细粒度不值得将 FP 控制寄存器更改为 FTZ 并返回。

或者,如果你想编写完全可移植的代码,但它在任何地方都很糟糕,即使这意味着忽略硬件支持并因此比它可能的速度要慢。

Some x86 CPUs do even rename MXCSR 因此更改舍入模式或 FTZ/DAZ 可能不必耗尽无序后端。它仍然不便宜,您应该避免每隔几条 FP 指令就执行一次。

ARM 还支持类似的功能:subnormal IEEE 754 floating point numbers support on iOS ARM devices (iPhone 4) - 但显然 ARM VFP / NEON 的默认设置是将次正规视为零,有利于性能而不是严格的 IEEE 合规性。

另请参阅flush-to-zero behavior in floating-point arithmetic,了解此功能的跨平台可用性。


在 x86 上,具体机制是您在 MXCSR 寄存器中设置 DAZ 和 FTZ 位(SSE FP 数学控制寄存器;还有用于 FP 舍入模式、FP 异常掩码和粘性 FP 的位屏蔽异常状态位)。 https://software.intel.com/en-us/articles/x87-and-sse-floating-point-assists-in-ia-32-flush-to-zero-ftz-and-denormals-are-zero-daz 展示了布局,还讨论了对旧版 Intel CPU 的一些性能影响。很多好的背景/介绍。

使用-ffast-math 编译会在调用main 之前链接一些额外的启动代码来设置FTZ/DAZ。 IIRC,线程在大多数操作系统上从主线程继承MXCSR 设置。 p>

  • DAZ = 非正规为零,将输入次正规视为零。这会影响比较(无论他们是否会经历减速),因此除了在位模式上使用整数内容之外,甚至无法区分 0 和次规范之间的区别。
  • FTZ = 清零,计算的次正常输出只是下溢归零。即禁用逐渐下溢。 (请注意,将两个小的正态数相乘可能会下溢。我认为除低位之外的尾数抵消的正态数的加法/减法也可能产生次正态数。)

通常您只需设置两者或都不设置。如果您正在处理来自另一个线程或进程的输入数据,或者是编译时常量,即使您生成的所有结果都已归一化或为 0,您仍然可能有次等输入。


具体随机问题:

float x = 0f; // Will x be just 0 or maybe some number like 1e-40;

这是一个语法错误。大概你的意思是0.f0.0f

0.0f 完全可以表示为 IEEE binary32 浮点数(使用位模式0x00000000),因此这绝对是您在任何使用 IEEE FP 的平台上都会得到的。你不会随机得到不是你写的次正规的。

float z = x / 1; // Will this "no-op" (x == 0) cause z be something like 1e-40 and thus denormal?

不,IEEE754 不允许 0.0 / 1.0 提供除 0.0 以外的任何内容。

再一次,次常态不会凭空出现。 只有在不能将精确结果表示为浮点数或双精度数时才会发生舍入“错误”。 IEEE“基本”操作(* / + - 和 sqrt)的最大允许错误是0.5 ulp,即准确的结果必须正确舍入到最接近的可表示的 FP 值,一直到尾数的最后一位。

 bool yzero = y < 1e-37; // Have comparisons any performance penalty when y is denormal or they don't?

也许,也许不是。对最近的 AMD 或 Intel 没有惩罚,但在 Core 2 上速度较慢。

请注意,1e-37 的类型为 double,并将导致将 y 提升为 double。您可能希望与使用 1e-37f 相比,这实际上可以避免低于正常水平的处罚。低于标准的 float->int 对 Core 2 没有惩罚,但不幸的是,cvtss2sd 在 Core 2 上仍然有很大的惩罚。(GCC/clang don't optimize away 即使使用 -ffast-math 也可以转换,尽管我认为它们可以,因为 1e-37 正是可以表示为一个平面,并且每个次正规的浮点数都可以精确地表示为一个规范化的双精度。因此,双精度的提升始终是精确的,并且不能改变结果)。

在 Intel Skylake 上,将两个次正规数与 vcmplt_oqpd 进行比较不会导致任何减速,而将 ucomisd 与整数 FLAGS 进行比较也不会。但在 Core 2 上,两者都很慢。

比较,如果像减法一样完成,确实必须移动输入以对齐它们的二进制位值,并且尾数的隐含前导数字是0而不是1,因此次正规是一种特殊情况。因此,硬件可能会选择不在快速路径上处理该问题,而是采用微码辅助。较旧的 x86 硬件可能会处理得更慢。

如果您构建一个与普通添加/子单元分开的特殊比较 ALU,则可能会有所不同。浮点位模式可以作为符号/幅度整数进行比较(NaN 的特殊情况),因为选择了 IEEE 指数偏差来实现这一点。 (即nextafter 只是整数 ++ 或 -- 在位模式上)。但这显然不是硬件的作用。


即使在 Core 2 上,FP 转换为整数也很快。 cvt[t]ps2dq 或 pd 等价物将压缩浮点/双精度转换为 int32,并使用截断或当前舍入模式。所以例如this recent proposed LLVM optimization is safe on Skylake and Core 2,根据我的测试。

同样在 Skylake 上,平方次正规(产生 0)没有惩罚。但它确实对Conroe(P6-family)造成了巨大的惩罚。

但是,即使在 Skylake 上乘以正常数来产生次正常的结果也会有损失(慢约 150 倍)。

【讨论】:

  • 所以,从理论上讲,在阅读了那里的两个主要事实(100 个周期非正规与 20-30 个误预测平均值),再加上比较非正规是非正规操作这一事实后,第一个版本将是除非两个操作数都是非正规的,否则总是更快,在这种情况下,第一个版本将有 5 个非正规操作,而第二个版本只有 2 个。此外,最后三个分支 (if(!some_zero)...) 通常是有条件的移动,所以我在这里没有惩罚。我在吗?
  • 好吧,我忘记了sqrt,这也是这里的一个因素,只有当任何操作数不正常时,才能避开它们。
  • @Peregring-lk: if (!some_zero) ret = sqrt(y * y + z * z); 只有在你实际计算结果时才能是无分支的!这样做的全部目的是避免在有输入次正规的情况下进行那些 FP 操作。编译器可能会将您的布尔设置和 if() 操作转换为更简单的分支,例如总共最多 3 个,或者可能在 y 和 z 之间进行无分支选择(例如,旧版 x87+P6 fcmov)然后在它们上进行非零分支.请注意,真正的传统 x87 没有 FP 条件移动。无分支 SSE 数学可以通过比较掩码和 ANDPS/ORPS...
  • @Peregring-lk:另请参阅我的上次编辑:也许您错过了某些 CPU 对某些操作 (mul) 而其他操作 (添加或比较) 的惩罚低于正常值。如果对次正常输入的任何操作总是有惩罚,那么您的简单模型可能适用于早期的 P6 系列 CPU。你似乎把我的误判惩罚成本从 10-20 提高到了 20-30。如果分支条件早早地准备好,那么在没有前端瓶颈的代码中可以有效地降低成本……在 OoO exec CPU 上进行模式并不简单。性能不是一维的,因此您不能只添加成本来获得总和。
  • 次要添加/cmets:float x = 0f;是非法的,需要写0.f或者0e0f什么的。并且:bool yzero = y &lt; 1e-37; 这可能会在比较之前将y 转换为两倍,这可能最好是y &lt; 1e-37f
猜你喜欢
  • 2021-09-22
  • 2016-05-27
  • 2016-05-13
  • 2018-06-06
  • 2013-03-29
  • 2014-11-01
  • 2016-09-14
  • 2013-12-11
  • 2012-10-08
相关资源
最近更新 更多