[英]Is there a branchless method to quickly find the min/max of two double-precision floating-point values?
我有两个双打, a
和b
,都在[0,1]中。 由于性能原因,我想要a
和b
的最小值/最大值而不进行分支。
假设a
和b
均为正且小于1,是否有一种有效的方法来获取两者的最小值/最大值? 理想情况下,我不希望分支。
是的,有一种方法可以计算两个double
的最大值或最小值,而无需任何分支。 这样做的C ++代码如下所示:
#include <algorithm>
double FindMinimum(double a, double b)
{
return std::min(a, b);
}
double FindMaximum(double a, double b)
{
return std::max(a, b);
}
我敢打赌,您以前见过。 以免您不相信这是无分支的, 请检查反汇编 :
FindMinimum(double, double):
minsd xmm1, xmm0
movapd xmm0, xmm1
ret
FindMaximum(double, double):
maxsd xmm1, xmm0
movapd xmm0, xmm1
ret
这就是从所有针对x86的流行编译器中获得的。 使用SSE2指令集,特别是minsd
/ maxsd
指令,该指令无maxsd
评估两个双精度浮点值的最小值/最大值。
所有64位x86处理器均支持SSE2 ; AMD64扩展需要它。 即使是大多数不带64位的x86处理器也支持SSE2。 它于2000年发布。您必须走很长一段路才能找到不支持SSE2的处理器。 但是,如果您呢? 好吧,即使到了那里, 您也可以在大多数流行的编译器上获得无分支代码 :
FindMinimum(double, double):
fld QWORD PTR [esp + 12]
fld QWORD PTR [esp + 4]
fucomi st(1)
fcmovnbe st(0), st(1)
fstp st(1)
ret
FindMaximum(double, double):
fld QWORD PTR [esp + 4]
fld QWORD PTR [esp + 12]
fucomi st(1)
fxch st(1)
fcmovnbe st(0), st(1)
fstp st(1)
ret
fucomi
指令执行比较并设置标志,然后fcmovnbe
指令根据这些标志的值执行条件移动。 这一切都是完全无分支的,并依赖于1995年Pentium Pro引入x86 ISA的指令,该指令自Pentium II以来在所有x86芯片上均受支持。
此处唯一不会生成无FCMOVxx
代码的编译器是MSVC,因为它没有利用FCMOVxx
指令 。 相反,您得到:
double FindMinimum(double, double) PROC
fld QWORD PTR [a]
fld QWORD PTR [b]
fcom st(1) ; compare "b" to "a"
fnstsw ax ; transfer FPU status word to AX register
test ah, 5 ; check C0 and C2 flags
jp Alt
fstp st(1) ; return "b"
ret
Alt:
fstp st(0) ; return "a"
ret
double FindMinimum(double, double) ENDP
double FindMaximum(double, double) PROC
fld QWORD PTR [b]
fld QWORD PTR [a]
fcom st(1) ; compare "b" to "a"
fnstsw ax ; transfer FPU status word to AX register
test ah, 5 ; check C0 and C2 flags
jp Alt
fstp st(0) ; return "b"
ret
Alt:
fstp st(1) ; return "a"
ret
double FindMaximum(double, double) ENDP
注意分支JP
指令(如果奇偶校验位置1,则跳转)。 FCOM
指令用于进行比较,这是基本x87 FPU指令集的一部分。 不幸的是,这会在FPU状态字中设置标志,因此为了分支这些标志,需要将其提取。 这就是FNSTSW
指令的目的,该指令将x87 FPU状态字存储到通用AX
寄存器中(它也可以存储到内存中,但是……为什么?)。 然后,代码将TEST
为适当的位,并进行相应分支以确保返回正确的值。 除了分支之外,检索FPU状态字也将相对较慢。 这就是Pentium Pro引入FCOM
指令的原因。
但是,通过位旋转操作确定最小/最大,您不太可能提高任何代码的速度。 有两个基本原因:
唯一生成低效代码的编译器是MSVC,没有任何强制方法来强制它生成所需的指令。 尽管MSVC支持32位x86目标的内联汇编, 但是在寻求性能改进时 , 这是愚蠢的 。 我还将引用自己:
内联汇编会以相当大的方式破坏优化器,因此,除非您在内联汇编中编写大量代码,否则不太可能获得实质性的净性能提升。 此外,Microsoft的内联汇编语法非常有限。 它在很大程度上以灵活性换取了简便性。 特别是,无法指定输入值,因此您不得不将输入从内存中加载到寄存器中,并且调用者被迫将输入从寄存器中溢出到内存中进行准备。 这会造成一种现象,我喜欢称之为“整个过程”,或者简称为“慢代码”。 在可接受慢速代码的情况下,您不会陷入内联汇编。 因此,总是最好(至少在MSVC上)弄清楚如何编写可说服编译器发出所需目标代码的C / C ++源代码。 即使您只能接近理想的输出,也仍然比使用内联汇编所付出的代价要好得多。
为了访问浮点值的原始位,您必须进行域转换,从浮点到整数,然后再回到浮点。 这很慢, 尤其是在没有SSE2的情况下,因为从x87 FPU到ALU中的通用整数寄存器获取值的唯一方法是间接通过内存。
如果您仍然想采用这种策略(例如,对其进行基准测试),则可以利用以下事实:浮点值按照其IEEE 754表示法按字典顺序排序,除了符号位。 因此,由于您假设两个值都是正值:
FindMinimumOfTwoPositiveDoubles(double a, double b):
mov rax, QWORD PTR [a]
mov rdx, QWORD PTR [b]
sub rax, rdx ; subtract bitwise representation of the two values
shr rax, 63 ; isolate the sign bit to see if the result was negative
ret
FindMaximumOfTwoPositiveDoubles(double a, double b):
mov rax, QWORD PTR [b] ; \ reverse order of parameters
mov rdx, QWORD PTR [a] ; / for the SUB operation
sub rax, rdx
shr rax, 63
ret
或者,为避免内联汇编:
bool FindMinimumOfTwoPositiveDoubles(double a, double b)
{
static_assert(sizeof(a) == sizeof(uint64_t),
"A double must be the same size as a uint64_t for this bit manipulation to work.");
const uint64_t aBits = *(reinterpret_cast<uint64_t*>(&a));
const uint64_t bBits = *(reinterpret_cast<uint64_t*>(&b));
return ((aBits - bBits) >> ((sizeof(uint64_t) * CHAR_BIT) - 1));
}
bool FindMaximumOfTwoPositiveDoubles(double a, double b)
{
static_assert(sizeof(a) == sizeof(uint64_t),
"A double must be the same size as a uint64_t for this bit manipulation to work.");
const uint64_t aBits = *(reinterpret_cast<uint64_t*>(&a));
const uint64_t bBits = *(reinterpret_cast<uint64_t*>(&b));
return ((bBits - aBits) >> ((sizeof(uint64_t) * CHAR_BIT) - 1));
}
请注意,有严重警告此实现。 特别是,如果两个浮点值具有不同的符号,或者两个值都为负,则它将中断。 如果两个值均为负,则可以修改代码以翻转其符号,进行比较,然后返回相反的值。 要处理两个值具有不同符号的情况,可以添加代码以检查符号位。
// ...
// Enforce two's-complement lexicographic ordering.
if (aBits < 0)
{
aBits = ((1 << ((sizeof(uint64_t) * CHAR_BIT) - 1)) - aBits);
}
if (bBits < 0)
{
bBits = ((1 << ((sizeof(uint64_t) * CHAR_BIT) - 1)) - bBits);
}
// ...
处理负零也将是一个问题。 IEEE 754表示+0.0等于-0.0,因此比较函数必须决定是否要将这些值视为不同,或者向比较例程添加特殊代码以确保将负零和正零视为等效。
添加所有这些特殊情况的代码肯定会降低性能,以至于我们无法通过简单的浮点比较来达到收支平衡,并且很可能最终会变得更慢。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.