C语言中如何实现带符号整数加法的 Package

vs91vp4v  于 2023-01-16  发布在  其他
关注(0)|答案(4)|浏览(198)
  • 这是对问题的完全改写,希望现在能更清楚 *

我想在C中实现一个函数,执行signed int s的加法,并在溢出时进行 Package 。
我想主要针对x86 - 64架构,当然实现的可移植性越好,我也主要关心通过gcc、clang、icc以及Windows上使用的任何东西生成像样的汇编代码。
目标有两个:
1.编写正确的C代码,避免陷入未定义行为的黑洞;
1.编写可以编译成合适机器码的代码。
所谓像样的机器代码,我指的是原生支持该操作的机器上的单个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文档,可以像这样使用__builtin_add_overflow

int add_wrap(int x, int y) {
    int res;
    __builtin_add_overflow(x, y, &res);
    return res;
}

这个应该能解决叮当声的问题,因为医生明确指出
如果可能,结果将等于数学上正确的结果,并且内置函数将返回0。否则,内置函数将返回1,并且结果将等于唯一值,该唯一值相当于数学上正确的结果模2的k次幂,其中k是结果类型中的位数。
问题是在GCC docs中他们说
这些内置函数将前两个操作数提升为无限精度带符号类型,并对提升后的操作数执行加法运算,然后将结果转换为第三个指针参数所指向的类型并存储在那里。
据我所知,从long intint的转换是特定于实现的,所以我不能保证这会导致 Package 行为。
正如你所看到的[here][godbolt],GCC也会生成预期的代码,但我想确定这不是偶然的,而且确实是__builtin_add_overflow规范的一部分。
icc似乎也产生了一些合理的东西。
这产生了不错的汇编,但是依赖于intrinsic,所以它不是真正符合标准的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
整个CERT INT32-C建议完全是垃圾,因为它试图通过强制程序员执行检查来将C转换为"安全"语言,而这些检查首先应该是语言定义的一部分。在这样做的过程中,它还强制程序员编写编译器无法再优化的代码,那么还有什么理由再使用C呢?

编辑

对比是在生成的程序集的兼容性和得体性之间。
例如,对于gccclang,下面两个本应执行相同操作的函数被编译到不同的程序集。f在这两种情况下都是错误的,g在这两种情况下都很好(addl + joaddl + cmovnol)。我不知道jo是否优于cmovnol,但函数g始终优于f

#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;
  }
}
0kjbasz6

0kjbasz61#

我不太确定,因为从无符号到有符号的转换规则
你准确地引用了规则。如果你从一个无符号值转换成一个有符号值,那么结果是实现定义的,或者会引发一个信号。简单地说,会发生什么由你的编译器描述。
例如,gcc9.2.0编译器在其文档中有以下关于实现定义的整数行为的内容:
当一个整数值不能在有符号整数类型的对象中表示时,将该整数转换为该类型的结果或产生的信号(C906.2.1.2、C99和C116.3.1.3)。
为了转换成宽度为N的类型,该值被模2^N缩减到该类型的范围内;不产生信号。

yh2wf1be

yh2wf1be2#

我不得不做一些类似的事情;然而,我使用的是stdint.h中已知的宽度类型,需要处理32位有符号整数运算的 Package 。下面的实现之所以有效,是因为stdint类型必须是2的补码。我试图在Java中模拟这种行为,所以我让一些Java代码生成了一系列测试用例,并在clang、gcc和MSVC上进行了测试。

inline int32_t add_wrap_i32(int32_t a, int32_t b)
{
    const int64_t a_widened = a;
    const int64_t b_widened = b;
    const int64_t sum = a_widened + b_widened;

    return (int32_t)(sum & INT64_C(0xFFFFFFFF));
}

inline int32_t sub_wrap_i32(int32_t a, int32_t b)
{
    const int64_t a_widened = a;
    const int64_t b_widened = b;
    const int64_t difference = a_widened - b_widened;

    return (int32_t)(difference & INT64_C(0xFFFFFFFF));
}

inline int32_t mul_wrap_i32(int32_t a, int32_t b)
{
    const int64_t a_widened = a;
    const int64_t b_widened = b;
    const int64_t product = a_widened * b_widened;

    return (int32_t)(product & INT64_C(0xFFFFFFFF));
}
qni6mghb

qni6mghb3#

这看起来很荒谬,但我认为推荐的方法是使用memcpy。显然,所有现代编译器都优化了memcpy,它最终做了您最初希望做的事情--保留无符号加法的位模式。

int a;
int b;

unsigned u = (unsigned)a + b;
int result;
memcpy(&result, &u, sizeof(result));

在优化的x86 clang上,如果目标是寄存器,则这是单指令。

apeeds0o

apeeds0o4#

有点像@Andrew的答案,但没有memcpy()
使用union来否定对memcpy()的需要。对于C2x,我们确信int是2的赞美。

int add_wrap(int x, int y) {
  union {
    unsigned un;
    int in;
  } u = {.un = (unsigned) x + (unsigned) y};
  return u.in;
}

对于那些喜欢1-liner的人,可以使用 compound literal

int add_wrap2(int x, int y) {
  return ( union { unsigned un; int in; }) {.un = (unsigned) x + (unsigned) y}.in;
}

相关问题