jye*_*lon 5 assembly gcc x86-64 micro-optimization
我这里有一些例程,它们都做同样的事情:它们将浮点数限制在 [0,65535] 范围内。令我惊讶的是,编译器(gcc -O3)使用三种不同的方式来实现 float-min 和 float-max。我想了解为什么它会生成三种不同的实现。好的,这是 C++ 代码:
float clamp1(float x) {
x = (x < 0.0f) ? 0.0f : x;
x = (x > 65535.0f) ? 65535.0f : x;
return x;
}
float clamp2(float x) {
x = std::max(0.0f, x);
x = std::min(65535.0f, x);
return x;
}
float clamp3(float x) {
x = std::min(65535.0f, x);
x = std::max(0.0f, x);
return x;
}
Run Code Online (Sandbox Code Playgroud)
这是生成的程序集(删除了一些样板)。可在https://godbolt.org/z/db775on4j上使用 GCC10.3重现-O3
。(还显示 clang14 的选择。)
CLAMP1:
movaps %xmm0, %xmm1
pxor %xmm0, %xmm0
comiss %xmm1, %xmm0
ja .L9
movss .LC1(%rip), %xmm0 # 65535.0f
movaps %xmm0, %xmm2
cmpltss %xmm1, %xmm2
andps %xmm2, %xmm0
andnps %xmm1, %xmm2
orps %xmm2, %xmm0
.L9:
ret
Run Code Online (Sandbox Code Playgroud)
CLAMP2:
pxor %xmm1, %xmm1
comiss %xmm1, %xmm0
ja .L20
pxor %xmm0, %xmm0
ret
.L20:
minss .LC1(%rip), %xmm0 # 65535.0f
ret
Run Code Online (Sandbox Code Playgroud)
CLAMP3:
movaps %xmm0, %xmm1
movss .LC1(%rip), %xmm0 # 65535.0f
comiss %xmm1, %xmm0
ja .L28
ret
.L28:
maxss .LC2(%rip), %xmm1 # 0.0f
movaps %xmm1, %xmm0
ret
Run Code Online (Sandbox Code Playgroud)
所以这里似乎有 MIN 和 MAX 的三种不同实现:
minss
和maxss
andps
、andnps
和orps
。有人可以澄清以下问题:
andps
带有、andnps
、 等等的东西到底是什么?minss
和maxss
且不使用分支会更快吗?这些速度都是一样的,还是其中一个速度更快?
它不一样,但差异取决于机器和给定的输入。
编译器最终如何选择所有这些不同的实现?
因为编译器并不像你想象的那么智能。
andps、andnps 等东西到底是什么?
就是下面这个逻辑。
float y = 65535;
float m = std::bit_cast<float>(-(x < y));
return x & m | y & ~m;
Run Code Online (Sandbox Code Playgroud)
您无法&
在 C++(和 C)中处理浮点数,但您会明白的。
同时使用 minss 和 maxss 并且不使用分支会更快吗?
通常,是的。您可以在 ( https://uops.info/table.html )查看每条指令的延迟和吞吐量。我会在汇编中编写以下内容。
xorps xmm1, xmm1
minss xmm0, [_ffff]
maxss xmm0, xmm1
Run Code Online (Sandbox Code Playgroud)
然而,如果你知道某个分支很可能被采用,那么用分支跳过 a 可能会更快。让我们看看编译器如何处理分支提示。
float clamp_minmax(float x) {
return std::max(std::min(x, 65535.0f), 0.0f);
}
float clamp_hint(float x) {
if (__builtin_expect(x > 65535, 0)) {
return 65535;
}
if (__builtin_expect(x < 0, 0)) {
return 0;
}
return x;
}
Run Code Online (Sandbox Code Playgroud)
GCC 基本上不在乎,但有趣的是 Clang 会生成不同的代码。
clamp_minmax(float):
movss xmm1, dword ptr [rip + .LCPI0_0]
minss xmm1, xmm0
xorps xmm0, xmm0
maxss xmm0, xmm1
ret
clamp_hint(float):
movaps xmm1, xmm0
movss xmm0, dword ptr [rip + .LCPI1_0]
ucomiss xmm1, xmm0
ja .LBB1_2
xorps xmm0, xmm0
maxss xmm0, xmm1
.LBB1_2:
ret
Run Code Online (Sandbox Code Playgroud)
ucomiss -> ja
并不比单个版本快minss
,但是如果您可以maxss
通过分支跳过大部分时间,则分支版本可能会比无分支版本运行得更快,因为您可以跳过maxss
无分支版本中不可避免的延迟,因为它依赖于以前的minss
。
另请参阅(在 x86 上提供无分支 FP 最小值和最大值的指令是什么?)。
归档时间: |
|
查看次数: |
433 次 |
最近记录: |