当函数放在条件语句中时性能更好?
Better performance when function placed within conditional statement?
在对 round() 实现中的 copysign() 函数的一个实现进行基准测试时遇到了这个“异常”:
float copysign(float x, float y){
float absx = std::fabs(x);
/* use atan2 to distinguish -0. from 0. */
if (y > 0.f || (y == 0.f && std::atan2(y, -1.f) > 0.f)) {
return absx;
} else {
return absx * -1.0f;
}
}
通过 quick-benc(和另一个衡量 rdtsc/val 的基准实用程序),当 atan2() 从条件语句中移出时,它会导致性能大大降低:
float copysign(float x, float y){
float absx = std::fabs(x);
float atan2y = std::atan2(y, -1.f); /* use atan2 to distinguish -0. from 0. */
if (y > 0.f || (y == 0.f && atan2y > 0.f)) {
return absx;
} else {
return absx *= -1.0f;
}
}
快速 C++ 基准测试:
海湾合作委员会 11.2:(-O3 -ffast-math / -O3)
Another Benchmark utility copysign() 和 round() 实施的结果 (rdtsc/val):
copysign():
~0.60 for "strd" (std::copysign(), same with copysign())
~3.12 for "cs1" (atan2() within condition statement)
~17.5 for "cs2" (atan2() as variable in condition statement)
并且在链接的 round() 实现中使用时:
~6.67 for case "strd"
~2.56 for case "cs1"
~20.5 for case "cs2"
问:在条件语句中使用 atan2() 会发生什么情况?它只是内联吗?
GCC 有关于 -ffast-math 用法的警告:
This option should never be turned on by any -O option since it can
result in incorrect output for programs which depend on an exact
implementation of IEEE or ISO rules/specifications for math functions.
问:这是启用 -ffast-math 时性能降低的原因吗(请参阅链接的 GCC 快速基准)?
What happens to atan2() when used within condition statement? Is it just inlined?
汇编中没有调用 std::atan2
,因此我们知道它一定是内联扩展的(它也可能作为死代码被删除,但我们可以推断情况并非如此)。
启用 -ffast-math
后,y > 0.f || (y == 0.f && std::atan2(y, -1.f) > 0.f)
基本上被编译为 y >= 0
,因为编译器不关心 -0 和 +0 之间的区别。这是 “可能导致输出不正确”的示例。如果您想按照 IEEE/ISO 处理 -0,请不要使用 -ffast-math
.
这里有两个主要问题与标题问题不同:
您在 quick-bench 上的基准测试看起来有缺陷,允许大量 constant-propagation 进入您的 copysign
实施。检查汇编。也许使用一个全局数组;编译器不知道是否有东西修改了它。并且可能生成一个随机 FP 值(一旦在循环之外)以将符号应用于。
x=rand(); for(...) something = copysign(x, arr[i]);
使用 copysign(x,x)
很奇怪 - 如果函数正确生成该输出,智能编译器可以将其优化到 x
而无需做任何工作。
-ffast-math
意味着 -fno-signed-zeros
,在优化时不关心 signed-zero 语义,从而破坏了您的 copysign 实现。 (y == 0.0f
为真让编译器假设它实际上 是 0.0f,忽略 -0.0f 的可能性。)
题目问题:条件语句中的atan2。
当 y==0.f
时 y == 0.f && std::atan2(y, -1.f) > 0.f
的短路评估仅 运行 秒 atan2
。这种情况很少见,因此通常不会发生。 (虽然没有 -ffast-math
,它仍然会发生。但是 和 -ffast-math
,如果 [=24],编译器假设 y
实际上是 0.0f =] 比较为真,允许它通过 atan2
执行 constant-propagation 并删除调用。)
在 fast-math 的情况下,进一步 constant-propagation 将 atan2() > 0.f
条件变成常量 true
,导致基本为零的 asm 指令。
您已经在 godbolt 上链接了 asm; copysign_1
中的任何地方都没有调用 atan2
,它所做的只是 return (y>0.0f || y == 0.0f) ? fabs(x) : -fabs(x)
。 (因为你用 -ffast-math
编译,这意味着 -fno-signed-zeros
,所以 atan2 的东西可以完全优化掉,除了 jne
上的奇怪 ja
而不是单个 jae
或 jnae
.)
在 copysign_2
中,带有 -ffast-math
的 GCC 确实设法将 float atan2y = std::atan2(y, -1.f);
拉入使用它的条件中。 Clang 也能做到这一点;您链接了 clang 基准测试但链接了 GCC asm,这很奇怪。
但是如果没有 -ffast-math
(就像你在 Quick-bench 上使用的那样),clang 13 不允许这样做,因此它会无条件地为每个输入调用 atan2
。 那是因为默认是-fmath-errno
,很遗憾。
使用任意 y
值调用 atan2
可能是一个可见的 side-effect,因为它会为无效输入值设置 errno
。我认为?也许不会; atan2
手册页说它没有错误(没有设置 errno 的情况),并且在描述中仅在 x 或 y 为 NaN 时返回 NaN。也许编译器没有意识到这一点?但无论出于何种原因,使用 -fno-math-errno
让 GCC 和 clang 将你的无条件 atan2
拉到分支中,所以即使它实际上会调用它,它只发生在罕见的 y == 0.0
情况下(即y
是 +0.0f
或 -0.0f
)
(我想我记得 clang 没有默认为 math-errnor,只有 GCC,但也许他们改变了或者我把它和 [=53= 混在一起了)。你应该总是用 [=47= 编译] 除非你的程序非常奇怪地想检查 errno
是否有 EDOM
或 ERANGE
或库函数调用后类似的 FP 错误,甚至包括 sqrt
,而不是使用 fenv.h
测试 FP 环境标志。)
此外,-ftrapping-math
不幸的是默认情况下启用,即使它不能完全正常工作。它可能会尝试将其视为可能 运行 SIGFPE signal-handler 代码,因此可能是可见的 side-effect。或者至少在 FP 环境中设置一个粘性位,其他代码可以用 fenv
.
读取
顺便说一句,这就是 GCC 编译你的 copysign_1:
的方式
# gcc -O3 -ffast-math
copysign_1(float, float):
comiss xmm1, DWORD PTR .LC1[rip] # compare y against 0.0f
andps xmm0, XMMWORD PTR .LC0[rip] # clear the sign bit in x
ja .L3 # if (y>0) return fabs(x)
jne .L5 # if (y!=0) return -fabs(x)
.L3:
ret
.L5:
xorps xmm0, XMMWORD PTR .LC2[rip] # flip the sign bit of x
ret
如您所见,它因负零而中断,-0.0f
,在这种情况下清除输出的符号位。编译器当然会在没有 -ffast-math
的情况下生成正确的 asm,但速度较慢并且实际上为 -0.0
或 +0.0
.
调用 atan2
顺便说一句,尽管 Quick-Bench 缺少编译器选项设置,但它确实有 -Ofast
,目前相当于 -O3 -ffast-math
。这确实使除了 M_STDNO
之外的所有东西都具有相同的速度。不知道为什么会慢;由于您使用 Benchmark::DoNotOptimize
的方式,asm 涉及很多 store/reload,但仍然允许通过 copysign
函数使用大量 constant-propagation。 (所以他们只是在做 andps
或 orps
来清除或设置某些东西的符号位。)
这对于 IEEE 浮点数
来说非常over-complicated
由于您显然使用的是 C++20,因此您也可以将 std::bit_cast<uint32_t>(x)
和 y
转换为整数并使用按位运算,至少在检查 float
具有其顶部的标志位或其他东西。 (例如 static_assert
-0.0f
具有 bit-pattern 0x80000000
;您可以在 static_assert 等 constexpr 中使用 std::bit_cast
。)
您不需要任何分支或乘法,您只想(u32x & 0x7FFFFFFFU) | (u32y & 0x80000000U)
将 y 的符号位与 x 的指数和尾数合并。
在对 round() 实现中的 copysign() 函数的一个实现进行基准测试时遇到了这个“异常”:
float copysign(float x, float y){
float absx = std::fabs(x);
/* use atan2 to distinguish -0. from 0. */
if (y > 0.f || (y == 0.f && std::atan2(y, -1.f) > 0.f)) {
return absx;
} else {
return absx * -1.0f;
}
}
通过 quick-benc(和另一个衡量 rdtsc/val 的基准实用程序),当 atan2() 从条件语句中移出时,它会导致性能大大降低:
float copysign(float x, float y){
float absx = std::fabs(x);
float atan2y = std::atan2(y, -1.f); /* use atan2 to distinguish -0. from 0. */
if (y > 0.f || (y == 0.f && atan2y > 0.f)) {
return absx;
} else {
return absx *= -1.0f;
}
}
快速 C++ 基准测试:
海湾合作委员会 11.2:(-O3 -ffast-math / -O3)
Another Benchmark utility copysign() 和 round() 实施的结果 (rdtsc/val):
copysign():
~0.60 for "strd" (std::copysign(), same with copysign())
~3.12 for "cs1" (atan2() within condition statement)
~17.5 for "cs2" (atan2() as variable in condition statement)
并且在链接的 round() 实现中使用时:
~6.67 for case "strd"
~2.56 for case "cs1"
~20.5 for case "cs2"
问:在条件语句中使用 atan2() 会发生什么情况?它只是内联吗?
GCC 有关于 -ffast-math 用法的警告:
This option should never be turned on by any -O option since it can result in incorrect output for programs which depend on an exact implementation of IEEE or ISO rules/specifications for math functions.
问:这是启用 -ffast-math 时性能降低的原因吗(请参阅链接的 GCC 快速基准)?
What happens to atan2() when used within condition statement? Is it just inlined?
汇编中没有调用 std::atan2
,因此我们知道它一定是内联扩展的(它也可能作为死代码被删除,但我们可以推断情况并非如此)。
启用 -ffast-math
后,y > 0.f || (y == 0.f && std::atan2(y, -1.f) > 0.f)
基本上被编译为 y >= 0
,因为编译器不关心 -0 和 +0 之间的区别。这是 “可能导致输出不正确”的示例。如果您想按照 IEEE/ISO 处理 -0,请不要使用 -ffast-math
.
这里有两个主要问题与标题问题不同:
您在 quick-bench 上的基准测试看起来有缺陷,允许大量 constant-propagation 进入您的
copysign
实施。检查汇编。也许使用一个全局数组;编译器不知道是否有东西修改了它。并且可能生成一个随机 FP 值(一旦在循环之外)以将符号应用于。
x=rand(); for(...) something = copysign(x, arr[i]);
使用copysign(x,x)
很奇怪 - 如果函数正确生成该输出,智能编译器可以将其优化到x
而无需做任何工作。-ffast-math
意味着-fno-signed-zeros
,在优化时不关心 signed-zero 语义,从而破坏了您的 copysign 实现。 (y == 0.0f
为真让编译器假设它实际上 是 0.0f,忽略 -0.0f 的可能性。)
题目问题:条件语句中的atan2。
当 y==0.f
时 y == 0.f && std::atan2(y, -1.f) > 0.f
的短路评估仅 运行 秒 atan2
。这种情况很少见,因此通常不会发生。 (虽然没有 -ffast-math
,它仍然会发生。但是 和 -ffast-math
,如果 [=24],编译器假设 y
实际上是 0.0f =] 比较为真,允许它通过 atan2
执行 constant-propagation 并删除调用。)
在 fast-math 的情况下,进一步 constant-propagation 将 atan2() > 0.f
条件变成常量 true
,导致基本为零的 asm 指令。
您已经在 godbolt 上链接了 asm; copysign_1
中的任何地方都没有调用 atan2
,它所做的只是 return (y>0.0f || y == 0.0f) ? fabs(x) : -fabs(x)
。 (因为你用 -ffast-math
编译,这意味着 -fno-signed-zeros
,所以 atan2 的东西可以完全优化掉,除了 jne
上的奇怪 ja
而不是单个 jae
或 jnae
.)
在 copysign_2
中,带有 -ffast-math
的 GCC 确实设法将 float atan2y = std::atan2(y, -1.f);
拉入使用它的条件中。 Clang 也能做到这一点;您链接了 clang 基准测试但链接了 GCC asm,这很奇怪。
但是如果没有 -ffast-math
(就像你在 Quick-bench 上使用的那样),clang 13 不允许这样做,因此它会无条件地为每个输入调用 atan2
。 那是因为默认是-fmath-errno
,很遗憾。
使用任意 y
值调用 atan2
可能是一个可见的 side-effect,因为它会为无效输入值设置 errno
。我认为?也许不会; atan2
手册页说它没有错误(没有设置 errno 的情况),并且在描述中仅在 x 或 y 为 NaN 时返回 NaN。也许编译器没有意识到这一点?但无论出于何种原因,使用 -fno-math-errno
让 GCC 和 clang 将你的无条件 atan2
拉到分支中,所以即使它实际上会调用它,它只发生在罕见的 y == 0.0
情况下(即y
是 +0.0f
或 -0.0f
)
(我想我记得 clang 没有默认为 math-errnor,只有 GCC,但也许他们改变了或者我把它和 [=53= 混在一起了)。你应该总是用 [=47= 编译] 除非你的程序非常奇怪地想检查 errno
是否有 EDOM
或 ERANGE
或库函数调用后类似的 FP 错误,甚至包括 sqrt
,而不是使用 fenv.h
测试 FP 环境标志。)
此外,-ftrapping-math
不幸的是默认情况下启用,即使它不能完全正常工作。它可能会尝试将其视为可能 运行 SIGFPE signal-handler 代码,因此可能是可见的 side-effect。或者至少在 FP 环境中设置一个粘性位,其他代码可以用 fenv
.
顺便说一句,这就是 GCC 编译你的 copysign_1:
的方式# gcc -O3 -ffast-math
copysign_1(float, float):
comiss xmm1, DWORD PTR .LC1[rip] # compare y against 0.0f
andps xmm0, XMMWORD PTR .LC0[rip] # clear the sign bit in x
ja .L3 # if (y>0) return fabs(x)
jne .L5 # if (y!=0) return -fabs(x)
.L3:
ret
.L5:
xorps xmm0, XMMWORD PTR .LC2[rip] # flip the sign bit of x
ret
如您所见,它因负零而中断,-0.0f
,在这种情况下清除输出的符号位。编译器当然会在没有 -ffast-math
的情况下生成正确的 asm,但速度较慢并且实际上为 -0.0
或 +0.0
.
atan2
顺便说一句,尽管 Quick-Bench 缺少编译器选项设置,但它确实有 -Ofast
,目前相当于 -O3 -ffast-math
。这确实使除了 M_STDNO
之外的所有东西都具有相同的速度。不知道为什么会慢;由于您使用 Benchmark::DoNotOptimize
的方式,asm 涉及很多 store/reload,但仍然允许通过 copysign
函数使用大量 constant-propagation。 (所以他们只是在做 andps
或 orps
来清除或设置某些东西的符号位。)
这对于 IEEE 浮点数
来说非常over-complicated由于您显然使用的是 C++20,因此您也可以将 std::bit_cast<uint32_t>(x)
和 y
转换为整数并使用按位运算,至少在检查 float
具有其顶部的标志位或其他东西。 (例如 static_assert
-0.0f
具有 bit-pattern 0x80000000
;您可以在 static_assert 等 constexpr 中使用 std::bit_cast
。)
您不需要任何分支或乘法,您只想(u32x & 0x7FFFFFFFU) | (u32y & 0x80000000U)
将 y 的符号位与 x 的指数和尾数合并。