在 C 系列语言中使用显式大小类型的缺点或权衡

Posted

技术标签:

【中文标题】在 C 系列语言中使用显式大小类型的缺点或权衡【英文标题】:drawbacks or tradeoffs of using explicitly sized types in C family languages 【发布时间】:2012-04-19 04:01:53 【问题描述】:

我正在开发几个 C 和 C++ 项目,这些项目需要在多个桌面和移动平台上进行移植。我知道当我在磁盘上读取和写入数据时,使用显式大小的类型 u32_t i64_t 等很重要。

使用显式大小的所有整数类型来确保执行一致是个好主意吗?我听说显式大小的类型会对性能产生影响,因为处理器针对其预期的int 类型等进行了优化。我还读到一个好的策略是在内部为类数据成员而不是在接口中使用显式大小的类型。

对于数据成员和接口上明确大小的类型,是否有任何最佳实践? (我假设在这些情况下 C 或 C++ 之间不会有巨大差异,但如果有,请告诉我)

【问题讨论】:

只要它们在需要时与替代品一样或更便携,我想除了确保您在开始,以便以后您不必更改类型或代码。唯一的缺点可能是 32 位整数(i32b 或其他)可能占用的空间是 int 所需空间的两倍。但是,我想知道我的数据类型有多大,所以我不认为这是不利的。如果我想要 >= 16 位,我会使用 >= 16 位,而不是 32 位。另外,你见过 stdint.h 吗?听起来你正在重新发明。 啊,实际上我错过了一个非常明显的缺点。与其他人的代码不兼容。我想你的 int32b 或其他任何东西几乎总是与int 隐式兼容,但在某些情况下可能不兼容(因此不应该假设)。 @Mehrdad 不确定这是针对我还是针对 OP,但如果是我,那么是的,有。我只是在想 C。 @Corbin:哦,我指的是 OP。是的,有 stdint.h/cstdint 几乎可以满足 OP 的需求.. @JustinMeiners:它有类似的类型,(int_least8_tint_least16_t 等...),但它也有精确大小的类型(int8_tint16_t 等... )。不同之处在于,如果您所在的平台没有这些精确大小的类型,则后者不存在。但如果是这种情况,那么您的 typedef 也将不起作用。 en.cppreference.com/w/cpp/types/integer 【参考方案1】:

基本“int”类型的好处在于,对于您当前正在编译的任何平台,它几乎总是最快的整数类型。

另一方面,使用 int32_t(而不仅仅是 int)的优势在于,无论在什么平台上编译,您的代码都可以依赖始终为 32 位宽的 int32_t,这意味着您可以安全地对值的行为做出比 int 更多的假设。对于固定大小的类型,如果您的代码在新平台 Y 上完全编译,那么它的行为更有可能与在旧平台 X 上完全相同。

int32_t 的(理论上)缺点是新平台 X 可能不支持 32 位整数(在这种情况下,您的代码根本无法在该平台上编译),或者它可能支持它们但处理它们的速度比它慢将处理普通的旧整数。

上面的例子有点做作,因为几乎所有现代硬件都在全速处理 32 位整数,但是确实存在(并且确实)存在操作 int64_ts 比操作 int 慢的平台,因为 (a) CPU 有32 位寄存器,因此必须将每个操作分成多个步骤,当然 (b) 64 位整数将占用 32 位整数的两倍内存,这会给缓存带来额外压力。

但是:请记住,对于 99% 的人编写的软件,这个问题不会对性能产生任何明显的影响,仅仅是因为现在 99% 的软件都不受 CPU 限制,而且即使对于代码来说,整数宽度也不太可能成为主要的性能问题。所以真正归结为,你希望你的整数数学表现如何

如果您希望编译器保证您的整数值始终占用 32 位 RAM,并且始终“环绕”在 2^31(或 2^32 无符号),无论你在什么平台上编译,都使用 int32_t (etc)。

如果您并不真正关心换行行为(因为您知道整数永远不会换行,因为它们存储的数据的性质),并且您想让代码更便携对于奇怪/不寻常的编译目标,并且至少理论上更快(尽管在现实生活中可能不是),那么您可以坚持使用普通的旧 short/int/long。

我个人默认使用固定大小的类型(int32_t 等),除非有非常明确的理由不这样做,因为我想尽量减少跨平台的变体行为数量。例如这段代码:

for (uint32_t i=0; i<4000000000; i++) foo();

... 将始终准确地调用 foo() 4000000000 次,而这段代码:

for (unsigned int i=0; i<4000000000; i++) foo();

可能调用 foo() 4000000000 次,或者它可能进入无限循环,这取决于 (sizeof(int)>=4) 与否。当然,可以手动验证第二个 sn-p 在任何给定平台上都这样做,但是考虑到两种样式之间的性能差异可能为零,我更喜欢第一个方法,因为预测其行为是不费吹灰之力的。我认为 char/short/int/long 方法在 C 的早期更有用,当时计算机体系结构更加多样化,CPU 足够慢,实现完整的本机性能比安全编码更重要。

【讨论】:

谢谢,这是一个完美的答案! 请注意,有符号整数的溢出是未定义的,因此如果您想要包装行为,请始终使用无符号类型。 Rust 的approach to types 用于将u8i32u64 等作为原始类型。 isize/usize 用于指针大小的整数,否则只有固定大小。所以语言设计者认为现代硬件提供依赖于实现的整数并不重要,因为 32 位在所有重要的事情上都很快,而且对于大多数用途来说足够大。 (如果你厌倦了 C 拒绝标准化二进制补码溢出/换行,或者像 popcnt、算术右移、旋转等之类的东西,试试 rust。)【参考方案2】:

使用inttypes.hstdint.h。它是 ANSI-C,因此任何旨在符合 ANSI 标准的工具链都将支持它。

此外,它还为您节省了重新发明***的工作。

你唯一要做的就是

#include <inttypes.h>

uint32_t integer_32bits_nosign;

关于可移植性的另一个问题: 数据字节序与数据宽度一样重要。 您必须使用标准宏检查目标字节序:

struct 
#if defined( __BIG_ENDIAN__ ) || defined( _BIG_ENDIAN )
    // Data disposition for Big Endian   
#else
    // Data disposition for Little Endian   
#endif
;

如果您使用位域,则特别敏感。


编辑:

当然,如果您打算在仅 C++ 的代码上使用 &lt;csdtint&gt;,您可以按照其他人的建议使用它。

【讨论】:

好的,你认为将它们用于数据成员等是个好主意吗? 您的类接口是最重要的契约,因此请关注它们。我会用整数编写接口。至于存储它们,你必须吗?如果没有,则使用 int。如果您必须然后指定大小。为了将数据存储在文件中,请使其尽可能可预测,因此要有一个只进行编码和解码的接口,这样您的代码就不必关心字节序 @JSPerfUnkn0wn 你为什么要让我的接口使用泛型类型? 因为int 有效,并且几乎不需要尝试修复未损坏的部分:) 您的界面会很好用,并且直观明显并且适合程序员见惯了。但最重要的是,只需选择一种编码标准并坚持下去。然后,您可以在使用不同类型时体验过所有内容,然后将您的体验反馈给社区:)【参考方案3】:

固定大小类型的一个相当讨厌的“陷阱”是,虽然它们给人的印象是代码不依赖于“int”的大小,但这实际上是一种错觉。一段代码如:

uint32_t mul(uint16_t a, uint16_t b)
 return a*b; 

对于所有平台上的“a”和“b”的所有值都有定义的含义 其中“int”为 40 位或更大,并且还将定义所有值的含义 在“int”为 16 位的所有平台上的“a”和“b”,尽管含义 当算术积为 65535 时会有所不同。 C89 标准指出,虽然没有要求这样做,但大多数 那个时代的实现定义了它们的整数数学行为,使得大多数 签名操作——除了一些特殊的例外——行为相同 即使结果在 INT_MAX+1 之间 和 UINT_MAX——因此在那些编译器上,“a”的所有值的行为 和“b”将匹配具有较大“int”类型的机器上的行为。它有 然而,对于 32 位编译器来说,生成将 打破大于 INT_MAX 的值,因为标准没有禁止它们 从这样做。

【讨论】:

@JustinMeiners:我想知道我应该如何最好地表达这样一个事实,即依赖于值 INT_MAX+1 到 UINT_MAX 的合理行为的代码将几乎始终有效,即使使用现代编译器也是如此,但是对代码的微小改动可能会让编译器看到它之前错过的“优化”,所以除非一个人禁用了一堆有用的优化,除了钝的超现代“优化”。【参考方案4】:

stdint.h中有fast_类型的定长整数,编译器会选择平台中所需尺寸的最快整数,例如(从stdint.h中提取)

typedef signed char     int_fast8_t;
#if __WORDSIZE == 64
typedef long int        int_fast16_t;
typedef long int        int_fast32_t;
typedef long int        int_fast64_t;
#else
typedef int         int_fast16_t;
typedef int         int_fast32_t;
__extension__
typedef long long int       int_fast64_t;
#endif

【讨论】:

问题是,快为了什么?对于临时用户,int32_t 比 x86-64 上的 int64_t 稍快。 (更小的代码大小(通常不需要 REX 前缀),并且在前 silvermont Atom 上的乘法速度更快)。如果您将它们存储到内存中,则需要 8B 而不是 4B。我认为 64b int_fast32_t 在 x86-64 上是一个非常奇怪的选择,但这是他们做出的选择。

以上是关于在 C 系列语言中使用显式大小类型的缺点或权衡的主要内容,如果未能解决你的问题,请参考以下文章

C语言 -- 字符串详解

C 语言数组 ( 数组本质 | 数组长度定义 | 数组初始化 | 编译时初始化 | 显式初始化 - 重置内存 )

C语言 int转char

谁能给我讲讲c的结构体啊

78.类型转换

C语言枚举类型是啥意思?