在C中编写饱和加法的最佳(最干净,最有效)方法是什么?
函数或宏应该添加两个无符号输入(需要16位和32位版本),如果总和溢出则返回所有位 - 1(0xFFFF或0xFFFFFFFF)。
目标是使用gcc(4.1.2)和Visual Studio的x86和ARM(仅用于模拟,因此可以使用后备实现)。
答案 0 :(得分: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;}
几乎是宏观的,直接传达了意义。
答案 1 :(得分: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
}
答案 2 :(得分:18)
您可能需要这里的可移植C
代码,您的编译器将转换为正确的ARM程序集。 ARM有条件移动,这些可以以溢出为条件。然后算法变为add,并且如果检测到溢出,则有条件地将目标设置为unsigned(-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;
}
请注意,这与其他算法的不同之处在于它可以纠正溢出,而不是依赖于另一个计算来检测溢出。
x86-64 clang 3.7 -O3 output for adds32:明显优于其他任何答案:
add edi, esi
mov eax, -1
cmovae eax, edi
ret
ARMv7: gcc 4.8 -O3 -mcpu=cortex-a15 -fverbose-asm
output for adds32:
adds r0, r0, r1 @ c, a, b
it cs
movcs r0, #-1 @ conditional-move
bx lr
16bit:仍然不使用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 @
答案 3 :(得分:11)
在ARM中,您可能已经内置了饱和算术。 ARMv5 DSP扩展可以使寄存器饱和到任何位长。同样在ARM饱和度上通常很便宜,因为你可以有条件地执行大多数指令。
ARMv6甚至还有饱和的加法,减法和所有其他32位和打包数字的东西。
在x86上,您可以通过MMX或SSE获得饱和算术。
这一切都需要汇编程序,所以这不是你要求的。
也有C-tricks做饱和算术。这个小代码对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位常量。
编辑:并行版本很可能比直接版本慢,但如果您一次必须使多个值饱和,它们会更快。
答案 4 :(得分:10)
如果你关心性能,你真的想在SIMD中做这类事情,其中x86具有原生饱和算法。
由于标量数学中缺乏饱和算法,人们可以得到在4变量宽SIMD上完成的操作更多的情况比等效C快4倍的情况(并且相应地为真)使用8变量宽SIMD):
sub8x8_dct8_c: 1332 clocks
sub8x8_dct8_mmx: 182 clocks
sub8x8_dct8_sse2: 127 clocks
答案 5 :(得分:9)
零分支解决方案:
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(a
和b
中的AT&amp; T语法,eax
和ebx
中,导致eax
):
add %eax,%ebx
sbb %eax,%eax
or %ebx,%eax
8位和16位版本应该是显而易见的。签名版本可能需要更多工作。
答案 6 :(得分: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 */
编辑:现在您已经发布了自己的版本,我不确定我的版本是否更清晰/更好/效率更高/更合适。
答案 7 :(得分: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
}
}
答案 8 :(得分:2)
我们目前使用的实现是:
#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)))
答案 9 :(得分:2)
以防万一有人想知道一个没有使用2位补码32位整数进行分支的实现。
警告!此代码使用未定义的操作:&#34;向右移动-1和#34;因此利用Intel Pentium SAL instruction的属性将计数操作数屏蔽为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);
}
这是我所知道的最佳实现
答案 10 :(得分: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”的版本作为练习留给读者。 ; - )
答案 11 :(得分:2)
我想,x86的最佳方法是使用内联汇编程序在添加后检查溢出标志。类似的东西:
add eax, ebx
jno @@1
or eax, 0FFFFFFFFh
@@1:
.......
它不是很便携,但恕我直言是最有效的方式。
答案 12 :(得分: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;
}
使用limits.h
中定义的限制,可以轻松将其转换为C语言。另请注意,您的系统可能无法使用Fixed width integer types。
答案 13 :(得分:0)
分支免费x86 asm解决方案的替代方案是(AT&amp; T语法,eax和ebx中的a和b,导致eax):
add %eax,%ebx
sbb $0,%ebx
答案 14 :(得分: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:网页上使用的编辑器不允许我发布一个宏,即它不理解非缩进语法等!
答案 15 :(得分:0)
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运算符(==
,!=
)和?:
运算符。它只使用按位运算符和逻辑运算符。
答案 16 :(得分:0)
饱和度算术不是C语言的标准,但通常是通过编译器内部函数实现的,因此最有效的方法并不是最干净的。您必须添加#ifdef块以选择正确的方式。对于x86架构,MSalters的答案是最快的。对于ARM,您需要将_arm_qadd16(Microsoft Visual Studio)的__qadd16函数(ARM编译器)用于16位版本,而对于__qadd使用32位版本。 它将自动翻译为一条ARM指令。
链接:
__ qadd16 http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0491c/CJAICDDF.html
_arm_qadd16 https://msdn.microsoft.com/en-US/library/hh875058.aspx
__ qadd http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0472m/chr1359125002575.html