性能损失:非规范化数字与分支错误预测
Performance penalty: denormalized numbers versus branch mis-predictions
对于那些已经测量过或对此类注意事项有深入了解的人,假设您必须执行以下操作(只是为示例选择一个)浮点运算符:
float calc(float y, float z)
{ return sqrt(y * y + z * z) / 100; }
其中 y
和 z
可能是非正规数,让我们假设两种可能的情况,其中只有 y、只有 z 或两者都可以以完全随机的方式成为非正规数
- 50% 的时间
- <1% 的时间
现在假设我想避免处理非正规数的性能损失,我只想将它们视为 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% 或 <1% 的情况),还是使用非正规数的性能损失?
为了正确解释在上一段代码中哪些操作可以是正常的或非正常的,我还想获得一些关于以下密切相关问题的单行但完全可选的答案:
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?
在包括 x86 在内的许多 ISA 中都有对此的免费硬件支持,请参阅下面的回复:FTZ / DAZ。当您使用 -ffast-math
或等效项进行编译时,大多数编译器会在启动期间设置这些标志。
另请注意,在某些情况下,您的代码无法避免惩罚(在硬件上有任何惩罚):y * y
或 z * z
对于小但标准化的情况可能是次正规的y
或 z
。 (). The exponent of y*y
is twice the exponent of y
, more negative or more positive. With 23 explicit mantissa bits in a float
,这是大约 12 个指数值,它们是次正规值的平方根,并且不会一直下溢到 0
。
次正规的平方总是下溢 0
;我不知道,次正规输入可能比次正规输出更不可能受到惩罚。 具有或不具有次正规惩罚可能因一个微体系结构中的操作而异,例如 add/sub 与乘法与除法。
此外,任何负数 y
或 z
都会被视为 0
,这可能是一个错误,除非您的输入已知为非负数。
if results can vary so widely, x86 microarchitectures will be my main use case
是的,处罚(或不处罚)差别很大。
历史上(P6 系列)Intel 过去总是采用非常慢的微码辅助来处理次正常的结果和次正常的输入,包括用于比较。现代 Intel CPU(Sandybridge 系列)处理一些但不是所有的 FP 操作,而不需要微码辅助。 (性能事件 fp_assists.any
)
微代码辅助就像一个异常并刷新无序管道,并在 SnB 系列上占用超过 160 个周期,而分支未命中约为 10 到 20 个周期。
在现代 CPU 上 。真正的分支未命中惩罚取决于周围的代码;例如如果分支条件真的很晚才准备好,可能会导致丢弃很多后来的独立工作。但是,如果您希望它经常发生,微码辅助可能仍然更糟。
请注意,您可以使用整数操作来检查次正规:只需检查全零的指数字段(以及非零的尾数:0.0
的全零编码在技术上是一种特殊情况低于常态)。 因此您可以使用 andps
/pcmpeqd
/andps
等整数 SIMD 操作手动清零
不幸的是,Agner Fog's microarch PDF has some info; he mentions this in general without a fully detailed breakdown for each uarch. I don't think https://uops.info/ 测试正常与次正常。
Knight's Landing (KNL) 只有分裂的次级惩罚,没有 add / mul。与 GPU 一样,他们采用了一种有利于吞吐量而不是延迟的方法,并且在其 FPU 中有足够的流水线阶段来处理相当于无分支的硬件中的次正规。尽管这可能意味着每个 FP 操作的延迟更高。
AMD Bulldozer / Piledriver 对 "subnormal or underflow" 的结果有约 175 个周期的惩罚,除非设置了 FTZ。 Agner 没有提到低于正常的输入。 Steamroller/Excavator 没有任何处罚。
AMD Ryzen (from Agner Fog's microarch pdf)
Floating point operations that give a subnormal result take a few clock cycles extra. The
same is the case when a multiplication or division underflows to zero. This is far less than
the high penalty on the Bulldozer and Piledriver. There is no penalty when flush-to-zero
mode and denormals-are-zero mode are both on.
相比之下,Intel Sandybridge 系列(至少是 Skylake)对下溢到 0.0 的结果没有惩罚。
Intel Silvermont (Atom) from Agner Fog's microarch pdf
Operations that have subnormal numbers as input or output or generate underflow take
approximately 160 clock cycles unless the flush-to-zero mode and denormals-are-zero
mode are both used.
这将包括比较。
我不知道任何非 x86 微体系结构的详细信息,例如 ARM cortex-a76 或任何 RISC-V,无法随机选择几个可能也相关的示例。预测错误的惩罚也千差万别,跨简单的有序管道与深度 OoO 执行 CPU,如现代 x86。真正的错误预测惩罚也取决于周围的代码。
And now assume I want to avoid the performance penalty of dealing with denormal numbers and I just want to treat them as 0
那么你应该将你的 FPU 设置为免费为你做这件事,消除所有因次正常而受到惩罚的可能性。
一些/大多数(?)现代 FPU(包括 x86 SSE,但不包括旧版 x87)让您免费将次正规(也称为非正规)视为零,因此只有当您想要此行为时才会出现此问题 some 在同一个线程中起作用,但不是全部。并且由于过于细粒度的切换不值得将 FP 控制寄存器更改为 FTZ 并返回。
或者,如果您想编写完全可移植的代码,但这些代码在任何地方都非常糟糕,即使这意味着忽略硬件支持并因此比它可能的速度慢,也可能是相关的。
因此更改舍入模式或 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
将 link 在调用 main
. IIRC 之前设置 FTZ/DAZ 的一些额外的启动代码,线程从大多数操作系统的主线程继承 MXCSR 设置。
- DAZ = 非正规化为零,将输入次正规化视为零。这会影响比较(无论他们是否会经历减速),除了在位模式上使用整数内容之外,甚至无法分辨
0
和次正规之间的区别。
- FTZ = 清零,计算的次正常输出只是下溢到零。即禁用渐进下溢。 (请注意,将两个小的正规数相乘可能会下溢。我认为 add/sub 的正规数的尾数抵消,除了低几位也可能产生次正规数。)
通常您只需设置两者或两者都不设置。如果您正在处理来自另一个线程或进程的输入数据,或编译时常量,即使您生成的所有结果都已归一化或为 0,您仍然可能有次正规输入。
具体随机问题:
float x = 0f; // Will x be just 0 or maybe some number like 1e-40;
这是语法错误。大概你的意思是 0.f
或 0.0f
0.0f 可以精确表示(使用位模式 0x00000000
)作为 IEEE 二进制 32 浮点数,因此这绝对是您在使用 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
以外的任何内容。
同样,次法线不会凭空出现。 舍入 "error" 仅当精确结果不能表示为浮点数或双精度数时才会发生。 IEEE "basic" 运算的最大允许误差 (* / + - 并且 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
可以精确表示为 flat,并且每个次正规浮点数都可以精确表示为正规化双精度。所以提升为双精度始终是精确的并且不能改变结果。
在 Intel Skylake 上,使用 vcmplt_oqpd
比较两个次正规不会导致任何减速,使用 ucomisd
也不会导致整数 FLAGS。但是在 Core 2 上,两者都很慢。
比较,如果像减法一样完成,确实必须移动输入以排列它们的二进制位值,并且尾数的隐含前导数字是 0
而不是 1
所以次正规是一种特殊情况。因此,硬件可能会选择不在快速路径上处理该问题,而是采用微码辅助。较旧的 x86 硬件处理速度可能较慢。
如果您构建一个与普通 add/sub 单元分开的特殊比较 ALU,则可以采用不同的方法。浮点位模式可以与 sign/magnitude 整数(NaN 的特殊情况)进行比较,因为选择了 IEEE 指数偏差来使其工作。 (即 nextafter
只是整数 ++ 或 -- 在位模式上)。但这显然不是硬件的作用。
FP 到整数的转换即使在 Core 2 上也很快。 cvt[t]ps2dq
或等效的 pd 将 packed float/double 转换为 int32 或当前舍入模式。例如this recent proposed LLVM optimization is safe on Skylake and Core 2,根据我的测试
同样在 Skylake 上,次正规的平方(产生 0
)没有惩罚。但它确实对 Conroe(P6 家族)造成了巨大的惩罚。
但是,即使在 Skylake 上,将正态数相乘以产生次正态结果也会受到惩罚(慢 ~150 倍)。
对于那些已经测量过或对此类注意事项有深入了解的人,假设您必须执行以下操作(只是为示例选择一个)浮点运算符:
float calc(float y, float z)
{ return sqrt(y * y + z * z) / 100; }
其中 y
和 z
可能是非正规数,让我们假设两种可能的情况,其中只有 y、只有 z 或两者都可以以完全随机的方式成为非正规数
- 50% 的时间
- <1% 的时间
现在假设我想避免处理非正规数的性能损失,我只想将它们视为 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% 或 <1% 的情况),还是使用非正规数的性能损失?
为了正确解释在上一段代码中哪些操作可以是正常的或非正常的,我还想获得一些关于以下密切相关问题的单行但完全可选的答案:
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?
在包括 x86 在内的许多 ISA 中都有对此的免费硬件支持,请参阅下面的回复:FTZ / DAZ。当您使用 -ffast-math
或等效项进行编译时,大多数编译器会在启动期间设置这些标志。
另请注意,在某些情况下,您的代码无法避免惩罚(在硬件上有任何惩罚):y * y
或 z * z
对于小但标准化的情况可能是次正规的y
或 z
。 (y*y
is twice the exponent of y
, more negative or more positive. With 23 explicit mantissa bits in a float
,这是大约 12 个指数值,它们是次正规值的平方根,并且不会一直下溢到 0
。
次正规的平方总是下溢 0
;我不知道,次正规输入可能比次正规输出更不可能受到惩罚。 具有或不具有次正规惩罚可能因一个微体系结构中的操作而异,例如 add/sub 与乘法与除法。
此外,任何负数 y
或 z
都会被视为 0
,这可能是一个错误,除非您的输入已知为非负数。
if results can vary so widely, x86 microarchitectures will be my main use case
是的,处罚(或不处罚)差别很大。
历史上(P6 系列)Intel 过去总是采用非常慢的微码辅助来处理次正常的结果和次正常的输入,包括用于比较。现代 Intel CPU(Sandybridge 系列)处理一些但不是所有的 FP 操作,而不需要微码辅助。 (性能事件 fp_assists.any
)
微代码辅助就像一个异常并刷新无序管道,并在 SnB 系列上占用超过 160 个周期,而分支未命中约为 10 到 20 个周期。
在现代 CPU 上
请注意,您可以使用整数操作来检查次正规:只需检查全零的指数字段(以及非零的尾数:0.0
的全零编码在技术上是一种特殊情况低于常态)。 因此您可以使用 andps
/pcmpeqd
/andps
Agner Fog's microarch PDF has some info; he mentions this in general without a fully detailed breakdown for each uarch. I don't think https://uops.info/ 测试正常与次正常。
Knight's Landing (KNL) 只有分裂的次级惩罚,没有 add / mul。与 GPU 一样,他们采用了一种有利于吞吐量而不是延迟的方法,并且在其 FPU 中有足够的流水线阶段来处理相当于无分支的硬件中的次正规。尽管这可能意味着每个 FP 操作的延迟更高。
AMD Bulldozer / Piledriver 对 "subnormal or underflow" 的结果有约 175 个周期的惩罚,除非设置了 FTZ。 Agner 没有提到低于正常的输入。 Steamroller/Excavator 没有任何处罚。
AMD Ryzen (from Agner Fog's microarch pdf)
Floating point operations that give a subnormal result take a few clock cycles extra. The same is the case when a multiplication or division underflows to zero. This is far less than the high penalty on the Bulldozer and Piledriver. There is no penalty when flush-to-zero mode and denormals-are-zero mode are both on.
相比之下,Intel Sandybridge 系列(至少是 Skylake)对下溢到 0.0 的结果没有惩罚。
Intel Silvermont (Atom) from Agner Fog's microarch pdf
Operations that have subnormal numbers as input or output or generate underflow take approximately 160 clock cycles unless the flush-to-zero mode and denormals-are-zero mode are both used.
这将包括比较。
我不知道任何非 x86 微体系结构的详细信息,例如 ARM cortex-a76 或任何 RISC-V,无法随机选择几个可能也相关的示例。预测错误的惩罚也千差万别,跨简单的有序管道与深度 OoO 执行 CPU,如现代 x86。真正的错误预测惩罚也取决于周围的代码。
And now assume I want to avoid the performance penalty of dealing with denormal numbers and I just want to treat them as 0
那么你应该将你的 FPU 设置为免费为你做这件事,消除所有因次正常而受到惩罚的可能性。
一些/大多数(?)现代 FPU(包括 x86 SSE,但不包括旧版 x87)让您免费将次正规(也称为非正规)视为零,因此只有当您想要此行为时才会出现此问题 some 在同一个线程中起作用,但不是全部。并且由于过于细粒度的切换不值得将 FP 控制寄存器更改为 FTZ 并返回。
或者,如果您想编写完全可移植的代码,但这些代码在任何地方都非常糟糕,即使这意味着忽略硬件支持并因此比它可能的速度慢,也可能是相关的。
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
将 link 在调用 main
. IIRC 之前设置 FTZ/DAZ 的一些额外的启动代码,线程从大多数操作系统的主线程继承 MXCSR 设置。
- DAZ = 非正规化为零,将输入次正规化视为零。这会影响比较(无论他们是否会经历减速),除了在位模式上使用整数内容之外,甚至无法分辨
0
和次正规之间的区别。 - FTZ = 清零,计算的次正常输出只是下溢到零。即禁用渐进下溢。 (请注意,将两个小的正规数相乘可能会下溢。我认为 add/sub 的正规数的尾数抵消,除了低几位也可能产生次正规数。)
通常您只需设置两者或两者都不设置。如果您正在处理来自另一个线程或进程的输入数据,或编译时常量,即使您生成的所有结果都已归一化或为 0,您仍然可能有次正规输入。
具体随机问题:
float x = 0f; // Will x be just 0 or maybe some number like 1e-40;
这是语法错误。大概你的意思是 0.f
或 0.0f
0.0f 可以精确表示(使用位模式 0x00000000
)作为 IEEE 二进制 32 浮点数,因此这绝对是您在使用 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
以外的任何内容。
同样,次法线不会凭空出现。 舍入 "error" 仅当精确结果不能表示为浮点数或双精度数时才会发生。 IEEE "basic" 运算的最大允许误差 (* / + - 并且 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
可以精确表示为 flat,并且每个次正规浮点数都可以精确表示为正规化双精度。所以提升为双精度始终是精确的并且不能改变结果。
在 Intel Skylake 上,使用 vcmplt_oqpd
比较两个次正规不会导致任何减速,使用 ucomisd
也不会导致整数 FLAGS。但是在 Core 2 上,两者都很慢。
比较,如果像减法一样完成,确实必须移动输入以排列它们的二进制位值,并且尾数的隐含前导数字是 0
而不是 1
所以次正规是一种特殊情况。因此,硬件可能会选择不在快速路径上处理该问题,而是采用微码辅助。较旧的 x86 硬件处理速度可能较慢。
如果您构建一个与普通 add/sub 单元分开的特殊比较 ALU,则可以采用不同的方法。浮点位模式可以与 sign/magnitude 整数(NaN 的特殊情况)进行比较,因为选择了 IEEE 指数偏差来使其工作。 (即 nextafter
只是整数 ++ 或 -- 在位模式上)。但这显然不是硬件的作用。
FP 到整数的转换即使在 Core 2 上也很快。 cvt[t]ps2dq
或等效的 pd 将 packed float/double 转换为 int32 或当前舍入模式。例如this recent proposed LLVM optimization is safe on Skylake and Core 2,根据我的测试
同样在 Skylake 上,次正规的平方(产生 0
)没有惩罚。但它确实对 Conroe(P6 家族)造成了巨大的惩罚。
但是,即使在 Skylake 上,将正态数相乘以产生次正态结果也会受到惩罚(慢 ~150 倍)。