[英]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.