如何在固定宽度类型上强制执行无符号算术?
How to force unsigned arithmetic on fixed-width types?
以下(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 大小,一些乘法可以对提升为(有符号)int 的参数执行,结果会溢出(有符号)int,因此就标准而言,结果是未定义的;并且可以想象错误的结果,特别是在(越来越少的)不使用 two's complement.
的机器上
如果 int
是 32 位(分别为 16 位、64 位、80 或 128 位),则 sqr16
(分别为 sqr8
、 sqr32
, sqr64
) 当 x
是 0xFFFFF
(resp. 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
并且保持未签名;因此,左乘法,然后是右乘法,作为无符号执行,因此被明确定义为在必要数量的低阶位中产生正确的结果;对于结果宽度的最终隐式转换也是如此。
更新: 正如 中的建议,我用八个编译器尝试了这个变体(从 3.1 开始的 x86 的三个 GCC 版本,MS C/C ++ 19.00,Keil ARM 编译器 5,两个用于 ST7 变体的 Cosmic 编译器,Microchip MCC18)。它们都生成了与原始代码完全相同的代码(我在实际项目的发布模式中使用了优化)。然而,编译器可能会生成比原始代码更糟糕的代码;我还有其他几个嵌入式编译器可以尝试,包括一些 68K 和 PowerPC 编译器。
我们还有哪些其他选择,在可能更好的性能、可读性和简单性之间取得合理的平衡?
对于较窄的无符号类型,您无法避免将类型提升为 int
。
它更像是乘法运算符的 属性。
为了避免未定义的行为极端情况,只有你能做的是从不在使用无符号类型时使用乘法,其中它们最大值的平方可以溢出 int
.
幸运的是(除非你在嵌入式领域工作,否则你总是可以查阅文档以了解准确的行为),你可以在很大程度上将 unsigned short
委托给历史:int
及其 unsigned
表弟很可能不会慢,甚至可能更快。
您已经确定了 <stdint.h>
中整数类型别名的一个基本缺点:它们不包含有关类型转换等级的任何信息。因此,您无法控制这些类型的值是否进行整型提升,并且正如您正确观察到的那样,当整型提升导致有符号类型时,表达式可能具有未定义的行为。
简而言之:您不能将别名类型用于执行模 2N 的常用算术运算。您需要使用其(已知!)转换等级至少为 int
.
的类型
一般的解决方案是将您的操作数转换为 unsigned int
、unsigned long int
或 unsigned long long int
中最小的适当值(前提是您的平台没有扩展整数类型),然后评估表达式,然后转换回原始类型(具有正确的模块化行为)。在 C++ 中,您可能可以编写一个类型特征,以可移植的方式计算出正确的类型。
作为一个更便宜的技巧,并且再次假设没有(更广泛的)扩展整数类型,您可以将所有内容提升到 unsigned long long int
并希望您的编译器以有效的方式进行计算。
How to force unsigned arithmetic on fixed-width types?
What other options do we have, ...?
通过使用至少与 unsigned
一样宽的固定宽度类型作为函数参数的类型。
这导致转换至少 unsigned
作为参数传递的一部分。形式参数和返回类型的类型仍然是经典的"fixed-width types"。该函数的实际扩充也是固定宽度类型,但可能是更宽的固定宽度类型。
#if UINT16_MAX >= UINT_MAX
typedef uint8 uint16_t
typedef uint16 uint16_t
typedef uint32 uint32_t
typedef uint64 uint64_t
#elif UINT32_MAX >= UINT_MAX
typedef uint8 uint32_t
typedef uint16 uint32_t
typedef uint32 uint32_t
typedef uint64 uint64_t
#elif UINT64_MAX >= UINT_MAX
typedef uint8 uint64_t
typedef uint16 uint64_t
typedef uint32 uint64_t
typedef uint64 uint64_t
#endif
uint16_t sqr16(uint16 x) { return x*x; }
uint16_t sqr32(uint32 x) { return x*x; }
uint16_t sqr64(uint64 x) { return x*x; }
// usage
uint16_t x16 = ...;
uint32_t x32 = ...;
uint64_t x64 = ...;
x16 = sqr16(x16);
x32 = sqr32(x32);
x64 = sqr64(x64);
根据函数的不同,如果使用如下更广泛的类型调用函数,则会出现问题。 uint16_t foo16(uint16 x)
可能没有采取预防措施来防止收到超出 uint16_t
范围的值。
x16 = foo16(x32);
如果这一切更好?我仍然更喜欢
中的显式1u
uint16_t sqr16(uint16_t x) { return 1u*x*x; }
以下(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 大小,一些乘法可以对提升为(有符号)int 的参数执行,结果会溢出(有符号)int,因此就标准而言,结果是未定义的;并且可以想象错误的结果,特别是在(越来越少的)不使用 two's complement.
的机器上如果 int
是 32 位(分别为 16 位、64 位、80 或 128 位),则 sqr16
(分别为 sqr8
、 sqr32
, sqr64
) 当 x
是 0xFFFFF
(resp. 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
并且保持未签名;因此,左乘法,然后是右乘法,作为无符号执行,因此被明确定义为在必要数量的低阶位中产生正确的结果;对于结果宽度的最终隐式转换也是如此。
更新: 正如
我们还有哪些其他选择,在可能更好的性能、可读性和简单性之间取得合理的平衡?
对于较窄的无符号类型,您无法避免将类型提升为 int
。
它更像是乘法运算符的 属性。
为了避免未定义的行为极端情况,只有你能做的是从不在使用无符号类型时使用乘法,其中它们最大值的平方可以溢出 int
.
幸运的是(除非你在嵌入式领域工作,否则你总是可以查阅文档以了解准确的行为),你可以在很大程度上将 unsigned short
委托给历史:int
及其 unsigned
表弟很可能不会慢,甚至可能更快。
您已经确定了 <stdint.h>
中整数类型别名的一个基本缺点:它们不包含有关类型转换等级的任何信息。因此,您无法控制这些类型的值是否进行整型提升,并且正如您正确观察到的那样,当整型提升导致有符号类型时,表达式可能具有未定义的行为。
简而言之:您不能将别名类型用于执行模 2N 的常用算术运算。您需要使用其(已知!)转换等级至少为 int
.
一般的解决方案是将您的操作数转换为 unsigned int
、unsigned long int
或 unsigned long long int
中最小的适当值(前提是您的平台没有扩展整数类型),然后评估表达式,然后转换回原始类型(具有正确的模块化行为)。在 C++ 中,您可能可以编写一个类型特征,以可移植的方式计算出正确的类型。
作为一个更便宜的技巧,并且再次假设没有(更广泛的)扩展整数类型,您可以将所有内容提升到 unsigned long long int
并希望您的编译器以有效的方式进行计算。
How to force unsigned arithmetic on fixed-width types?
What other options do we have, ...?
通过使用至少与 unsigned
一样宽的固定宽度类型作为函数参数的类型。
这导致转换至少 unsigned
作为参数传递的一部分。形式参数和返回类型的类型仍然是经典的"fixed-width types"。该函数的实际扩充也是固定宽度类型,但可能是更宽的固定宽度类型。
#if UINT16_MAX >= UINT_MAX
typedef uint8 uint16_t
typedef uint16 uint16_t
typedef uint32 uint32_t
typedef uint64 uint64_t
#elif UINT32_MAX >= UINT_MAX
typedef uint8 uint32_t
typedef uint16 uint32_t
typedef uint32 uint32_t
typedef uint64 uint64_t
#elif UINT64_MAX >= UINT_MAX
typedef uint8 uint64_t
typedef uint16 uint64_t
typedef uint32 uint64_t
typedef uint64 uint64_t
#endif
uint16_t sqr16(uint16 x) { return x*x; }
uint16_t sqr32(uint32 x) { return x*x; }
uint16_t sqr64(uint64 x) { return x*x; }
// usage
uint16_t x16 = ...;
uint32_t x32 = ...;
uint64_t x64 = ...;
x16 = sqr16(x16);
x32 = sqr32(x32);
x64 = sqr64(x64);
根据函数的不同,如果使用如下更广泛的类型调用函数,则会出现问题。 uint16_t foo16(uint16 x)
可能没有采取预防措施来防止收到超出 uint16_t
范围的值。
x16 = foo16(x32);
如果这一切更好?我仍然更喜欢
中的显式1u
uint16_t sqr16(uint16_t x) { return 1u*x*x; }