- 这是对问题的完全改写,希望现在能更清楚 *
我想在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;
}
这似乎适用于gcc、clang和icc,然而,据我所知,C标准没有指定从unsigned int
到signed int
的转换,让实现自由选择(另请参见here)。
否则,如果新类型有符号,并且无法在其中表示该值;结果是实现定义的或者产生实现定义的信号。
我相信大多数(所有的?)主要编译器都执行了预期的从unsigned
到int
的转换,这意味着它们采用了正确的代表性模数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 int
到int
的转换是特定于实现的,所以我不能保证这会导致 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
,但不使用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;
}
}
4条答案
按热度按时间0kjbasz61#
我不太确定,因为从无符号到有符号的转换规则
你准确地引用了规则。如果你从一个无符号值转换成一个有符号值,那么结果是实现定义的,或者会引发一个信号。简单地说,会发生什么由你的编译器描述。
例如,gcc9.2.0编译器在其文档中有以下关于实现定义的整数行为的内容:
当一个整数值不能在有符号整数类型的对象中表示时,将该整数转换为该类型的结果或产生的信号(C906.2.1.2、C99和C116.3.1.3)。
为了转换成宽度为N的类型,该值被模2^N缩减到该类型的范围内;不产生信号。
yh2wf1be2#
我不得不做一些类似的事情;然而,我使用的是
stdint.h
中已知的宽度类型,需要处理32位有符号整数运算的 Package 。下面的实现之所以有效,是因为stdint类型必须是2的补码。我试图在Java中模拟这种行为,所以我让一些Java代码生成了一系列测试用例,并在clang、gcc和MSVC上进行了测试。qni6mghb3#
这看起来很荒谬,但我认为推荐的方法是使用
memcpy
。显然,所有现代编译器都优化了memcpy,它最终做了您最初希望做的事情--保留无符号加法的位模式。在优化的x86 clang上,如果目标是寄存器,则这是单指令。
apeeds0o4#
有点像@Andrew的答案,但没有
memcpy()
。使用
union
来否定对memcpy()
的需要。对于C2x,我们确信int
是2的赞美。对于那些喜欢1-liner的人,可以使用 compound literal。