这是对问题的完全重写。希望现在已经清楚了。

我想在C中实现一个函数,该函数在溢出的情况下使用包装执行signed int的添加。

我想主要针对x86-64体系结构,但是当然,实现越容易移植就越好。我还主要关心通过gcc,clang,icc以及Windows上使用的任何东西来生成体面的汇编代码。

目标是双重的:

  • 编写不会落入未定义行为黑洞中的正确C代码;
  • 写代码,将其编译为合适的机器代码。

  • 体面的机器代码是指 native 支持该操作的机器上的单个leal或单个addl指令。

    我能够满足两个条件之一,但不能同时满足两个条件。

    尝试1

    我想到的第一个实现是
    int add_wrap(int x, int y) {
        return (unsigned) x + (unsigned) y;
    }
    

    这似乎适用于gccclangicc。但是,据我所知,C标准没有指定从unsigned intsigned int的转换,从而为实现留有余地(另请参见here)。



    我相信大多数(所有?)主要编译器都可以实现从unsignedint的预期转换,这意味着它们采用正确的代表模数2 ^ N,其中N是位数,但是标准没有规定,因此不能依赖之后(愚蠢的C标准再次出现)。同样,虽然这是在二进制补码机上执行的最简单的操作,但在一个二进制补码机上却是不可能的,因为存在一个无法表示的类:2 ^(N/2)。

    尝试2

    根据clang docs,可以这样使用__builtin_add_overflow
    int add_wrap(int x, int y) {
        int res;
        __builtin_add_overflow(x, y, &res);
        return res;
    }
    

    这应该可以解决clang的问题,因为文档明确指出



    问题是他们在GCC docs中说



    据我所知,从long int转换为int是特定于实现的,因此我看不到有任何保证会导致包装行为的保证。

    正如您在[here] [godbolt]中看到的那样,GCC还将生成预期的代码,但是我想确保这并非偶然,并且确实是__builtin_add_overflow规范的一部分。

    icc似乎也可以产生一些合理的结果。

    这产生了不错的汇编,但是依赖于内在函数,因此它不是真正符合标准的C。

    尝试3

    遵循SEI CERT C Coding Standard中那些学究的人的建议。

    在他们的CERT INT32-C建议中,他们解释了如何提前检查潜在的溢出。以下是他们的建议:
    #include <limits.h>
    
    int add_wrap(int x, int y) {
        if ((x > 0) && (y > INT_MAX - x))
            return (x + INT_MIN) + (y + INT_MIN);
        else if ((x < 0) && (y < INT_MIN - x))
            return (x - INT_MIN) + (y - INT_MIN);
        else
            return x + y;
    }
    

    该代码执行正确的检查,并使用gcc编译为leal,而不使用clangicc编译为f

    整个CERT INT32-C建议都是完全垃圾,因为它试图通过迫使程序员执行检查,而该检查首先应作为语言定义的一部分,从而将C转换为“安全”语言。这样做还迫使程序员编写代码,编译器无法再对其进行优化,那么使用C的原因又是什么呢?

    编辑

    对比在于所生成程序集的兼容性和体面性之间。

    例如,对于gccclang,应该执行相同操作的以下两个函数被编译为不同的程序集。
    两种情况下g都不好,两种情况下addl都很好(jo + addlcmovnol + jo)。我不知道cmovnol是否比g好,但是函数f始终比ojit_code好。
    #include <limits.h>
    
    signed int f(signed int si_a, signed int si_b) {
      signed int sum;
      if (((si_b > 0) && (si_a > (INT_MAX - si_b))) ||
          ((si_b < 0) && (si_a < (INT_MIN - si_b)))) {
        return 0;
      } else {
        return si_a + si_b;
      }
    }
    
    signed int g(signed int si_a, signed int si_b) {
      signed int sum;
      if (__builtin_add_overflow(si_a, si_b, &sum)) {
        return 0;
      } else {
        return sum;
      }
    }
    

    最佳答案


    您完全引用了规则。如果将无符号值转换为有符号值,则结果是实现定义的或引发信号。简而言之,编译器将描述发生的情况。
    例如,gcc9.2.0编译器在it's documentation about implementation defined behavior of integers中具有以下内容:

    10-07 19:32
    查看更多