对无符号字节进行饱和减法/加法

Saturating subtract/add for unsigned bytes

提问人:ovk 提问时间:11/2/2015 最后编辑:Peter Cordesovk 更新时间:11/18/2023 访问量:15524

问:

想象一下,我有两个无符号字节和 .我需要计算为 和 as .但是,我不希望在这些操作期间发生下溢/溢出。例如(伪代码):bxbsubb - xbaddb + 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);

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

C++ C 优化 位操作 饱和-算术

评论

13赞 Bathsheba 11/2/2015
y ^ ((x ^ y) & -(x < y))对于类型,计算不进行分支。根据您目前所拥有的情况,这可能构成最终解决方案的一部分。intmin(x, y)
3赞 Shafik Yaghmour 11/2/2015
也许 Clamped Increment Integer? 是有帮助的。
8赞 fuz 11/3/2015
这是 C 还是 C++ 问题?请选择一个。
9赞 Shafik Yaghmour 11/3/2015
@AlanCampbell它被称为饱和算术
8赞 porglezomp 11/3/2015
你需要它是便携式的吗?因为如果你正在研究一个特定的架构,那么可能会有一个很好的单一指令。我知道 ARM 对字节有饱和向量加法和减法。在 X86 上,内部函数将在单个指令中执行 16 个字节的饱和添加。_mm_adds_epi8

答:

17赞 chux - Reinstate Monica 11/2/2015 #1

要对无符号字节进行饱和减法/加法:

对于减法:

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)) fails too

感谢@R_Kapp

感谢@NathanOliver

本练习显示了简单编码的价值:

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

评论

0赞 R_Kapp 11/2/2015
也许 ?sum(a + b) | -(a <= (255 - b))
0赞 user694733 11/3/2015
你可以这样做,假设,但这看起来很复杂,我不知道你是否会从中获得任何东西(除了头痛)。sum = ((a + b) | (!!((a + b) & ~0xFF) * 0xFF)) & 0xFFsizeof(int) > sizeof(unsigned char)
0赞 chux - Reinstate Monica 11/3/2015
@user694733 是的,甚至可能.(a+b+1)*(a <= (255-b)) - 1
0赞 chux - Reinstate Monica 11/3/2015
@NathanOliver 感谢您的监督 - 这很能说明问题的方面是,这很容易,因为限制是.但其他限制会带来复杂性,请关注 user2079303 评论。sub0
1赞 chux - Reinstate Monica 11/3/2015
@user1969104 OP不清楚“更好”(代码空间与速度性能),也不清楚目标平台或编译器。在未发布的较大问题的上下文中,速度评估最有意义。
1赞 user4580220 11/2/2015 #2

这个呢:

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

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

评论

0赞 Bathsheba 11/2/2015
我修复了(明显的?)错别字,但我仍然认为这是不正确的。
0赞 fuz 11/2/2015
这也包括分支。
0赞 11/2/2015
我将删除这个答案,只是在没有优化的情况下进行组装中的一个快速问题:三元运算符和 if/else 语句有什么区别?
0赞 fuz 11/2/2015
@GRC 没有区别。
0赞 edmz 11/3/2015
@GRC FUZxxl 是对的,但一如既往,请尝试自己。即使您不知道组装(如果您不清楚某些内容,您可以在 SO 上提出问题),只需检查您知道的长度/说明即可。
40赞 user1969104 11/2/2015 #3

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

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 秒。此解决方案也更具可读性。

评论

5赞 fuz 11/3/2015
@user694733 它没有被优化掉,而是根据进位标志优化为条件赋值。
2赞 user1969104 11/3/2015
是的,user694733 是对的。它被优化为条件赋值。
0赞 Cristian F 11/17/2015
这并不适用于所有情况,例如 badd: b = 155 x =201,而不是 badd = 156,并且大于 b。 您需要将结果与两个变量的 min() 或 max() 进行比较,具体取决于操作
0赞 user1969104 11/17/2015
@CristianF 你如何计算 155+201 = 156?我认为它需要是 155+201 = 356%256 = 100。我不认为 min()、max() 在 b、x 值的任意组合中是必需的。
2赞 Robert Ramey 11/3/2015 #4

您还可以使用 Boost Library Incubator 中的安全数字库。它提供了 int、long 等的直接替换......这保证您永远不会遇到未检测到的溢出、下溢等。

评论

7赞 Shafik Yaghmour 11/3/2015
提供如何使用该库的示例将使这成为一个更好的答案。此外,它们是否提供无胸罩保证?
0赞 Robert Ramey 11/4/2015
该库包含大量文档和示例。但归根结底,它就像包含适当的标头并用 safe<int> 替换 int 一样简单。
0赞 Robert Ramey 11/4/2015
无分支?我猜你这个人没有分支。该库仅在必要时才使用模板元编程来包含运行时检查。例如,unsigned char times unsigned char 将导致 unsigned int。这永远不会溢出,因此根本不需要进行检查。另一方面,unsigned times unsigned 可能会溢出,因此必须在运行时进行检查。
90赞 Shafik Yaghmour 11/3/2015 #5

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

评论

2赞 Shafik Yaghmour 11/3/2015
@user1969104可能是这种情况,但正如文章中的评论所表明的那样,这可以通过在应用一元减号之前转换为无符号来解决。在实践中,除了二的互补之外,您不太可能需要处理其他任何事情
2赞 Yakk - Adam Nevraumont 11/4/2015
这可能是一个很好的 C 答案,但不是一个很好的 C++ 答案。
6赞 JPhi1618 11/4/2015
@Yakk 是什么让这是一个“糟糕”的C++答案?这些是基本的数学运算,我不明白它如何被解释为只有 C 或糟糕的 C++。
4赞 Yakk - Adam Nevraumont 11/4/2015
@JPhi1618 更好的C++答案可能是使用饱和的超载运算符?正确使用命名空间。主要是糖。template<class T>struct sat{T t;};
6赞 JPhi1618 11/4/2015
@Yakk,啊,好吧。我只是把这看作是OP可以根据需要进行调整的最小例子。我不希望看到如此完整的实现。感谢您的澄清。
2赞 user1196549 11/3/2015 #6

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

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

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

评论

1赞 Adrien Hamelin 11/7/2015
这实际上是最好的解决方案之一。之前进行减法或加法的所有其他操作实际上都在 C++ 中创建未定义的行为,导致编译器能够做任何它想做的事情。在实践中,你大多可以预测会发生什么,但仍然如此。
1赞 DanielHsH 11/3/2015 #7

如果您要经常调用这些方法,那么最快的方法不是位操作,而可能是查找表。为每个操作定义一个长度为 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 位计算的运行速度会更快一些。轮到你了

评论

3赞 edmz 11/3/2015
实际上,它可能不会:首先,当然,加载表格很慢。位操作需要 1 个周期,从内存加载大约需要 80 ns;即使从 L1 缓存来看,我们也在 20 ns 的范围内,这在 3GHz CPU 上几乎是 7 个周期。
0赞 DanielHsH 11/3/2015
你不完全正确。LUT 方法需要一些 sycles,但位操作也不是一个周期。有一些顺序操作。例如,仅计算 MAX() 需要 2 次减法、逻辑运算和一次右移。别忘了整数晋升/降级
1赞 edmz 11/3/2015
我的意思是说,单位运算需要 1 个周期,自然而然地假设寄存器操作数。使用 Shafik 展示的代码,clang 输出 4 条基本指令。此外,是无分支的。(x > y)
0赞 DanielHsH 11/3/2015
首先,(x > y) 可能会使用分支。你不知道你在哪个架构上运行。我倾向于同意它在英特尔架构上可能是无分支的。大多数智能手机都不是英特尔。这也是您无法知道将有多少组装指令的原因。在您的 PC 上试用我的解决方案。我很想听听结果。
1赞 gnasher729 11/3/2015
L1 缓存比 20 ns 快得多,大约为 4 个处理器周期。并且可能会使用一个未使用的执行单元,并且无论如何都会完全流水线化。测量它。20ns 是 3 GHz CPU 中的 60 个周期。
3赞 supercat 11/3/2015 #8

另外:

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

无需比较运算符或乘法。

2赞 gnasher729 11/3/2015 #9

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

如果要使用 200 亿字节执行此操作,请检查处理器上可用的矢量指令以及是否可以使用它们。您可能会发现您的处理器可以通过一条指令执行其中的 32 项操作。

14赞 erebos 11/3/2015 #10

如果您使用的是足够新的 gcc 或 clang(可能还有其他一些版本),则可以使用内置版本来检测溢出。

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

评论

1赞 Cephalopod 11/3/2015
这是最好的答案。使用编译器内置函数而不是 bit magic 不仅更快,而且更清晰,使代码更易于维护。
0赞 ovk 11/3/2015
谢谢你,@erebos。我一定会在可用的平台上尝试一下。
4赞 Shafik Yaghmour 11/4/2015
我无法让 gcc 用这个生成无胸代码,这有点令人失望。这里特别不幸的是,clang对它们使用了不同的名称
1赞 Ela782 11/4/2015
@Cephalopod 而且它是完全非跨平台的,很可能甚至不能在其他编译器上运行。对于21世纪来说,这不是一个好的解决方案。
1赞 Cephalopod 11/5/2015
@Ela782 情况恰恰相反:内置不是 20 世纪的好解决方案。欢迎来到未来!
2赞 MichaelMitchell 11/5/2015 #11

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

对于减法:

我们可以使用 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;
}

我不喜欢这个减法,但我认为它很漂亮。

如果 add 溢出,则 .not-ing 产生 0,所以当有溢出时。由于会将无符号整数值设置为最大值,因此如果没有进位,该函数将返回加法结果,如果有进位,则返回所选整数值的最大值。carry_flag = 1carry_flag!carry_flag * result = 00 - 1

评论

2赞 Toby Speight 3/5/2019
您可能想提一下,这个答案是针对特定指令集架构(x86?)的,并且需要针对每个目标架构(SPARC、MIPS、ARM等)重新实现