簡體   English   中英

是否有一種無分支方法可以快速找到兩個雙精度浮點值的最小值/最大值?

[英]Is there a branchless method to quickly find the min/max of two double-precision floating-point values?

我有兩個雙打, ab ,都在[0,1]中。 由於性能原因,我想要ab的最小值/最大值而不進行分支。

假設ab均為正且小於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指令的原因。

但是,通過位旋轉操作確定最小/最大,您不太可能提高任何代碼的速度。 有兩個基本原因:

  1. 唯一生成低效代碼的編譯器是MSVC,沒有任何強制方法來強制它生成所需的指令。 盡管MSVC支持32位x86目標的內聯匯編, 但是在尋求性能改進時這是愚蠢的 我還將引用自己:

    內聯匯編會以相當大的方式破壞優化器,因此,除非您在內聯匯編中編寫大量代碼,否則不太可能獲得實質性的凈性能提升。 此外,Microsoft的內聯匯編語法非常有限。 它在很大程度上以靈活性換取了簡便性。 特別是,無法指定輸入值,因此您不得不將輸入從內存中加載到寄存器中,並且調用者被迫將輸入從寄存器中溢出到內存中進行准備。 這會造成一種現象,我喜歡稱之為“整個過程”,或者簡稱為“慢代碼”。 在可接受慢速代碼的情況下,您不會陷入內聯匯編。 因此,總是最好(至少在MSVC上)弄清楚如何編寫可說服編譯器發出所需目標代碼的C / C ++源代碼。 即使您只能接近理想的輸出,也仍然比使用內聯匯編所付出的代價要好得多。

  2. 為了訪問浮點值的原始位,您必須進行域轉換,從浮點到整數,然后再回到浮點。 這很慢, 尤其是在沒有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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM