饱和减/加无符号字节


83

假设我有两个无符号字节bx。我需要计算bsubasb - xbaddas 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);

我只是想知道是否有更好的方法可以做到这一点,即通过一些恶意的操作?


13
y ^ ((x ^ y) & -(x < y))int各类评估min(x, y)没有分支。根据目前的情况,这可能构成最终解决方案的一部分。
Bathsheba

3
也许固定增量整数?是有帮助的。
Shafik Yaghmour,2015年

8
这是C还是C ++问题?请选择一个。
2015年

9
@AlanCampbell称为饱和算术
沙菲克·雅格慕

7
您需要便携式吗?因为如果您正在查看特定的体系结构,那么可能会有一条不错的单一说明。我知道ARM对字节具有饱和的向量加法和减法。在X86上,_mm_adds_epi8内部函数将在一条指令中完成16个字节的饱和加法。
porglezomp 2015年

Answers:


86

文章“无分支饱和算法”为此提供了策略:

他们的附加解决方案如下:

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;
}

2
@ user1969104可能是这种情况,但正如文章中的注释所示,可以通过在应用一元减号之前强制转换为unsigned来解决。实际上,除了二进制补码之外,您几乎不需要处理其他任何事情
Shafik Yaghmour,2015年

2
这可能是一个很好的C答案,但不是一个很好的C ++答案。
Yakk-Adam Nevraumont

4
@Yakk是什么使它成为“错误的” C ++答案?这些是基本的数学运算,我看不到如何将其解释为仅C或不良的C ++。
JPhi1618

4
@ JPhi1618一个更好的C ++答案可能是template<class T>struct sat{T t;};饱和的重载运算符?正确使用名称空间。主要是糖。
Yakk-亚当·内夫罗蒙特2015年

6
@Yakk,嗯,好的。我只是将其视为OP可以根据需要进行调整的最小示例。我不希望看到如此完整的实现。感谢您的澄清。
JPhi1618

40

一种简单的方法是检测溢出并相应地重置值,如下所示

bsub = b - x;
if (bsub > b)
{
    bsub = 0;
}

badd = b + x;
if (badd < b)
{
    badd = 255;
}

使用-O2进行编译时,GCC可以将溢出检查优化为条件赋值。

与其他解决方案相比,我测量了多少优化。通过我的PC上的1000000000次操作,该解决方案和@ShafikYaghmour的解决方案平均为4.2秒,@ chux的解决方案平均为4.8秒。该解决方案也更具可读性。


5
@ user694733尚未优化,它已根据进位标志优化为条件赋值。
2015年

2
是,user694733是正确的。它已优化为条件分配。
user1969104

这不适用于所有情况,例如badd:b = 155 x = 201,而不是badd = 156,且大于b。您需要根据操作将结果与两个变量的min()或max()进行比较
Cristian F

@CristianF您如何计算155 + 201 = 156?我认为它必须为155 + 201 = 356%256 =100。我不认为b,x值的任何组合都需要min(),max()。
user1969104

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

感谢@NathanOliver

此练习显示了简单编码的价值。

sum = b + min(255 - b, a);

对于sum可能(a + b) | -(a <= (255 - b))
R_Kapp 2015年

可以sum = ((a + b) | (!!((a + b) & ~0xFF) * 0xFF)) & 0xFF假设进行sizeof(int) > sizeof(unsigned char)此操作,但这看起来太复杂了,我不知道您是否会从中受益(除了头痛)。
2015年

@ user694733是的,甚至可能(a+b+1)*(a <= (255-b)) - 1
chux-恢复莫妮卡2015年

@NathanOliver感谢您的监督-这样做的好处是,sub限制很容易0。但其他限制带来了麻烦,并遵循user2079303的评论。
chux-恢复莫妮卡

1
@ user1969104 OP在“更好”(代码空间与速度性能),目标平台或编译器上不清楚。在未发布的较大问题的背景下,速度评估最有意义。
chux-恢复莫妮卡

13

如果您使用的是最新版本的gcc或clang(也许还有其他版本),则可以使用内置函数来检测溢出。

if (__builtin_add_overflow(a,b,&c))
{
  c = UINT_MAX;
}

这是最好的答案。使用编译器内置而不是魔术,不仅更快,而且更清晰,并使代码更可维护。
头足类动物2015年

谢谢@erebos。我一定会在可用的平台上尝试。
ovk 2015年

3
我无法让gcc与此代码生成无缝代码,这有点令人失望。这里特别不幸的是,clang使用了不同的名称
Shafik Yaghmour,2015年

1
@Cephalopod而且它是完全非跨平台的,很可能甚至无法在另一个编译器上运行。对于21世纪而言,这不是一个好的解决方案。
Ela782 2015年

1
@ Ela782恰恰相反:内置不是20世纪的好解决方案。欢迎来到未来!
头足类动物2015年

3

补充:

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);

不需要比较运算符或乘法。


3

如果您愿意使用汇编或内部函数,我想我有一个最佳解决方案。

对于减法:

我们可以使用sbb指令

在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指令

在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。注意会carry_flag产生0,所以!carry_flag * result = 0当有溢出时。并且由于0 - 1会将无符号整数值设置为其最大值,因此如果没有进位,该函数将返回加法结果,如果存在进位,则该函数将返回所选积分值的最大值。


1
您可能要提到,这个答案是针对特定的指令集体系结构(x86?)的,并且需要针对每个目标体系结构(SPARC,MIPS,ARM等)重新实现
Toby Speight,

2

那这个呢:

bsum = a + b;
bsum = (bsum < a || bsum < b) ? 255 : bsum;

bsub = a - b;
bsub = (bsub > a || bsub > b) ? 0 : bsub;

我修正了(明显的?)错字,但我仍然不认为这是正确的。
Bathsheba

这也包括分支。
fuz 2015年

我将在不进行优化的情况下在汇编中删除一个快速问题,即三元运算符和if / else语句之间的区别是什么?

@GRC没有区别。
fuz 2015年

@GRC FUZxxl是正确的,但像往常一样,请尝试一下。即使您不了解汇编(如果您不清楚某些地方,也可以在SO上提问),只需检查一下您知道的长度/说明即可。
edmz 2015年

2

所有这些都可以用无符号字节算术完成

// Addition without overflow
return (b > 255 - a) ? 255 : a + b

// Subtraction without underflow
return (b > a) ? 0 : a - b;

1
这实际上是最好的解决方案之一。所有其他之前进行减法或加法运算的人实际上都在C ++中创建了未定义的行为,从而使编译器能够执行其所需的任何操作。在实践中,您基本上可以预测会发生什么,但是仍然可以。
Adrien Hamelin 2015年

2

如果要使用两个字节来执行此操作,请使用最简单的代码。

如果要用200亿字节来完成此操作,请检查处理器上可用的矢量指令以及是否可以使用它们。您可能会发现您的处理器可以用一条指令完成这些操作中的32个。


2

您也可以使用Boost Library Incubator上的安全数字库。它提供int,long等的直接替换项,以确保您永远不会遇到未检测到的上溢,下溢等情况。


7
提供有关如何使用该库的示例将使这个问题更好。此外,它们是否提供无懈可击的保证?
Shafik Yaghmour,2015年

该库包含大量文档和示例。但是,总而言之,就像包含适当的标头并用safe <int>代替int一样容易。
罗伯特·拉米

无分支?我猜你这个男人无分支。该库仅在必要时使用模板元编程来包括运行时检查。例如,无符号字符时间乘无符号字符将导致无符号整数。这永远不会溢出,因此根本不需要进行检查。另一方面,无符号时间无符号会溢出,因此必须在运行时进行检查。
罗伯特·拉米

1

如果您将大量调用这些方法,最快的方法不是位操作,而是查找表。为每个操作定义一个长度为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> 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位计算的运行速度更快。由你决定


2
实际上,它可能不会:首先,当然,加载表很慢。位操作需要1个周期,从内存中加载大约需要80 ns;即使从L1缓存,我们也处于20 ns的范围内,在3GHz CPU上这几乎是7个周期。
edmz 2015年

您并不完全正确。LUT方法将花费几个周期,但位操作也不是一个周期。有一些顺序的动作。例如,仅计算MAX()需要2次减法,逻辑运算和右移。并且不要忘记整数升迁/降级
DanielHsH 2015年

1
我的意思是说单个位操作需要1个周期,自然是假设寄存器操作数。使用Shafik显示的代码,clang输出4条基本指令。另外(x > y),是无分支的。
edmz 2015年

首先,(x> y)可能会使用分支。您不知道您在哪种架构上运行。我倾向于同意,它在英特尔架构上可能是无分支的。大多数智能手机不是英特尔。这也是您不知道会有多少条汇编指令的原因。在您的PC上尝试我的解决方案。我有兴趣听到结果。
DanielHsH 2015年

1
L1缓存比20 ns快得多,大约是4个处理器周期。并且可能会使用原本未使用的执行单元,并且无论如何都将完全流水线化。测量它。在3 GHz CPU中20ns是60个周期。
gnasher729
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.