缓慢的浮点运算

一些进行大量浮点值计算的应用程序容易出现一个非常微妙的问题,从而导致性能下降。这个问题出现在应用程序遇到“次规格”浮点值时,我们将在本节讨论。您还可以找到术语“去规格”浮点值,它是指同一个东西。根据 IEEE 标准 754,2 次规格值是一个非零数字,其指数小于最小规格数。1 @lst:Subnormals 展示了一个非常简单的次规格值的实例。

在实际应用中,次规格值通常表示一个非常小的信号,以至于它与零无法区分。在音频中,它可能意味着一个非常安静的信号,超出了人类的听觉范围。在图像处理中,它可以表示像素的任何 RGB 颜色分量非常接近于零,等等。有趣的是,次规格值存在于许多生产软件包中,包括天气预报、光线追踪、物理模拟和建模等等。

代码清单:实例化一个正常和次正常的FP值

unsigned usub = 0x80200000; // -2.93873587706e-39 (subnormal)
unsigned unorm = 0x411a428e; // 9.641248703 (normal)
float sub = *((float*)&usub);
float norm = *((float*)&unorm);
assert(std::fpclassify(sub) == FP_SUBNORMAL);
assert(std::fpclassify(norm) != FP_SUBNORMAL);

如果没有次规格值,两个浮点值 a - b 的减法可能会溢出并产生零,即使这两个值不相等。次规格值允许计算逐渐失去精度,而不会将结果舍入为零。不过,正如我们稍后将看到的,这也是有代价的。次规格值也可能出现在生产软件中,当一个值在一个循环中不断减小或除法时出现。

从硬件的角度来看,处理次规格值比处理普通浮点值更困难,因为它需要特殊处理,通常被认为是一种特殊情况。应用程序不会崩溃,但性能会下降。生成或消耗次规格值的计算比对普通数字执行类似计算要慢得多,速度可以慢 10 倍甚至更多。例如,英特尔处理器目前使用微码 协助 处理次规格值操作。当处理器识别到次规格浮点值时,微码执行器 (MSROM) 将提供必要的微操作 (μ\muops) 来计算结果。

在许多情况下,次规格值是由算法自然生成的,因此是不可避免的。幸运的是,大多数处理器都提供了将次规格值刷新为零并一开始不生成次规格值的选项。事实上,许多用户宁愿结果稍微不那么准确,也不愿让代码变慢。不过,对于金融软件来说,相反的论点也可以成立:如果你将次规格值刷新为零,你就失去了精度,并且无法将其向上扩展,因为它仍然是零。这可能会让一些客户生气。

假设你可以接受没有次规格值,那么如何检测和禁用它们?虽然可以使用 @lst:Subnormals 所示的运行时检查,但在整个代码库中插入它们并不实际。使用 PMU(性能监控单元)检测应用程序是否生成次规格值是一种更好的方法。在英特尔 CPU 上,你可以收集 FP_ASSIST.ANY 性能事件,每次使用次规格值时都会增加该事件。TMA 方法将这种瓶颈归类为“Retiring”类别,是的,这是高“Retiring”值不好的情况之一。

一旦确认存在次规格值,你可以启用 FTZ 和 DAZ 模式:

  • DAZ (Denormals Are Zero)。任何低于正常值的输入在使用之前都被替换为零。
  • FTZ (Flush To Zero)。任何会变为低于正常值的输出都替换为零。

启用它们后,CPU 浮点运算中就不需要昂贵地处理次规格值了。在 x86 平台上,MXCSR(全局控制和状态寄存器)中有两个独立的位字段。在 ARM Aarch64 中,两种模式由 FPCR 控制寄存器的 FZAH 位控制。如果你用 -ffast-math 编译你的应用程序,就不用担心了,编译器会自动在程序开始时插入所需代码启用这两个标志。-ffast-math 编译器选项有点过载,所以 GCC 开发人员创建了一个单独的 -mdaz-ftz 选项,只控制次规格值的行为。如果你想从源代码控制它,@lst:EnableFTZDAZ 显示了你可以使用的示例。如果你选择这个选项,请避免频繁更改 MXCSR 寄存器,因为操作相对昂贵。读取 MXCSR 寄存器有相当长的延迟,写入寄存器是一个序列化指令。

代码清单:手动启用FTZ和DAZ模式

unsigned FTZ = 0x8000;
unsigned DAZ = 0x0040;
unsigned MXCSR = _mm_getcsr();
_mm_setcsr(MXCSR | FTZ | DAZ);

请注意,FTZDAZ 模式都与 IEEE 754 标准不兼容。它们是在硬件中实现的,以提高在溢出常见且生成非规格化结果不必要时的应用程序性能。通常,我们观察到一些使用次规格值的生产浮点应用程序速度提高了 3% - 5%,有时甚至高达 50%。

1. 次规格数 - https://en.wikipedia.org/wiki/Subnormal_number
2. IEEE 754 标准 - https://ieeexplore.ieee.org/document/8766229

results matching ""

    No results matching ""