想象一下,我有两个无符号字节b
和x
。我需要将bsub
计算为b - x
,将badd
计算为b + x
。但是,我不希望在这些操作期间发生下溢/溢出。例如(伪代码):
b = 3; x = 5;
bsub = b - x; // bsub must be 0, not 254
和
b = 250; x = 10;
badd = b + x; // badd must be 255, not 4
显而易见的方法包括分支:
bsub = b - min(b, x);
badd = b + min(255 - b, x);
我只是想知道是否有更好的方法可以做到这一点,即通过一些黑客的操作?
答案 0 :(得分:83)
文章Branchfree Saturating Arithmetic提供了相应的策略:
他们的补充解决方案如下:
u32b sat_addu32b(u32b x, u32b y)
{
u32b res = x + y;
res |= -(res < x);
return res;
}
为uint8_t修改:
uint8_t sat_addu8b(uint8_t x, uint8_t y)
{
uint8_t res = x + y;
res |= -(res < x);
return res;
}
及其减法解决方案是:
u32b sat_subu32b(u32b x, u32b y)
{
u32b res = x - y;
res &= -(res <= x);
return res;
}
为uint8_t修改:
uint8_t sat_subu8b(uint8_t x, uint8_t y)
{
uint8_t res = x - y;
res &= -(res <= x);
return res;
}
答案 1 :(得分:39)
一种简单的方法是检测溢出并相应地重置值,如下所示
bsub = b - x;
if (bsub > b)
{
bsub = 0;
}
badd = b + x;
if (badd < b)
{
badd = 255;
}
GCC可以在使用-O2编译时将溢出检查优化为条件赋值。
我测量了与其他解决方案相比的优化程度。在我的电脑上运行了100万次以上的操作,这个解决方案和@ShafikYaghmour的平均时间为4.2秒,而@chux的解决方案平均为4.8秒。此解决方案也更具可读性。
答案 2 :(得分:16)
减法:
diff = (a - b)*(a >= b);
增加:
sum = (a + b) | -(a > (255 - b))
演进
// sum = (a + b)*(a <= (255-b)); this fails
// sum = (a + b) | -(a <= (255 - b)) falis too
感谢@R_Kapp
本练习显示了简单编码的价值。
sum = b + min(255 - b, a);
答案 3 :(得分:12)
如果您使用的是最新版本的gcc或clang(也许还有其他版本),您可以使用built-ins来检测溢出。
if (__builtin_add_overflow(a,b,&c))
{
c = UINT_MAX;
}
答案 4 :(得分:3)
如果您愿意使用汇编或内在函数,我认为我有一个最佳解决方案。
对于减法:
我们可以使用sbb
instruction
在MSVC中,我们可以使用内在函数_subborrow_u64(也可用于其他位大小)。
以下是它的使用方法:
// *c = a - (b + borrow)
// borrow_flag is set to 1 if (a < (b + borrow))
borrow_flag = _subborrow_u64(borrow_flag, a, b, c);
以下是我们如何将其应用于您的情况
uint64_t sub_no_underflow(uint64_t a, uint64_t b){
uint64_t result;
borrow_flag = _subborrow_u64(0, a, b, &result);
return result * !borrow_flag;
}
添加:
我们可以使用adcx
instruction
在MSVC中,我们可以使用内在函数_addcarry_u64(也可用于其他位大小)。
以下是它的使用方法:
// *c = a + b + carry
// carry_flag is set to 1 if there is a carry bit
carry_flag = _addcarry_u64(carry_flag, a, b, c);
以下是我们如何将其应用于您的情况
uint64_t add_no_overflow(uint64_t a, uint64_t b){
uint64_t result;
carry_flag = _addcarry_u64(0, a, b, &result);
return !carry_flag * result - carry_flag;
}
我不喜欢这个和减法一个,但我认为这很漂亮。
如果添加溢出,carry_flag = 1
。 Not-ing carry_flag
产生0,所以当有溢出时!carry_flag * result = 0
。由于0 - 1
将无符号整数值设置为最大值,如果没有进位,函数将返回相加的结果,如果有进位则返回所选整数值的最大值。
答案 5 :(得分:2)
怎么样:
bsum = a + b;
bsum = (bsum < a || bsum < b) ? 255 : bsum;
bsub = a - b;
bsub = (bsub > a || bsub > b) ? 0 : bsub;
答案 6 :(得分:2)
您也可以在Boost Library Incubator使用安全数字库。它为int,long等提供了直接替换...这保证了你永远不会得到未检测到的溢出,下溢等。
答案 7 :(得分:2)
所有都可以用无符号字节算法
完成// Addition without overflow
return (b > 255 - a) ? 255 : a + b
// Subtraction without underflow
return (b > a) ? 0 : a - b;
答案 8 :(得分:2)
如果你将这些方法调用很多,最快的方法不是位操作,而是一个查找表。为每个操作定义长度为511的数组。 减(减法)的例子
static unsigned char maxTable[511];
memset(maxTable, 0, 255); // If smaller, emulates cutoff at zero
maxTable[255]=0; // If equal - return zero
for (int i=0; i<256; i++)
maxTable[255+i] = i; // If greater - return the difference
该数组是静态的,只初始化一次。现在您的减法可以定义为内联方法或使用预编译器:
#define MINUS(A,B) maxTable[A-B+255];
它是如何工作的?那么你想预先计算无符号字符的所有可能的减法。结果从-255到+255不等,共有511个不同的结果。我们定义了一个包含所有可能结果的数组,但因为在C中我们无法从负数指数访问它,我们使用+255(在[A-B + 255]中)。您可以通过定义指向数组中心的指针来删除此操作。
const unsigned char *result = maxTable+255;
#define MINUS(A,B) result[A-B];
使用它像:
bsub = MINUS(13,15); // i.e 13-15 with zero cutoff as requested
请注意,执行速度非常快。只有一个减法和一个指针参数来获得结果。没有分支。静态数组非常短,因此它们将完全加载到CPU的缓存中以进一步加速计算
同样适用于添加,但有一个不同的表(前256个元素将是索引,最后255个元素将等于255以模拟超过255的截止。
如果您坚持使用位操作,则使用(a&gt; b)的答案是错误的。这仍然可以实现为分支。使用符号位技术
// (num1>num2) ? 1 : 0
#define is_int_biggerNotEqual( num1,num2) ((((__int32)((num2)-(num1)))&0x80000000)>>31)
现在你可以用它来计算减法和加法。
如果要模拟函数max(),min()而不使用分支:
inline __int32 MIN_INT(__int32 x, __int32 y){ __int32 d=x-y; return y+(d&(d>>31)); }
inline __int32 MAX_INT(__int32 x, __int32 y){ __int32 d=x-y; return x-(d&(d>>31)); }
我上面的例子使用32位整数。您可以将其更改为64,但我相信32位计算运行速度更快一些。由您决定
答案 9 :(得分:2)
补充:
unsigned temp = a+b; // temp>>8 will be 1 if overflow else 0
unsigned char c = temp | -(temp >> 8);
减法:
unsigned temp = a-b; // temp>>8 will be 0xFF if neg-overflow else 0
unsigned char c = temp & ~(temp >> 8);
不需要比较运算符或乘法值。
答案 10 :(得分:2)
如果要使用两个字节执行此操作,请尽可能使用最简单的代码。
如果要以200亿字节执行此操作,请检查处理器上可用的矢量指令以及是否可以使用它们。您可能会发现您的处理器可以通过一条指令执行其中32项操作。