如何在C中实现包装signed int加法

Fed*_*ico 6 c integer integer-overflow

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

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

我想主要针对 x86-64 架构,但当然实现越便携越好。我还主要关心通过 gcc、clang、icc 以及 Windows 上使用的任何东西生成体面的汇编代码。

目标是双重的:

  1. 编写不落入未定义行为黑洞的正确 C 代码;
  2. 编写被编译为体面的机器代码的代码。

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

我能够满足这两个要求中的任何一个,但不能同时满足。

尝试 1

想到的第一个实现是

int add_wrap(int x, int y) {
    return (unsigned) x + (unsigned) y;
}
Run Code Online (Sandbox Code Playgroud)

这似乎适用于gcc、clang和 icc。但是,据我所知,C 标准没有指定从unsigned intto的强制转换signed int,为实现留下了自由(另请参见此处)。

否则,如果新类型是有符号的并且值不能在其中表示;要么结果是实现定义的,要么引发实现定义的信号。

我相信大多数(全部?)主要编译器都会从unsignedto进行预期的转换int,这意味着它们采用正确的代表性模数 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;
}
Run Code Online (Sandbox Code Playgroud)

这应该可以用clang解决问题,因为文档清楚地说

如果可能,结果将等于数学上正确的结果,内置函数将返回 0。否则,内置函数将返回 1,结果将等于唯一值,该值等于数学上正确的结果模 2 提升为k 次幂,其中 k 是结果类型中的位数。

问题是在GCC 文档中他们说

这些内置函数将前两个操作数提升为无限精度有符号类型,并对这些提升的操作数执行加法。然后将结果转换为第三个指针参数指向的类型并存储在那里。

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

正如你所看到的 [here][godbolt],GCC 也会生成预期的代码,但我想确定这不是偶然的,因为它确实是__builtin_add_overflow.

icc似乎也产生了一些合理的东西。

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

尝试 3

遵循SEI CERT C 编码标准中那些迂腐家伙的建议。

在他们的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;
}
Run Code Online (Sandbox Code Playgroud)

代码执行正确的检查并leal使用gcc 编译,但不使用clang或icc。

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

编辑

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

例如,对于gcc和clang,以下两个应该执行相同操作的函数被编译为不同的程序集。 f在两种情况下g都不好,在两种情况下都很好(addl+jo或addl+ 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;
  }
}
Run Code Online (Sandbox Code Playgroud)

chu*_*ica 2

有点像@Andrew 的答案,没有memcpy().

使用 aunion来否定 的需要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;
}
Run Code Online (Sandbox Code Playgroud)

对于那些喜欢 1-liners 的人,请使用复合文字。

int add_wrap2(int x, int y) {
  return ( union { unsigned un; int in; }) {.un = (unsigned) x + (unsigned) y}.in;
}
Run Code Online (Sandbox Code Playgroud)