是的,有办法计算两个的最大值或最小值 double s没有任何分支。这样做的C ++代码如下所示:
double
#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 指令,无分支地评估两个双精度浮点值的最小值/最大值。
minsd
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芯片。
fucomi
fcmovnbe
唯一的编译器 惯于 这里生成无分支代码是MSVC,因为 它没有利用 FCMOVxx 指令 。相反,你得到:
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 指令(如果设置了奇偶校验位,则跳转)该 FCOM 指令用于进行比较,它是基本x87 FPU指令集的一部分。不幸的是,这会在FPU状态字中设置标志,因此为了分支这些标志,需要提取它们。这就是目的 FNSTSW 指令,将x87 FPU状态字存储到通用目的 AX 注册(它也可以存储到内存中,但是为什么?)。那么代码 TEST s相应的位,并相应地分支以确保返回正确的值。除了分支之外,检索FPU状态字也将相对较慢。这就是Pentium Pro推出的原因 FCOM 说明。
JP
FCOM
FNSTSW
AX
TEST
但是,确实如此 不会 通过使用bit-twiddling操作来确定min / max,您将能够提高任何代码的速度。有两个基本原因:
生成低效代码的唯一编译器是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,因此您的比较函数必须决定是否要将这些值视为不同,或者将特殊代码添加到确保负零和正零的比较例程被视为等价。
添加所有这些特殊情况代码将 当然 将性能降低到我们将通过一个简单的浮点比较来实现收支平衡,并且最终可能会变慢。