C++ 是否有一种无分支的方法可以快速找到两个双精度浮点值的最小值/最大值?

C++ 是否有一种无分支的方法可以快速找到两个双精度浮点值的最小值/最大值?,c++,bit-manipulation,numerical-methods,C++,Bit Manipulation,Numerical Methods,我有两个替身,a和b,都在[0,1]中。由于性能原因,我希望a和b的最小/最大值不进行分支 鉴于a和b均为正值,且均小于1,是否有一种有效的方法来获取两者的最小值/最大值?理想情况下,我不需要分支。是的,有一种方法可以计算两个doubles的最大值或最小值,而不需要任何分支。这样做的C++代码看起来是这样的: #include <algorithm> double FindMinimum(double a, double b) { return std::min(a, b)

我有两个替身,
a
b
,都在[0,1]中。由于性能原因,我希望
a
b
的最小/最大值不进行分支


鉴于
a
b
均为正值,且均小于1,是否有一种有效的方法来获取两者的最小值/最大值?理想情况下,我不需要分支。

是的,有一种方法可以计算两个
double
s的最大值或最小值,而不需要任何分支。这样做的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);
}
这是从所有面向x86的流行编译器中得到的。使用SSE2指令集,特别是
minsd
/
maxsd
指令,它无分支地计算两个双精度浮点值的最小值/最大值

所有64位x86处理器都支持;它是AMD64扩展所必需的。即使是大多数没有64位支持SSE2的x86处理器。它于2000年发行。要找到一个不支持SSE2的处理器,您需要走很长的路。但是如果你做了呢?即使在那里

fucomi
指令执行比较,设置标志,然后
fcmovnbe
指令根据这些标志的值执行条件移动。这完全是无分支的,并且依赖于1995年Pentium Pro引入x86 ISA的指令,自Pentium II以来,所有x86芯片都支持这些指令

这里唯一不会生成无分支代码的编译器是MSVC,因为。相反,你会得到:

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
寄存器(它也可以存储到内存,但…为什么?)。然后,代码测试相应的位,并相应地进行分支,以确保返回正确的值。除了分支之外,检索FPU状态字的速度也相对较慢。这就是为什么奔腾Pro引入了
FCOM
指令

但是,通过使用位旋转操作来确定最小值/最大值,不太可能提高这些代码的速度。有两个基本原因:

  • 唯一生成低效代码的编译器是MSVC,没有好的方法强迫它生成您想要的指令。尽管MSVC支持32位x86目标的内联程序集。我还要引用我自己的话:

    内联汇编以相当重要的方式破坏了优化器,因此,除非您在内联汇编中编写大量代码,否则不太可能有显著的净性能提升。此外,Microsoft的内联汇编语法非常有限。它在很大程度上以灵活性换取简单性。特别是,无法指定输入值,因此无法将输入从内存加载到寄存器中,调用者在准备时被迫将输入从寄存器溢出到内存。这就产生了一种现象,我喜欢称之为“一大堆乱七八糟的事情”,或者简称为“慢代码”。在可以接受慢速代码的情况下,您不会使用内联程序集。因此,最好(至少在MSVC上)弄清楚如何编写C/C++源代码,说服编译器发出所需的目标代码。即使您只能接近理想的输出,这仍然比使用内联汇编所付出的代价要好得多

  • 为了访问浮点值的原始位,您必须进行域转换,从浮点到整数,然后再回到浮点。这很慢,特别是没有SSE2,因为从x87 FPU到ALU中的通用整数寄存器获取值的唯一方法是间接通过内存

  • 如果你想采用这种策略,比如说,要对其进行基准测试,你可以利用这样一个事实,即浮点值是按字母顺序排列的,除了符号位。既然你假设两个值都是正的:

    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));
    }
    
    bool FindMinimumOfTwoPositiveDoubles(双a,双b)
    {
    静态断言(sizeof(a)=sizeof(uint64\u t),
    “一个双精度计数器必须与uint64_t的大小相同,才能进行位操作。”);
    const uint64_t aBits=*(重新解释类型(&a));
    const uint64_t bBits=*(重新解释类型(&b));
    返回((aBits-bBits)>>((sizeof(uint64\u t)*字符位)-1);
    }
    布尔查找两个正整数的最大值(双a,双b)
    {
    静态断言(sizeof(a)=sizeof(uint64\u t),
    “一个双精度计数器必须与uint64_t的大小相同,才能进行位操作。”);
    const uint64_t aBits=*(重新解释类型(&a));
    const uint64_t bBits=*(重新解释类型(&b));
    返回((bBits-aBits)>>((sizeof(uint64\u t)*字符位)-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);
        }
    
        // ...
    
    /。。。
    //执行二的补码词典排序。
    if(aBits<0)
    {
    
    aBits=((1)听起来有点像过早优化。除非你已经有了一些东西,经过测量,发现这是一个热门的sp
    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);
        }
    
        // ...