为何将 0.1f 改为 0 会使性能降低 10 倍?

为何将 0.1f 改为 0 会使性能降低 10 倍?

技术背景

在进行浮点运算时,有时会遇到性能急剧下降的情况。比如在某些代码中,将常量 0.1f 改为 0 后,性能可能会降低 10 倍。这主要与非规范化浮点数(denormalized floating-point)有关。非规范化数是一种特殊的浮点数表示,用于在浮点数表示中获取非常接近零的额外值。然而,对非规范化浮点数的操作比规范化浮点数慢得多,因为许多处理器不能直接处理它们,必须使用微代码进行捕获和解析。

实现步骤

测试代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <omp.h>
#include <cstdlib>

int main() {

double start = omp_get_wtime();

const float x[16]={1.1,1.2,1.3,1.4,1.5,1.6,1.7,1.8,1.9,2.0,2.1,2.2,2.3,2.4,2.5,2.6};
const float z[16]={1.123,1.234,1.345,156.467,1.578,1.689,1.790,1.812,1.923,2.034,2.145,2.256,2.367,2.478,2.589,2.690};
float y[16];
for(int i=0;i<16;i++)
{
y[i]=x[i];
}
for(int j=0;j<9000000;j++)
{
for(int i=0;i<16;i++)
{
y[i]*=x[i];
y[i]/=z[i];
#ifdef FLOATING
y[i]=y[i]+0.1f;
y[i]=y[i]-0.1f;
#else
y[i]=y[i]+0;
y[i]=y[i]-0;
#endif

if (j > 10000)
std::cout << y[i] << " ";
}
if (j > 10000)
std::cout << std::endl;
}

double end = omp_get_wtime();
std::cout << end - start << std::endl;

std::system("pause");
return 0;
}

代码解释

  • 代码中定义了两个常量数组 xz,以及一个数组 y
  • 通过嵌套循环对 y 数组进行多次乘法、除法和加法、减法操作。
  • 根据是否定义 FLOATING 宏,分别进行 y[i]=y[i]+0.1f; y[i]=y[i]-0.1f;y[i]=y[i]+0; y[i]=y[i]-0; 操作。
  • 记录程序运行时间并输出结果。

核心代码

测试代码

上述的测试代码展示了不同操作下的性能差异。

消除非规范化数的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 方法 1: 可能在某些 GCC 环境中不起作用
#include <fenv.h>
fesetenv(FE_DFL_DISABLE_SSE_DENORMS_ENV);

// 方法 2: 可能在某些 Visual Studio 环境中不起作用
#include <xmmintrin.h>
_mm_setcsr( _mm_getcsr() | (1<<15) | (1<<6) );

// 方法 3: 在 GCC 和 Visual Studio 中似乎都有效
#include <xmmintrin.h>
#include <pmmintrin.h>
_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);
_MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON);

// 在 gcc 中启用 FTZ 和 DAZ
#include <xmmintrin.h>

#define FTZ 1
#define DAZ 1

void enableFtzDaz()
{
int mxcsr = _mm_getcsr ();

if (FTZ) {
mxcsr |= (1<<15) | (1<<11);
}

if (DAZ) {
mxcsr |= (1<<6);
}

_mm_setcsr (mxcsr);
}

最佳实践

  • 尽量避免产生非规范化浮点数。可以通过在代码中添加 _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);_MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON); 来将非规范化数直接舍入为零,从而提高性能。
  • 在编写代码时,要注意浮点运算的精度问题,避免不必要的小数值计算导致非规范化数的产生。

常见问题

为什么 y[i]=y[i]+0.1f; y[i]=y[i]-0.1f; 不是无操作?

因为浮点数的精度有限,当 y[i] 是一个非常小的数时,加上 0.1f 后再减去 0.1f 可能会导致精度丢失,从而避免了非规范化数的产生,使后续的浮点运算保持快速。

不同编译器和环境对非规范化数的处理有什么不同?

不同的编译器和环境对非规范化数的处理方式可能不同。例如,某些 GCC 环境和 Visual Studio 环境对消除非规范化数的方法支持程度不同。因此,在实际应用中,需要根据具体的编译器和环境选择合适的方法。

非规范化数对性能的影响在不同 CPU 上是否相同?

不同的 CPU 对非规范化数的处理能力不同,因此非规范化数对性能的影响也会有所差异。例如,在某些 CPU 上,非规范化数的运算可能只比规范化数慢一点,而在其他 CPU 上可能会慢很多。


为何将 0.1f 改为 0 会使性能降低 10 倍?
https://119291.xyz/posts/why-changing-01f-to-0-slows-down-performance/
作者
ww
发布于
2025年5月28日
许可协议