49

在 C 中编写饱和加法的最佳(最干净、最有效)的方法是什么?

如果总和溢出,函数或宏应添加两个无符号输入(需要 16 位和 32 位版本)并返回全位为一(0xFFFF 或 0xFFFFFFFF)。

目标是使用 gcc (4.1.2) 和 Visual Studio 的 x86 和 ARM(仅用于模拟,因此可以使用后备实现)。

4

18 回答 18

30

您可能希望在此处使用可移植的 C 代码,您的编译器会将其转换为正确的 ARM 程序集。ARM 有条件移动,这些移动可以以溢出为条件。然后算法变为:添加并有条件地将目标设置为无符号(-1),如果检测到溢出。

uint16_t add16(uint16_t a, uint16_t b)
{
  uint16_t c = a + b;
  if (c < a)  /* Can only happen due to overflow */
    c = -1;
  return c;
}

请注意,这与其他算法的不同之处在于它纠正了溢出,而不是依靠另一个计算来检测溢出。

add32 的 x86-64 clang 3.7 -O3 输出:明显优于任何其他答案:

add     edi, esi
mov     eax, -1
cmovae  eax, edi
ret

ARMv7:gcc 4.8 -O3 -mcpu=cortex-a15 -fverbose-asmadds32 的输出

adds    r0, r0, r1      @ c, a, b
it      cs
movcs   r0, #-1         @ conditional-move
bx      lr

16 位:仍然不使用 ARM 的无符号饱和加法指令 ( UADD16)

add     r1, r1, r0        @ tmp114, a
movw    r3, #65535      @ tmp116,
uxth    r1, r1  @ c, tmp114
cmp     r0, r1    @ a, c
ite     ls        @
movls   r0, r1        @,, c
movhi   r0, r3        @,, tmp116
bx      lr  @
于 2008-10-03T11:22:18.257 回答
24

在普通 C 中:

uint16_t sadd16(uint16_t a, uint16_t b) {
  return (a > 0xFFFF - b) ? 0xFFFF : a + b;
}
     
uint32_t sadd32(uint32_t a, uint32_t b) {
  return (a > 0xFFFFFFFF - b) ? 0xFFFFFFFF : a + b;
}

几乎是宏观化的,直接传达意思。

于 2008-09-23T16:57:56.747 回答
18

在没有条件跳转的 IA32 中:

uint32_t sadd32(uint32_t a, uint32_t b)
{
#if defined IA32
  __asm
  {
    mov eax,a
    xor edx,edx
    add eax,b
    setnc dl
    dec edx
    or eax,edx
  }
#elif defined ARM
  // ARM code
#else
  // non-IA32/ARM way, copy from above
#endif
}
于 2008-09-23T14:31:08.627 回答
11

在 ARM 中,您可能已经内置了饱和算术。ARMv5 DSP 扩展可以使寄存器饱和到任何位长。同样在 ARM 上,饱和通常很便宜,因为您可以有条件地执行大多数指令。

ARMv6 甚至具有饱和加法、减法和所有其他 32 位和压缩数字的东西。

在 x86 上,您可以通过 MMX 或 SSE 获得饱和算术。

所有这些都需要汇编程序,所以这不是您所要求的。

也有 C 技巧可以做饱和算术。这个小代码对 dword 的四个字节进行饱和加法。它基于并行计算32个半加器的想法,例如添加数字而没有进位溢出。

这是首先完成的。然后计算进位,添加并在加法溢出时用掩码替换。

uint32_t SatAddUnsigned8(uint32_t x, uint32_t y) 
{
  uint32_t signmask = 0x80808080;
  uint32_t t0 = (y ^ x) & signmask;
  uint32_t t1 = (y & x) & signmask;
  x &= ~signmask;
  y &= ~signmask;
  x += y;
  t1 |= t0 & x;
  t1 = (t1 << 1) - (t1 >> 7);
  return (x ^ t0) | t1;
}

您可以通过更改符号掩码常量和底部的移位来获得相同的 16 位(或任何类型的位域),如下所示:

uint32_t SatAddUnsigned16(uint32_t x, uint32_t y) 
{
  uint32_t signmask = 0x80008000;
  uint32_t t0 = (y ^ x) & signmask;
  uint32_t t1 = (y & x) & signmask;
  x &= ~signmask;
  y &= ~signmask;
  x += y;
  t1 |= t0 & x;
  t1 = (t1 << 1) - (t1 >> 15);
  return (x ^ t0) | t1;
}

uint32_t SatAddUnsigned32 (uint32_t x, uint32_t y)
{
  uint32_t signmask = 0x80000000;
  uint32_t t0 = (y ^ x) & signmask;
  uint32_t t1 = (y & x) & signmask;
  x &= ~signmask;
  y &= ~signmask;
  x += y;
  t1 |= t0 & x;
  t1 = (t1 << 1) - (t1 >> 31);
  return (x ^ t0) | t1;
}

上面的代码对 16 位和 32 位值执行相同的操作。

如果您不需要函数并行添加和饱和多个值的功能,只需屏蔽您需要的位。在 ARM 上,您还想更改符号掩码常量,因为 ARM 无法在单个周期内加载所有可能的 32 位常量。

编辑:并行版本很可能比直接方法慢,但如果您必须一次饱和多个值,它们会更快。

于 2008-09-23T14:26:12.180 回答
10

如果您关心性能,那么您真的想在 SIMD 中做这种事情,其中​​ x86 具有本机饱和算法。

由于标量数学中缺乏饱和算术,因此可能会遇到这样的情况:在 4 变量宽 SIMD 上完成的运算等效的 C 快 4 倍以上(相应地,对于 8 变量宽 SIMD 也是如此):

sub8x8_dct8_c: 1332 clocks
sub8x8_dct8_mmx: 182 clocks
sub8x8_dct8_sse2: 127 clocks
于 2008-09-23T17:07:19.907 回答
10

零分支解决方案:

uint32_t sadd32(uint32_t a, uint32_t b)
{
    uint64_t s = (uint64_t)a+b;
    return -(s>>32) | (uint32_t)s;
}

一个好的编译器会对此进行优化以避免执行任何实际的 64 位算术(s>>32将仅仅是进位标志,并且-(s>>32)是 的结果sbb %eax,%eax)。

在 x86 asm 中(AT&T 语法,在aand中,结果为):beaxebxeax

add %eax,%ebx
sbb %eax,%eax
or %ebx,%eax

8 位和 16 位版本应该很明显。签名版本可能需要更多的工作。

于 2010-08-07T19:12:30.933 回答
7
uint32_t saturate_add32(uint32_t a, uint32_t b)
{
    uint32_t sum = a + b;
    if ((sum < a) || (sum < b))
        return ~((uint32_t)0);
    else
        return sum;
} /* saturate_add32 */

uint16_t saturate_add16(uint16_t a, uint16_t b)
{
    uint16_t sum = a + b;
    if ((sum < a) || (sum < b))
        return ~((uint16_t)0);
    else
        return sum;
} /* saturate_add16 */

编辑:既然您已经发布了您的版本,我不确定我的版本是否更清洁/更好/更高效/更研究。

于 2008-09-23T14:17:08.863 回答
3

我们正在使用的当前实现是:

#define sadd16(a, b)  (uint16_t)( ((uint32_t)(a)+(uint32_t)(b)) > 0xffff ? 0xffff : ((a)+(b)))
#define sadd32(a, b)  (uint32_t)( ((uint64_t)(a)+(uint64_t)(b)) > 0xffffffff ? 0xffffffff : ((a)+(b)))
于 2008-09-23T14:18:30.200 回答
3

我不确定这是否比 Skizz 的解决方案(总是配置文件)更快,但这里有一个替代的无分支组装解决方案。请注意,这需要条件移动(CMOV)指令,我不确定您的目标是否可用。


uint32_t sadd32(uint32_t a, uint32_t b)
{
    __asm
    {
        movl eax, a
        addl eax, b
        movl edx, 0xffffffff
        cmovc eax, edx
    }
}
于 2008-09-23T15:37:28.653 回答
2

最好的性能通常涉及内联汇编(正如一些人已经说过的)。

但是对于可移植的 C,这些功能只涉及一个比较并且没有类型转换(因此我认为是最佳的):

unsigned saturate_add_uint(unsigned x, unsigned y)
{
    if (y > UINT_MAX - x) return UINT_MAX;
    return x + y;
}

unsigned short saturate_add_ushort(unsigned short x, unsigned short y)
{
    if (y > USHRT_MAX - x) return USHRT_MAX;
    return x + y;
}

作为宏,它们变成:

SATURATE_ADD_UINT(x, y) (((y)>UINT_MAX-(x)) ? UINT_MAX : ((x)+(y)))
SATURATE_ADD_USHORT(x, y) (((y)>SHRT_MAX-(x)) ? USHRT_MAX : ((x)+(y)))

我将 'unsigned long' 和 'unsigned long long' 的版本留给读者作为练习。;-)

于 2008-09-24T00:22:57.970 回答
2

以防万一有人想知道一个实现而不使用 2 的补码 32 位整数进行分支。

警告!此代码使用未定义的操作:“右移 -1”,因此利用Intel Pentium SAL 指令的属性将计数操作数屏蔽为 5 位。

int32_t sadd(int32_t a, int32_t b){
    int32_t sum = a+b;
    int32_t overflow = ((a^sum)&(b^sum))>>31;
    return (overflow<<31)^(sum>>overflow);
 }

这是我所知道的最好的实现

于 2015-10-01T08:54:46.433 回答
1

我想,x86 的最佳方法是使用内联汇编程序在添加后检查溢出标志。就像是:

add eax, ebx
jno @@1
or eax, 0FFFFFFFFh
@@1:
.......

它不是很便携,但恕我直言,这是最有效的方式。

于 2008-09-23T14:24:19.330 回答
1

无分支 x86 asm 解决方案的替代方案是(AT&T 语法,eax 和 ebx 中的 a 和 b,导致 eax):

add %eax,%ebx
sbb $0,%ebx
于 2015-01-21T18:28:59.970 回答
1
int saturating_add(int x, int y)
{
    int w = sizeof(int) << 3;
    int msb = 1 << (w-1);

    int s = x + y;
    int sign_x = msb & x;
    int sign_y = msb & y;
    int sign_s = msb & s;

    int nflow = sign_x && sign_y && !sign_s;
    int pflow = !sign_x && !sign_y && sign_s;

    int nmask = (~!nflow + 1);
    int pmask = (~!pflow + 1);

    return (nmask & ((pmask & s) | (~pmask & ~msb))) | (~nmask & msb);
}

这个实现不使用控制流、campare 操作符( ==, !=) 和?:操作符。它只使用位运算符和逻辑运算符。

于 2017-09-22T06:49:05.013 回答
0

使用 C++,您可以编写Remo.D解决方案的更灵活变体:

template<typename T>
T sadd(T first, T second)
{
    static_assert(std::is_integral<T>::value, "sadd is not defined for non-integral types");
    return first > std::numeric_limits<T>::max() - second ? std::numeric_limits<T>::max() : first + second;
}

这可以很容易地转换为 C 使用定义的限制limits.h。另请注意,固定宽度整数类型可能在您的系统上不可用。

于 2014-06-17T12:08:27.423 回答
0
//function-like macro to add signed vals, 
//then test for overlow and clamp to max if required
#define SATURATE_ADD(a,b,val)  ( {\
if( (a>=0) && (b>=0) )\
{\
    val = a + b;\
    if (val < 0) {val=0x7fffffff;}\
}\
else if( (a<=0) && (b<=0) )\
{\
    val = a + b;\
    if (val > 0) {val=-1*0x7fffffff;}\
}\
else\
{\
    val = a + b;\
}\
})

我做了一个快速测试,似乎可以工作,但还没有广泛地抨击它!这适用于 SIGNED 32 位。op:网页上使用的编辑器不允许我发布宏,即它不理解非缩进语法等!

于 2016-03-08T20:58:30.380 回答
0

饱和算法不是 C 的标准,但它通常通过编译器内在函数实现,因此最有效的方法不是最干净的。您必须添加#ifdef块以选择正确的方式。MSalters 的答案是 x86 架构中最快的。对于 ARM,16 位版本和32 位版本需要使用(Microsoft Visual Studio)的__qadd16函数(ARM 编译器) 。它们将被自动转换为一条 ARM 指令。_arm_qadd16__qadd

链接:

于 2018-09-19T17:57:03.353 回答
0

我将添加上面尚未提到的解决方案。

Intel x86 中存在ADC指令。它表示为_addcarry_u32()内在函数。对于 ARM,应该有类似的内在特性。

uint32_t这使我们能够为 Intel x86实现非常快速的饱和加法:

在线尝试!

#include <stdint.h>
#include <immintrin.h>

uint32_t add_sat_u32(uint32_t a, uint32_t b) {
    uint32_t r, carry = _addcarry_u32(0, a, b, &r);
    return r | (-carry);
}

Intel x86 MMX 饱和加法指令可用于实现uint16_t变体:

在线尝试!

#include <stdint.h>
#include <immintrin.h>

uint16_t add_sat_u16(uint16_t a, uint16_t b) {
    return _mm_cvtsi64_si32(_mm_adds_pu16(
        _mm_cvtsi32_si64(a),
        _mm_cvtsi32_si64(b)
    ));
}

我没有提到 ARM 解决方案,因为它可以通过其他答案的其他通用解决方案来实现。

于 2021-12-18T12:15:56.193 回答