[英]How can I safely average two unsigned ints in C++?
單獨使用整數數學,我想在 C++ 中“安全地”平均兩個無符號整數。
我所說的“安全”是指避免溢出(以及任何其他可以想到的東西)。
and is easy:例如,平均和很容易:
unsigned int a = 200;
unsigned int b = 5000;
unsigned int average = (a + b) / 2; // Equals: 2600 as intended
and then:但在和的情況下:
unsigned int a = 4294967295;
unsigned int b = 5000;
unsigned int average = (a + b) / 2; // Equals: 2499 instead of 2147486147
我想出的最好的是:
unsigned int a = 4294967295;
unsigned int b = 5000;
unsigned int average = (a / 2) + (b / 2); // Equals: 2147486147 as expected
有更好的方法嗎?
您的最后一種方法似乎很有希望。 您可以通過手動考慮 a 和 b 的最低位來改進它:
unsigned int average = (a / 2) + (b / 2) + (a & b & 1);
在 a 和 b 都是奇數的情況下,這給出了正確的結果。
如果你提前知道哪個更高,那么一種非常有效的方法是可能的。 否則,您最好使用其他策略之一,而不是有條件地交換使用它。
unsigned int average = low + ((high - low) / 2);
這是一篇相關文章: http : //googleresearch.blogspot.com/2006/06/extra-extra-read-all-about-it-nearly.html
如果兩個數字都是奇數,例如 5 和 7,則您的方法不正確,平均值為 6,但您的方法 #3 返回 5。
試試這個:
average = (a>>1) + (b>>1) + (a & b & 1)
僅使用數學運算符:
average = a/2 + b/2 + (a%2) * (b%2)
如果您不介意一點 x86 內聯匯編(GNU C 語法),您可以利用 supercat 的建議,在添加后使用rotate-with-carry將完整 33 位結果的高 32 位放入寄存器.
當然,您通常應該介意使用 inline-asm,因為它會破壞一些優化( https://gcc.gnu.org/wiki/DontUseInlineAsm )。 但無論如何我們都要去:
// works for 64-bit long as well on x86-64, and doesn't depend on calling convention
unsigned average(unsigned x, unsigned y)
{
unsigned result;
asm("add %[x], %[res]\n\t"
"rcr %[res]"
: [res] "=r" (result) // output
: [y] "%0"(y), // input: in the same reg as results output. Commutative with next operand
[x] "rme"(x) // input: reg, mem, or immediate
: // no clobbers. ("cc" is implicit on x86)
);
return result;
}
%
修飾符告訴編譯器 args 是可交換的,在我嘗試過的情況下,實際上並沒有幫助改進 asm,調用函數時 y 是常量或指針取消引用(內存操作數)。 可能對輸出操作數使用匹配約束會失敗,因為您不能將它與讀寫操作數一起使用。
正如您在 Godbolt 編譯器資源管理器上看到的,這可以正確編譯,我們將操作數更改為unsigned long
的版本也是如此,具有相同的內聯 asm。 但是,clang3.9 把它弄得一團糟,並決定對"rme"
約束使用"m"
選項,因此它存儲到內存並使用內存操作數。
RCR-by-one 並不太慢,但在 Skylake 上仍然是 3 uop,有 2 個周期的延遲。 它非常適合 AMD CPU,其中 RCR 具有單周期延遲。 (來源: Agner Fog 的指令表,另請參閱x86標簽 wiki 以獲取 x86 性能鏈接)。 它仍然比@sellibitze 的版本好,但比@Sheldon 的依賴訂單的版本差。 (參見 Godbolt 上的代碼)
但請記住,inline-asm 會擊敗諸如常量傳播之類的優化,因此在這種情況下,任何純 C++ 版本都會更好。
而正確答案是...
(A&B)+((A^B)>>1)
你所擁有的很好,有一個小細節,它會聲稱 3 和 3 的平均值是 2。我猜你不想要那樣; 幸運的是,有一個簡單的解決方法:
unsigned int average = a/2 + b/2 + (a & b & 1);
在兩個部門都被截斷的情況下,這只會使平均值上升。
如果代碼用於嵌入式微,並且速度至關重要,則匯編語言可能會有所幫助。 在許多微控制器上,加法的結果自然會進入進位標志,並且存在將其移回寄存器的指令。 在 ARM 上,平均操作(寄存器中的源和目標)可以在兩條指令中完成; 任何等效的 C 語言都可能產生至少 5 個,並且可能比這多一點。
順便說一句,在字長較短的機器上,差異可能更大。 在 8 位 PIC-18 系列上,平均兩個 32 位數字需要 12 條指令。 進行移位、加法和校正,每個移位需要 5 條指令,加法 8 條,校正 8 條,所以 26(不是 2.5 倍的差異,但絕對值可能更重要)。
在 C++20 中,您可以使用std::midpoint
:
template <class T>
constexpr T midpoint(T a, T b) noexcept;
介紹std::midpoint
的論文P0811R3推薦了這個片段(稍微采用了 C++11):
#include <type_traits>
template <typename Integer>
constexpr Integer midpoint(Integer a, Integer b) noexcept {
using U = std::make_unsigned<Integer>::type;
return a>b ? a-(U(a)-b)/2 : a+(U(b)-a)/2;
}
為了完整起見,這里是論文中未修改的 C++20 實現:
constexpr Integer midpoint(Integer a, Integer b) noexcept {
using U = make_unsigned_t<Integer>;
return a>b ? a-(U(a)-b)/2 : a+(U(b)-a)/2;
}
int[] array = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
decimal avg = 0;
for (int i = 0; i < array.Length; i++){
avg = (array[i] - avg) / (i+1) + avg;
}
預計此測試的 avg == 5.0
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.