如何在C ++中安全地平均两个无符号整数?

时间:2010-09-28 19:43:59

标签: c++ math unsigned-integer

单独使用整数数学,我想在C ++中“安全地”平均两个无符号整数。

我所说的“安全”是避免溢出(以及其他任何可以想到的事情)。

例如,平均 200 5000 很容易:

unsigned int a = 200;
unsigned int b = 5000;
unsigned int average = (a + b) / 2; // Equals: 2600 as intended

但是在 4294967295 5000 的情况下:

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

有更好的方法吗?

11 个答案:

答案 0 :(得分:49)

你最后的方法看起来很有希望。您可以通过手动考虑a和b的最低位来改进:

unsigned int average = (a / 2) + (b / 2) + (a & b & 1);

如果a和b都是奇数,则给出正确的结果。

答案 1 :(得分:27)

unsigned int average = low + ((high - low) / 2);

修改

以下是相关文章:http://googleresearch.blogspot.com/2006/06/extra-extra-read-all-about-it-nearly.html

答案 2 :(得分:17)

如果两个数字都是奇数,例如5和7,平均值为6,但你的方法#3返回5,则你的方法不正确。

试试这个:

average = (a>>1) + (b>>1) + (a & b & 1)

仅限数学运算符:

average = a/2 + b/2 + (a%2) * (b%2)

答案 3 :(得分:9)

如果你不介意一点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;
}

% modifier告诉编译器args是可交换的,实际上并没有帮助在我尝试的情况下做出更好的asm,调用y是常量或指针deref(内存操作数)的函数。可能对输出操作数使用匹配约束会使其失败,因为您无法将其与读写操作数一起使用。

正如您所看到的on the Godbolt compiler explorer,这可以正确编译,我们将操作数更改为unsigned long的版本也是如此,并使用相同的内联asm。然而,clang3.9弄得一团糟,并且决定对"m"约束使用"rme"选项,因此它存储到内存并使用内存操作数。


RCR-by-one并不是太慢,但Skylake上还有3个uop,有2个周期延迟。它在AMD CPU上非常出色,其中RCR具有单周期延迟。 (来源:Agner Fog's instruction tables,另请参阅标记wiki for x86性能链接)。它仍然比@ sellibitze的版本更好,但比@ Sheldon的依赖订单的版本更糟糕。 (参见Godbolt的代码)

但请记住,inline-asm会破坏常量传播等优化,因此在这种情况下,任何纯C ++版本都会更好。

答案 4 :(得分:7)

正确答案是......

(A&B)+((A^B)>>1)

答案 5 :(得分:4)

你所拥有的一切都很好,有一个小细节,它会声称平均3和3是2.我猜你不想要那个;幸运的是,有一个简单的解决方法:

unsigned int average = a/2 + b/2 + (a & b & 1);

在两个部门被截断的情况下,这只会使平均值回升。

答案 6 :(得分:2)

如果代码用于嵌入式微型,并且速度至关重要,汇编语言可能会有所帮助。在许多微控制器上,添加的结果自然会进入进位标志,并且存在将指令移回寄存器的指令。在ARM上,平均操作(寄存器中的源和目标)可以在两个指令中完成;任何C语言的等价物都可能产生至少5,并且可能比这更多。

顺便说一句,在字长较短的机器上,差异可能更大。在8位PIC-18系列上,平均两个32位数字需要12条指令。进行移位,添加和校正,每个班次需要5个指令,8个用于加法,8个用于校正,因此26个(不是相差2.5倍,但绝对值可能更重要)。

答案 7 :(得分:0)

在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;
}

答案 8 :(得分:-1)

最后的方法

unsigned int average = (a / 2) + (b / 2); // Equals: 2147486147 as expected

有时由于舍入错误而无法正常工作。

答案 9 :(得分:-2)

(((a&b << 1) + (a^b)) >> 1)也是一个很好的方式。

礼貌:http://www.ragestorm.net/blogs/?p=29

答案 10 :(得分:-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