是否有一个C代码片段,可以在不使用编译器内置函数的情况下有效地计算溢出安全加法?


11

这是一个C函数,将C添加int到另一个函数,如果发生溢出将失败:

int safe_add(int *value, int delta) {
        if (*value >= 0) {
                if (delta > INT_MAX - *value) {
                        return -1;
                }
        } else {
                if (delta < INT_MIN - *value) {
                        return -1;
                }
        }

        *value += delta;
        return 0;
}

不幸的是,GCC或Clang 无法对其进行优化

safe_add(int*, int):
        movl    (%rdi), %eax
        testl   %eax, %eax
        js      .L2
        movl    $2147483647, %edx
        subl    %eax, %edx
        cmpl    %esi, %edx
        jl      .L6
.L4:
        addl    %esi, %eax
        movl    %eax, (%rdi)
        xorl    %eax, %eax
        ret
.L2:
        movl    $-2147483648, %edx
        subl    %eax, %edx
        cmpl    %esi, %edx
        jle     .L4
.L6:
        movl    $-1, %eax
        ret

这个版本 __builtin_add_overflow()

int safe_add(int *value, int delta) {
        int result;
        if (__builtin_add_overflow(*value, delta, &result)) {
                return -1;
        } else {
                *value = result;
                return 0;
        }
}

更好地优化

safe_add(int*, int):
        xorl    %eax, %eax
        addl    (%rdi), %esi
        seto    %al
        jo      .L5
        movl    %esi, (%rdi)
        ret
.L5:
        movl    $-1, %eax
        ret

但我很好奇是否有一种方法可以不使用将由GCC或Clang进行模式匹配的内建函数。


1
我看到乘法中有gcc.gnu.org/bugzilla/show_bug.cgi?id=48580。但是加法应该更容易进行模式匹配。我会报告。
塔维安·巴恩斯

Answers:


6

如果您无法访问体系结构的溢出标志,那么我想到的最好的方法就是在中执行操作unsigned。只需考虑一下此处的所有位算术,因为我们只对最高位感兴趣,最高位是当解释为有符号值时的符号位。

(所有模数符号错误,我都没有认真检查过,但是我希望这个想法很清楚)

#include <stdbool.h>

bool overadd(int a[static 1], int b) {
  unsigned A = a[0];
  unsigned B = b;
  // This computation will be done anyhow
  unsigned AB = A + B;
  // See if the sign bits are equal
  unsigned AeB = ~(A^B);
  unsigned AuAB = (A^AB);
  // The function result according to these should be:
  //
  // AeB \ AuAB | false | true
  //------------+-------+------
  // false      | false | false
  // true       | false | true
  //
  // So the expression to compute from the sign bits is (AeB & AuAB)

  // This is INT_MAX
  unsigned M = -1U/2;
  bool ret = (AeB & AuAB) > M;

  if (!ret) a[0] += b;
  return ret;
}

如果找到不包含UB的附加版本,例如原子版本,则汇编器甚至没有分支(但带有锁定前缀)

#include <stdbool.h>
#include <stdatomic.h>
bool overadd(_Atomic(int) a[static 1], int b) {
  unsigned A = a[0];
  atomic_fetch_add_explicit(a, b, memory_order_relaxed);
  unsigned B = b;
  // This computation will be done anyhow
  unsigned AB = A + B;
  // See if the sign bits are equal
  unsigned AeB = ~(A^B);
  unsigned AuAB = (A^AB);
  // The function result according to these should be:
  //
  // AeB \ AuAB | false | true
  //------------+-------+------
  // false      | false | false
  // true       | false | true
  //
  // So the expression to compute from the sign bits is (AeB & AuAB)

  // This is INT_MAX
  unsigned M = -1U/2;
  bool ret = (AeB & AuAB) > M;
  return ret;
}

因此,如果我们进行了这样的操作,但更加“放松”了,则可以进一步改善这种情况。

Take3:如果我们使用从未签名结果到已签名结果的特殊“ cast”,则现在是无分支的:

#include <stdbool.h>
#include <stdatomic.h>

bool overadd(int a[static 1], int b) {
  unsigned A = a[0];
  //atomic_fetch_add_explicit(a, b, memory_order_relaxed);
  unsigned B = b;
  // This computation will be done anyhow
  unsigned AB = A + B;
  // See if the sign bits are equal
  unsigned AeB = ~(A^B);
  unsigned AuAB = (A^AB);
  // The function result according to these should be:
  //
  // AeB \ AuAB | false | true
  //------------+-------+------
  // false      | false | false
  // true       | false | true
  //
  // So the expression to compute from the sign bits is (AeB & AuAB)

  // This is INT_MAX
  unsigned M = -1U/2;
  unsigned res = (AeB & AuAB);
  signed N = M-1;
  N = -N - 1;
  a[0] =  ((AB > M) ? -(int)(-AB) : ((AB != M) ? (int)AB : N));
  return res > M;
}

2
不是DV,但是我认为第二个XOR不应该被否定。参见例如尝试测试所有建议的尝试
Bob__

我尝试了类似的方法,但无法正常工作。看起来很有希望,但我希望GCC优化惯用代码。
R .. GitHub停止帮助ICE

1
@PSkocik,不,这不取决于符号表示,计算完全按照进行unsigned。但这取决于一个事实,即无符号类型不仅掩盖了符号位。(现在在C2x中都保证了这两种功能,也就是说,保留我们可以找到的所有拱门)。然后,unsigned如果结果大于,则无法将其回退INT_MAX,这将是实现定义的,并且可能会发出信号。
Jens Gustedt

1
@PSkocik,不幸的是,这似乎对委员会来说是革命性的。但是这是一个“ Take3”,它在我的机器上没有分支出现。
Jens Gustedt

1
很抱歉打扰你,但我认为你应该改变Take3弄成这样,以获得正确的结果。不过,它似乎很有希望
Bob__

2

有符号操作的情况比无符号操作的情况要糟糕得多,我只看到一种模式的带符号加法,仅适用于clang且只有在有更广泛的类型可用时:

int safe_add(int *value, int delta)
{
    long long result = (long long)*value + delta;

    if (result > INT_MAX || result < INT_MIN) {
        return -1;
    } else {
        *value = result;
        return 0;
    }
}

clang提供与__builtin_add_overflow 完全相同的asm

safe_add:                               # @safe_add
        addl    (%rdi), %esi
        movl    $-1, %eax
        jo      .LBB1_2
        movl    %esi, (%rdi)
        xorl    %eax, %eax
.LBB1_2:
        retq

否则,我能想到的最简单的解决方案是(使用Jens使用的接口):

_Bool overadd(int a[static 1], int b)
{
    // compute the unsigned sum
    unsigned u = (unsigned)a[0] + b;

    // convert it to signed
    int sum = u <= -1u / 2 ? (int)u : -1 - (int)(-1 - u);

    // see if it overflowed or not
    _Bool overflowed = (b > 0) != (sum > a[0]);

    // return the results
    a[0] = sum;
    return overflowed;
}

gcc和clang生成非常相似的asm。gcc给出了以下内容:

overadd:
        movl    (%rdi), %ecx
        testl   %esi, %esi
        setg    %al
        leal    (%rcx,%rsi), %edx
        cmpl    %edx, %ecx
        movl    %edx, (%rdi)
        setl    %dl
        xorl    %edx, %eax
        ret

我们要计算中的总和unsigned,因此unsigned必须能够表示所有值,int而不能将它们中的任何一个粘在一起。为了轻松将结果从转换unsignedint,相反的方法也很有用。总体而言,假设为二进制补码。

我认为在所有流行的平台上,我们都可以从 unsignedint通过简单的赋值为,int sum = u;但正如Jens所提到的,即使是最新的C2x标准变体也允许它发出信号。第二种最自然的方法是执行类似的操作:*(unsigned *)&sum = u;但是对于有符号和无符号类型,填充的非陷阱变体显然可能有所不同。因此,上面的示例很困难。幸运的是,gcc和clang都优化了这种棘手的转换。

PS上面的两个变体不能直接比较,因为它们的行为不同。第一个遵循原始问题,*value在溢出的情况下不会破坏。第二个遵循Jens的答案,并且总是掩盖第一个参数指向的变量,但是它是无分支的。


您能显示生成的asm吗?
R .. GitHub停止帮助冰

在溢出检查中用xor替换了相等性,以使用gcc获得更好的asm。添加了asm。
亚历山大·切列帕诺夫

1

我能想到的最好的版本是:

int safe_add(int *value, int delta) {
    long long t = *value + (long long)delta;
    if (t != ((int)t))
        return -1;
    *value = (int) t;
    return 0;
}

产生:

safe_add(int*, int):
    movslq  %esi, %rax
    movslq  (%rdi), %rsi
    addq    %rax, %rsi
    movslq  %esi, %rax
    cmpq    %rsi, %rax
    jne     .L3
    movl    %eax, (%rdi)
    xorl    %eax, %eax
    ret
.L3:
    movl    $-1, %eax
    ret

即使不使用溢出标志,我也感到惊讶。仍然比显式范围检查好得多,但是并不能推广到长整型。
塔维安·巴恩斯

@TavianBarnes你是对的,可惜的是使用溢出C标志位(除特定的编译器内建)没有什么好办法
伊利亚Bursov

1
此代码遭受带符号的溢出,这是未定义的行为。
emacs

@emacsdrivesmenuts,您是对的,比较中的演员可能会溢出。
Jens Gustedt

@emacsdrivesmenuts强制转换不是未定义的。如果超出的范围int,则从更广泛的类型进行强制类型转换将产生实现定义的值或发出信号。我关心的所有实现都将其定义为保留执行正确操作的位模式。
塔维安·巴恩斯

0

我可以通过假设(并断言)二进制补码表示而无需填充字节来使编译器使用符号标志。尽管我在标准中找不到对此要求的肯定正式确认(而且可能没有),但这样的实现应该在注释所注释的行中产生所需的行为。

请注意,以下代码仅处理正整数加法,但可以扩展。

int safe_add(int* lhs, int rhs) {
    _Static_assert(-1 == ~0, "integers are not two's complement");
    _Static_assert(
        1u << (sizeof(int) * CHAR_BIT - 1) == (unsigned) INT_MIN,
        "integers have padding bytes"
    );
    unsigned value = *lhs;
    value += rhs;
    if ((int) value < 0) return -1; // impl. def., 6.3.1.3/3
    *lhs = value;
    return 0;
}

这会在clang和GCC上产生:

safe_add:
        add     esi, DWORD PTR [rdi]
        js      .L3
        mov     DWORD PTR [rdi], esi
        xor     eax, eax
        ret
.L3:
        mov     eax, -1
        ret

我认为比较中的类型转换是不确定的。但是您可以像我在回答中那样避免这种情况。但同时,所有的乐趣在于能够涵盖所有情况。您_Static_assert的目的不多,因为在任何当前体系结构上这都是微不足道的,甚至对于C2x都是如此。
Jens Gustedt

2
@Jens实际上,如果我正确阅读(ISO / IEC 9899:2011)6.3.1.3/3,则强制转换似乎是实现定义的,而不是未定义的。你能仔细检查一下吗?(但是,将其扩展为否定论点会使整个过程变得很复杂,最终类似于您的解决方案。)
Konrad Rudolph

您是对的,它是实现的定义,但也可能会发出信号:(
Jens Gustedt

@Jens是的,从技术上讲,我猜二进制补码实现可能仍包含填充字节。也许代码应该通过将理论范围与进行比较来对此进行测试INT_MAX。我将编辑帖子。但是话又说回来,我认为该代码无论如何都不应该在实践中使用。
康拉德·鲁道夫
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.