无符号字节饱和 subtract/add
Saturating subtract/add for unsigned bytes
假设我有两个无符号字节 b
和 x
。我需要将 bsub
计算为 b - x
,将 badd
计算为 b + x
。但是,我不希望 underflow/overflow 在这些操作期间发生。例如(伪代码):
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);
我只是想知道是否有更好的方法来做到这一点,即通过一些 hacky 位操作?
减法:
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
感谢
感谢
这个练习展示了简单编码的价值。
sum = b + min(255 - b, a);
这个怎么样:
bsum = a + b;
bsum = (bsum < a || bsum < b) ? 255 : bsum;
bsub = a - b;
bsub = (bsub > a || bsub > b) ? 0 : bsub;
一个简单的方法是检测溢出并相应地重置值,如下所示
bsub = b - x;
if (bsub > b)
{
bsub = 0;
}
badd = b + x;
if (badd < b)
{
badd = 255;
}
GCC 可以在使用 -O2 编译时将溢出检查优化为条件赋值。
我测量了与其他解决方案相比优化了多少。在我的 PC 上进行了 1000000000 次以上的操作后,这个解决方案和@ShafikYaghmour 的平均耗时 4.2 秒,@chux 的平均耗时 4.8 秒。此解决方案也更具可读性。
您还可以使用 Boost Library Incubator 上的安全数字库。它提供了 int、long 等的直接替换...保证您永远不会遇到未检测到的溢出、下溢等。
文章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;
}
无符号字节运算都可以
// Addition without overflow
return (b > 255 - a) ? 255 : a + b
// Subtraction without underflow
return (b > a) ? 0 : a - b;
如果您经常调用这些方法,最快的方法不是位操作,而是查找 table。为每个操作定义一个长度为 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 的缓存中以进一步加快计算速度
同样适用于加法,但有一点不同 table(前 256 个元素将是索引,最后 255 个元素将等于 255,以模拟超过 255 的截止值。
如果坚持位运算,使用(a>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 位计算 运行 会快一点。由你决定
补充:
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);
不需要比较运算符或乘法。
如果您想用两个字节执行此操作,请使用尽可能简单的代码。
如果您想对 200 亿字节执行此操作,请检查您的处理器上有哪些向量指令可用以及是否可以使用它们。您可能会发现您的处理器可以使用一条指令执行其中的 32 项操作。
如果您使用的是最新版本的 gcc 或 clang(也许还有其他一些),您可以使用 built-ins 来检测溢出。
if (__builtin_add_overflow(a,b,&c))
{
c = UINT_MAX;
}
如果你愿意使用汇编或内在函数,我想我有一个最优解。
减法:
我们可以使用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
会将无符号整数值设置为其最大值,如果没有进位,函数将 return 加法的结果,如果有,则 return 所选整数值的最大值是进位。
假设我有两个无符号字节 b
和 x
。我需要将 bsub
计算为 b - x
,将 badd
计算为 b + x
。但是,我不希望 underflow/overflow 在这些操作期间发生。例如(伪代码):
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);
我只是想知道是否有更好的方法来做到这一点,即通过一些 hacky 位操作?
减法:
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
感谢
感谢
这个练习展示了简单编码的价值。
sum = b + min(255 - b, a);
这个怎么样:
bsum = a + b;
bsum = (bsum < a || bsum < b) ? 255 : bsum;
bsub = a - b;
bsub = (bsub > a || bsub > b) ? 0 : bsub;
一个简单的方法是检测溢出并相应地重置值,如下所示
bsub = b - x;
if (bsub > b)
{
bsub = 0;
}
badd = b + x;
if (badd < b)
{
badd = 255;
}
GCC 可以在使用 -O2 编译时将溢出检查优化为条件赋值。
我测量了与其他解决方案相比优化了多少。在我的 PC 上进行了 1000000000 次以上的操作后,这个解决方案和@ShafikYaghmour 的平均耗时 4.2 秒,@chux 的平均耗时 4.8 秒。此解决方案也更具可读性。
您还可以使用 Boost Library Incubator 上的安全数字库。它提供了 int、long 等的直接替换...保证您永远不会遇到未检测到的溢出、下溢等。
文章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;
}
无符号字节运算都可以
// Addition without overflow
return (b > 255 - a) ? 255 : a + b
// Subtraction without underflow
return (b > a) ? 0 : a - b;
如果您经常调用这些方法,最快的方法不是位操作,而是查找 table。为每个操作定义一个长度为 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 的缓存中以进一步加快计算速度
同样适用于加法,但有一点不同 table(前 256 个元素将是索引,最后 255 个元素将等于 255,以模拟超过 255 的截止值。
如果坚持位运算,使用(a>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 位计算 运行 会快一点。由你决定
补充:
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);
不需要比较运算符或乘法。
如果您想用两个字节执行此操作,请使用尽可能简单的代码。
如果您想对 200 亿字节执行此操作,请检查您的处理器上有哪些向量指令可用以及是否可以使用它们。您可能会发现您的处理器可以使用一条指令执行其中的 32 项操作。
如果您使用的是最新版本的 gcc 或 clang(也许还有其他一些),您可以使用 built-ins 来检测溢出。
if (__builtin_add_overflow(a,b,&c))
{
c = UINT_MAX;
}
如果你愿意使用汇编或内在函数,我想我有一个最优解。
减法:
我们可以使用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
会将无符号整数值设置为其最大值,如果没有进位,函数将 return 加法的结果,如果有,则 return 所选整数值的最大值是进位。