在 C 中编写饱和加法的最佳(最干净、最有效)的方法是什么?
如果总和溢出,函数或宏应添加两个无符号输入(需要 16 位和 32 位版本)并返回全位为一(0xFFFF 或 0xFFFFFFFF)。
目标是使用 gcc (4.1.2) 和 Visual Studio 的 x86 和 ARM(仅用于模拟,因此可以使用后备实现)。
在 C 中编写饱和加法的最佳(最干净、最有效)的方法是什么?
如果总和溢出,函数或宏应添加两个无符号输入(需要 16 位和 32 位版本)并返回全位为一(0xFFFF 或 0xFFFFFFFF)。
目标是使用 gcc (4.1.2) 和 Visual Studio 的 x86 和 ARM(仅用于模拟,因此可以使用后备实现)。
您可能希望在此处使用可移植的 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-asm
adds32 的输出:
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 @
在普通 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;
}
几乎是宏观化的,直接传达意思。
在没有条件跳转的 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
}
在 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 位常量。
编辑:并行版本很可能比直接方法慢,但如果您必须一次饱和多个值,它们会更快。
如果您关心性能,那么您真的想在 SIMD 中做这种事情,其中 x86 具有本机饱和算法。
由于标量数学中缺乏饱和算术,因此可能会遇到这样的情况:在 4 变量宽 SIMD 上完成的运算比等效的 C 快 4 倍以上(相应地,对于 8 变量宽 SIMD 也是如此):
sub8x8_dct8_c: 1332 clocks
sub8x8_dct8_mmx: 182 clocks
sub8x8_dct8_sse2: 127 clocks
零分支解决方案:
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 语法,在a
and中,结果为):b
eax
ebx
eax
add %eax,%ebx
sbb %eax,%eax
or %ebx,%eax
8 位和 16 位版本应该很明显。签名版本可能需要更多的工作。
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 */
编辑:既然您已经发布了您的版本,我不确定我的版本是否更清洁/更好/更高效/更研究。
我们正在使用的当前实现是:
#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)))
我不确定这是否比 Skizz 的解决方案(总是配置文件)更快,但这里有一个替代的无分支组装解决方案。请注意,这需要条件移动(CMOV)指令,我不确定您的目标是否可用。
uint32_t sadd32(uint32_t a, uint32_t b)
{
__asm
{
movl eax, a
addl eax, b
movl edx, 0xffffffff
cmovc eax, edx
}
}
最好的性能通常涉及内联汇编(正如一些人已经说过的)。
但是对于可移植的 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' 的版本留给读者作为练习。;-)
以防万一有人想知道一个实现而不使用 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);
}
这是我所知道的最好的实现
我想,x86 的最佳方法是使用内联汇编程序在添加后检查溢出标志。就像是:
add eax, ebx
jno @@1
or eax, 0FFFFFFFFh
@@1:
.......
它不是很便携,但恕我直言,这是最有效的方式。
无分支 x86 asm 解决方案的替代方案是(AT&T 语法,eax 和 ebx 中的 a 和 b,导致 eax):
add %eax,%ebx
sbb $0,%ebx
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 操作符( ==
, !=
) 和?:
操作符。它只使用位运算符和逻辑运算符。
使用 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
。另请注意,固定宽度整数类型可能在您的系统上不可用。
//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:网页上使用的编辑器不允许我发布宏,即它不理解非缩进语法等!
饱和算法不是 C 的标准,但它通常通过编译器内在函数实现,因此最有效的方法不是最干净的。您必须添加#ifdef
块以选择正确的方式。MSalters 的答案是 x86 架构中最快的。对于 ARM,16 位版本和32 位版本需要使用(Microsoft Visual Studio)的__qadd16
函数(ARM 编译器) 。它们将被自动转换为一条 ARM 指令。_arm_qadd16
__qadd
链接:
我将添加上面尚未提到的解决方案。
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 解决方案,因为它可以通过其他答案的其他通用解决方案来实现。