【发布时间】:2017-04-09 17:46:49
【问题描述】:
以下(C99 和更高版本)代码想要计算一个正方形,限制为与原始固定宽度类型相同的位数。
#include <stdint.h>
uint8_t sqr8( uint8_t x) { return x*x; }
uint16_t sqr16(uint16_t x) { return x*x; }
uint32_t sqr32(uint32_t x) { return x*x; }
uint64_t sqr64(uint64_t x) { return x*x; }
问题是:根据 int 大小,一些乘法可以在提升为 (signed) int 的参数上执行,结果溢出 (signed) int,因此就标准而言,结果未定义;并且可能是错误的结果,尤其是在(越来越少见的)不使用 two's complement 的机器上。
如果 int 是 32 位(分别为 16 位、64 位、80 或 128 位),则 sqr16(分别为 sqr8、sqr32、sqr64)会发生这种情况当x 为0xFFFFF 时(分别为0xFF、0xFFFFFFFF、0xFFFFFFFFFFFFFFFF)。这 4 个函数都不能在 C99 下正式移植!!
C11 或更高版本或某些版本的 C++ 是否可以解决这种不幸的情况?
一个简单有效的解决方案是:
#include <stdint.h>
uint8_t sqr8( uint8_t x) { return 1u*x*x; }
uint16_t sqr16(uint16_t x) { return 1u*x*x; }
uint32_t sqr32(uint32_t x) { return 1u*x*x; }
uint64_t sqr64(uint64_t x) { return 1u*x*x; }
这是符合标准的,因为 1u 没有提升为 int 并且保持未签名;因此,左乘法,然后是右乘法,以无符号方式执行,因此定义良好,可以在必要数量的低位中产生正确的结果;最终隐式转换为结果宽度也是如此。
已更新: 正如comment by Marc Glisse 中所建议的那样,我使用八个编译器(三个版本的 GCC for x86 从 3.1、MS C/C++ 19.00、Keil ARM 编译器 5、两个 Cosmic ST7 变体的编译器,Microchip MCC18)。它们都生成了与原始代码完全相同的代码(我在实际项目的发布模式下使用了优化)。但是,编译器可能会生成比原始代码更差的代码。我还有其他几个嵌入式编译器可以尝试,包括一些 68K 和 PowerPC 的。
我们还有哪些其他选择,可以在可能更好的性能、可读性和简单性之间做出合理的平衡?
【问题讨论】:
-
将乘法转换为更宽的类型,然后再转换回更窄的类型
uint8_t sqr8(uint8_t x) { return (uint8_t)((uint16_t)x * (uint16_t)x); } -
@Toby:我的理解是,即使有演员表,参数也会仍然被提升为
int。 -
是的,他们会的,但只要您始终将乘法转换为 更宽 类型,那么签名性就不会影响它。 (例如,将最大
uint16_t值 65535 平方会导致值小于最大uint32_t值)。 -
哪个编译器会在您的第二个版本中生成什么错误代码?
-
抛开整数促销,注意如果
x > sqrt(UINTn_MAX),这些函数中的每一个都会溢出。这不是 C 标准的错,而是算法的错。您需要构建运行时检查或记录函数可以处理的最大值。例如,对于uint8_t,函数调用对于大于16 的值没有任何意义。我真的不明白为sqr8(17)获取值33 会有什么用处。我宁愿得到值 289。这显然不适合 uint8_t,所以算法是问题所在。
标签: c++ c c99 standards-compliance