为什么uint32_t比uint_fast32_t更受青睐?

88

看起来 uint32_tuint_fast32_t 更常见(我认识到这是个人经验证据)。对我来说,这似乎是不符合直觉的。

几乎每次我看到一个实现使用 uint32_t,它真正需要的只是一个能够容纳值高达4,294,967,295(通常在65,535和4,294,967,295之间的较低边界)的整数。

这时,使用 uint32_t 似乎很奇怪,因为“完全32位”保证并不是必需的,而 uint_fast32_t 的“最快可用>= 32位”的保证似乎恰到好处。此外,尽管它通常被实现,但 uint32_t 实际上并不保证存在。

那么,为什么会优先选择 uint32_t 呢?它是否简单更为熟悉,还是有技术上的优势?


25
也许他们需要一个恰好有32位的整数? - Stargateur
7
我第一次听说uint32_fast_t,如果我理解正确的话,它至少是32位(这意味着可能会更多?这听起来有点误导)。目前我在我的项目中使用uint32_t和其它类型,因为我要将这些数据打包并发送到网络上,我希望发送方和接收方都知道字段的确切大小。但这似乎不是最健壮的解决方案,因为某些平台可能没有实现uint32_t,但显然我所有的平台都支持,所以我对自己的做法感到满意。 - yano
5
对于网络编程来说,你还需要关注字节顺序/端序——uint32_t类型并没有涉及到这个问题(而且很遗憾目前没有像uint32_t_beuint32_t_le这样更适用于几乎所有情况的类型来代替uint32_t)。 - Brendan
3
关于 "_be" 和 "_le",你问能否使用 htonl() 和 ntohl() 来实现同样的功能? - mpez0
2
@Brendan,这是一个非常沉重的对象,隐藏在所有原始类型的标准整数中。我原则上同意你的观点,在标准库中应该处理这个问题,但我认为这可能不是合适的地方。 - Steve Cox
显示剩余9条评论
11个回答

84

uint32_t在任何支持它的平台上几乎具有相同的属性。1

相比之下,uint_fast32_t在不同系统上的行为保证很少。

如果您切换到一个uint_fast32_t大小不同的平台,则使用uint_fast32_t的所有代码都必须重新测试和验证。所有稳定性假设都将被抛弃。整个系统将以不同的方式工作。

编写代码时,您甚至可能无法访问大小不是32位的uint_fast32_t系统。

uint32_t不会表现出不同(请参见脚注)。

正确性比速度更重要。因此,过早地进行正确性优化比过早进行优化更好。

如果我要为uint_fast32_t为64位或更多位的系统编写代码,我可能会为两种情况测试我的代码并使用它。除非需要和机会都存在,否则这样做是个坏计划。

最后,如果您存储uint_fast32_t的时间或实例数量超过一定值,由于缓存大小问题和内存带宽,它可能比uint32更慢。今天的计算机往往更多地受到内存限制而不是CPU限制,因此在考虑内存开销后,uint_fast32_t可能会比其它类型更慢。


1正如@chux在评论中指出的那样,如果unsigned大于uint32_t,则对uint32_t进行算术运算将通过常规整数升级,否则它将保持为uint32_t。这可能会导致错误。没有什么是完美的。


16
“uint32_t在任何支持它的平台上都保证具有相同的特性。” 当“unsigned”比“uint32_t”更宽时,可能会出现问题,其中在一个平台上,“uint32_t”经过通常的整数提升,而在另一个平台上则不会。 然而,使用“uint32_t”可以显著减少这些整数运算问题。 - chux - Reinstate Monica
2
@chux 这是一个需要注意的边界情况,当进行乘法运算时可能会导致未定义行为,因为类型提升会优先选择 signed int,而带符号整数溢出是未定义行为。 - CodesInChaos
3
虽然这个回答在某种程度上是正确的,但它非常淡化了关键细节。简而言之,uint32_t 用于需要准确了解类型的机器表示细节的情况,而 uint_fast32_t 用于计算速度最重要的情况下,(无)符号和最小范围很重要,表示细节不是必要的情况。还有 uint_least32_t 用于最重要的情况是(无)符号和最小范围,紧凑性比速度更重要,并且精确表示并不是必要的情况。 - John Bollinger
1
一个特殊情况的例子:假设 unsigned short 等于 uint32_t,而 int 等于 int48_t。如果计算 (uint32_t)0xFFFFFFFF * (uint32_t)0xFFFFFFFF 这样的表达式,那么操作数会被提升为 signed int 并且触发有符号整数溢出,这是未定义行为。参见此问题。 - Nayuki
正确性比速度更重要,这是真的。但像 uint32_t 这样的类型在公共 API 中根本无法正确使用,因为精确的魔数如“32”实际上不在规范中(这应该是大多数软件开发人员的常见情况,直到涉及 ABI/某些线路格式之类的邪恶问题)。过早引入固定宽度的机器整数是一个常见的错误,会泄漏实现细节,降低可移植性,并增加由于规范解释错误而隐藏错误的风险。 - FrankHB
显示剩余2条评论

35
为什么很多人使用uint32_t而不是uint32_fast_t
注意:名称错误的uint32_fast_t应该为uint_fast32_tuint32_tuint_fast32_t有更严格的规范,因此功能更加一致。

uint32_t 优点:

  • 各种算法都指定了这种类型。我认为这是使用它的最好原因。
  • 确切的宽度和范围已知。
  • 该类型的数组不会浪费空间。
  • 带有溢出的无符号整数运算更可预测。
  • 在范围和数学上更接近其他语言的32位类型。
  • 从不填充。

uint32_t 缺点:

  • 并非总是可用(但在2018年这很少见)。
    例如:缺乏8/16/32位整数的平台(9/18/36位,others)。
    例如:使用非2的补码的平台。old 2200

uint_fast32_t 优点:

  • 始终可用。
    这使得所有平台,新旧,都可以使用快速/最小类型。
  • "Fastest" 支持32位范围的类型。

uint_fast32_t 缺点:

  • 范围仅略知。例如,它可能是一个64位类型。
  • 此类型的数组在内存中可能浪费空间。
  • 所有答案(包括我的),帖子和评论都使用了错误的名称uint32_fast_t。看起来很多人并不需要或使用这种类型。我们甚至没有使用正确的名称!
  • 可能存在填充-(罕见)。
  • 在某些情况下,“最快”的类型可能真正是另一种类型。因此,uint_fast32_t只是第一次近似。

最终,最佳选择取决于编码目标。除非编码非常广泛可移植或某些特定性能函数,否则请使用uint32_t


使用这些类型时会遇到另一个问题:它们与 int/unsigned 的等级比较。
假设 uint_fastN_t 可以作为 unsigned 的等级。这并没有被指定,但是这是一种可验证的条件。
因此,相对于 unsigned,uintN_t 更可能比 uint_fastN_t 更窄。这意味着使用 uintN_t 进行计算的代码更可能受到整数提升的影响,而不是使用 uint_fastN_t,这涉及到可移植性问题。
考虑到这个问题,使用选择的数学操作来获得可移植性优势 uint_fastN_t。

关于使用int32_t而不是int_fast32_t的一点说明:在某些罕见的机器上,INT_FAST32_MIN可能是-2,147,483,647而不是-2,147,483,648。更重要的是:(u)intN_t类型具有严格的规范,并且可生成可移植的代码。


2
支持32位范围的最快类型?这是一个时代的遗物,当时RAM以CPU速度运行,如今在PC上平衡已经发生了巨大变化,因此(1)从内存中提取32位整数比提取64位整数快两倍,(2)在32位整数上矢量化指令可以处理两倍于64位整数。它还真的是最快的吗? - Matthieu M.
5
有些东西是最快的,而有些则比较慢。考虑到数组和零扩展的需求,“什么是最快的整数大小”并没有一种适用于所有情况的答案。在x86-64 System V ABI中,uint32_fast_t是一个64位类型,因此它可以避免时不时的符号扩展,当与64位整数或指针一起使用时,可以使用imul rax, [mem]代替一个单独的零扩展加载指令。但这也意味着你需要付出双倍的缓存占用和额外的代码大小(对所有指令都要添加REX前缀)。 - Peter Cordes
3
作为所有C和C++应用程序的加权平均值,我认为在x86上使用uint32_fast_t是一个糟糕的选择。更快的操作很少见,并且其好处通常微不足道:正如@PeterCordes提到的imul rax,[mem]情况下的区别非常非常小:在融合域中只有一个微操作,在未融合的域中则没有。在大多数有趣的情况下,它甚至不会增加一个周期。将其与使用双倍内存和较差向量化相平衡,很难看到它经常胜出的情况。 - BeeOnRope
1
@chux CPU调优选项不会改变ABI(即-march=silvermont不会使uint32_fast_t成为32位类型而非64位)。stdint.h#defineunsigned long int。如果您想链接来自不同编译器(或具有不同选项的相同编译器)的对象,则编译器需要就大小达成一致。这不是编译器错过的优化,而是ABI设计者(即编译器开发人员)近20年来选择不良的情况(我个人认为)。对于标量指令计数的重视太大了,而对缓存占用和自动向量化的重视不够。 - Peter Cordes
2
@PeterCordes - 有趣但也很糟糕 :). 这将使fast_t成为更糟糕的int: 它不仅在不同平台上具有不同的大小,而且它会根据优化决策在不同文件中具有不同的大小!实际上,我认为即使进行整个程序的优化,它也无法工作:在C和C++中,大小是固定的,因此sizeof(uint32_fast_t)或任何直接确定它的东西都必须始终返回相同的值,所以编译器要进行这样的转换非常困难。 - BeeOnRope
显示剩余12条评论

26
为什么很多人使用uint32_t而不是uint32_fast_t
愚蠢的回答:
  • 没有标准类型uint32_fast_t,正确拼写应该是uint_fast32_t
实用的回答:
  • 许多人实际上使用uint32_tint32_t来表示它们精确的语义,即无符号的 32 位带环绕算术 (uint32_t) 或补码表示 (int32_t)。 xxx_fast32_t类型可能更大,因此不适合存储到二进制文件中,也不适合在紧凑的数组和结构中使用,或通过网络发送。 此外,它们甚至可能不会更快。
实用主义的回答:
  • 很多人就是不知道(或者根本不关心)uint_fast32_t,正如评论和答案中所示,并且可能认为普通的unsigned int具有相同的语义,尽管许多当前的体系结构仍然有 16 位的int,一些罕见的博物馆样本则存在其他奇怪的小于 32 的 int 大小。
用户体验答案:
  • 虽然uint_fast32_t可能比uint32_t更快,但使用起来更慢:打字时间更长,尤其是考虑到在 C 文档中查找拼写和语义的时间;-)
优雅也很重要(显然是基于个人观点):
  • uint32_t看起来很糟糕,以至于许多程序员更喜欢定义自己的u32uint32类型...从这个角度来看,uint_fast32_t看起来笨拙不堪。难怪它和它的朋友uint_least32_t等一样沉寂无声。

+1 for UX。我想这比std::reference_wrapper更好,但有时我会想知道标准委员会是否真的希望使用它所标准化的类型... - Matthieu M.

8
一个原因是无需任何特殊的typedef或需要包含其他内容,unsigned int已经是“最快”的。因此,如果您需要快速操作,请使用基本的intunsigned int类型。
虽然标准并没有明确保证它是最快的,但是在3.9.1中通过陈述“普通int具有执行环境架构建议的自然大小”间接保证了它的速度。换句话说,int(或其无符号对应物)是处理器最舒适的类型。
当然,您不知道unsigned int的大小。您只知道它至少与short一样大(我似乎记得short必须至少为16位,尽管我现在找不到标准!)。通常它只是简单的4个字节,但理论上它可能更大,或者在极端情况下甚至更小(尽管我从未遇到过这种情况的架构,即使在20世纪80年代的8位计算机上也是如此...也许是一些微控制器,谁知道结果我患有痴呆症,那时候int非常清晰地是16位)。
C++标准不费心指定<cstdint>类型或它们的保证,只是提到“与C相同”。
根据C标准,uint32_t保证您获得确切的32位。没有任何不同,没有少于和填充位。有时这正是您所需要的,因此它非常有价值。 uint_least32_t保证大小不能小于32位(但可能更大)。有时,但比精确宽度或“不关心”要少得多,这正是您想要的。
最后,uint_fast32_t在我看来有些多余,除了用于意图记录目的的文档之外。C标准规定“指定通常最快的整数类型”(注意“通常”一词),并明确提到它不需要在所有情况下都是最快的。换句话说,uint_fast32_tuint_least32_t基本相同,后者通常也是最快的,只是没有给出保证(但双方都没有保证)。
由于大多数情况下,您要么不关心确切的大小,要么需要恰好32(或64,有时16)位,并且由于“不关心”的unsigned int类型仍然是最快的,这就解释了为什么uint_fast32_t不太常用。

3
我很惊讶你不记得8位处理器上的16位int,在那些日子里我记不起来有任何使用更大型号的了。如果我没记错的话,基于分段的x86架构的编译器也使用16位的int - Mark Ransom
@MarkRansom:哇,你说得对。我一直非常确信int在68000上是32位(我把它当作一个例子)。但事实并非如此… - Damon
在过去,int 被认为是最快的类型,具有最小的 16 位宽度(这也是 C 语言具有整数提升规则的原因),但今天随着 64 位体系结构的出现,这已经不再正确。例如,在 x86_64 位上,8 字节整数比 4 字节整数更快,因为对于 4 字节整数,编译器必须插入额外的指令,将其扩展为 8 字节值,然后再与其他 8 字节值进行比较。 - user1143634
“unsigned int” 在 x64 上不一定是最快的。会发生奇怪的事情。 - Joshua
另一个常见情况是,由于历史原因,long 需要是 32 位的,而 int 现在需要不超过 long,因此即使使用 64 位会更快,int 也可能需要保持为 32 位。 - Davislor
换句话说,int(或其无符号对应)是处理器最舒适的类型。这是完全错误的。 int是至少16位大小的类型,其中最适合当前架构的类型。在所有8位CPU上,8位将是最“自然”和最快的整数,这可能仍然是按数量销售的处理器中最多的,并且其中一个是无符号的16位。 - 12431234123412341234123

6
我没有看到使用uint32_t的证据来表明它的范围。相反,大多数情况下,我看到uint32_t被用于各种算法中精确地保存4个八位字节的数据,并保证了环绕和移位语义!
此外,还有其他使用uint32_t而不是uint_fast32_t的原因:通常是因为它会提供稳定的ABI。此外,内存使用量可以准确地知道。这很大程度上抵消了从uint_fast32_t获得的速度优势,无论何时该类型与uint32_t不同时。
对于小于65536的值,已经有一个方便的类型,称为unsigned intunsigned short也需要至少具有该范围,但unsigned int是本机字大小)。对于小于4294967296的值,还有另一个称为unsigned long的类型。
最后,人们不使用uint_fast32_t是因为它太长了,容易打错:D

1
你的最后一句话完全是错的。声称应该使用 unsigned int 而不是 uint16_fast_t 意味着你声称比编译器更懂得如何做。 - ikegami
同时,对于更改您的文本意图,我深表歉意。那不是我的本意。 - ikegami
1
@ikegami:类型“unsigned int”始终会作为无符号类型进行操作,即使在提升时也是如此。在这方面,它优于uint16_tuint_fast16_t。如果uint_fast16_t的规范比普通整数类型更松散,以至于其范围不需要对那些地址未被取的对象保持一致,那么在内部执行32位算术但具有16位数据总线的平台上,这可能会带来一些性能优势。然而,标准并不允许这种灵活性。 - supercat
@supercat,那与我所说的无关。使用unsigned int可能有其原因,但Antii说应该使用unsigned int而不是unsigned short是因为前者更快。我的评论就针对这点。如果你只是想要更快的话,你应该使用uint_fast16_t。如果还有其他方面的考虑,你可能需要其他类型,但这不是Antii所说的。 - ikegami
从性能的角度来看,使用uint_fast16_t类型的单个对象与unsigned相比不太可能有任何特定的优势;因此,偏向于其中一个应该基于其他标准,而不是认为自己“比编译器更好”。然而,关于聚合体,程序员通常会“知道得更好”,因为单个32位对象比16位对象更快,但16位对象的聚合体比32位对象的聚合体更快。 - supercat
显示剩余4条评论

6

有几个原因。

  1. 很多人不知道“快速”类型的存在。
  2. 打字更冗长。
  3. 当你不知道类型的实际大小时,更难理解程序的行为。
  4. 标准实际上没有确定最快的类型,而且实际上哪种类型最快可能非常依赖于上下文。
  5. 我没有看到平台开发者在定义其平台时考虑这些类型大小的证据。例如,在x86-64 Linux上,“快速”类型都是64位的,即使x86-64支持32位值的快速操作。

总之,“快速”类型是毫无价值的垃圾。如果你真的需要弄清楚对于特定应用程序来说哪种类型最快,你需要在你的编译器上对你的代码进行基准测试。


历史上,有些处理器具有32位和/或64位内存访问指令,但没有8位和16位。因此,在20多年前,int_fast{8,16}_t可能不是完全愚蠢的选择。据我所知,最后一个这样的主流处理器是原始DEC Alpha 21064(第二代21164得到了改进)。可能仍然有一些嵌入式DSP或其他只执行字访问的设备,但在这些设备上可移植性通常不是一个很大的问题,因此我不明白为什么你会在这些设备上使用fast_t。还有手工制作的Cray“一切都是64位”的机器。 - user1998586
1
类别1b:许多人并不在意“快速”类型的存在。那就是我的类别。 - gnasher729
第六类:许多人不相信“快速”类型是最快的。我属于这一类。 - Clearer

6
从正确性和编程的易用性角度来看,uint32_t 相对于 uint_fast32_t 有许多优势,特别是由于更精确定义的大小和算术语义,正如许多用户所指出的那样。
可能被忽视的一点是,uint_fast32_t 的一个所谓优势 - 它可以更快,实际上从未以任何有意义的方式实现。大多数主导 64 位时代的 64 位处理器(主要是 x86-64 和 Aarch64)都是从 32 位架构发展而来,在 64 位模式下甚至拥有快速的 32 位本机操作。因此,在这些平台上,uint_fast32_t 就像 uint32_t 一样。
即使一些“落后”的平台,如POWER、MIPS64、SPARC只提供64位ALU操作,但大多数有趣的32位操作可以在64位寄存器上完成:底部32位将具有所需结果(所有主流平台至少允许您加载/存储32位)。左移是主要的问题,但即使是这种情况,编译器中的值/范围跟踪优化也可以在许多情况下进行优化。

我怀疑偶尔稍慢的左移或32x32->64乘法除了最晦涩的应用程序外,不会超过使用双倍内存的代价。

最后,我要指出的是,虽然权衡通常被描述为“内存使用和向量化潜力”(支持uint32_t)与指令计数/速度(支持uint_fast32_t),但即使这对我来说也不清楚。是的,在某些平台上,您需要额外的指令来执行一些32位操作,但您还将节省一些指令,因为:

  • Using a smaller type often allows the compiler to cleverly combine adjacent operations by using one 64-bit operation to accomplish two 32-bit ones. An example of this type of "poor man's vectorization" is not uncommon. For example, create of a constant struct two32{ uint32_t a, b; } into rax like two32{1, 2} can be optimized into a single mov rax, 0x20001 while the 64-bit version needs two instructions. In principle this should also be possible for adjacent arithmetic operations (same operation, different operand), but I haven't seen it in practice.
  • Lower "memory use" also often leads to fewer instructions, even if memory or cache footprint isn't a problem, because any type structure or arrays of this type are copied, you get twice the bang for your buck per register copied.
  • Smaller data types often exploit better modern calling conventions like the SysV ABI which pack data structure data efficiently into registers. For example, you can return up to a 16-byte structure in registers rdx:rax. For a function returning structure with 4 uint32_t values (initialized from a constant), that translates into

    ret_constant32():
        movabs  rax, 8589934593
        movabs  rdx, 17179869187
        ret
    

    The same structure with 4 64-bit uint_fast32_t needs a register move and four stores to memory to do the same thing (and the caller will probablyhave to read the values back from memory after the return):

    ret_constant64():
        mov     rax, rdi
        mov     QWORD PTR [rdi], 1
        mov     QWORD PTR [rdi+8], 2
        mov     QWORD PTR [rdi+16], 3
        mov     QWORD PTR [rdi+24], 4
        ret
    

    Similarly, when passing structure arguments, 32-bit values are packed about twice as densely into the registers available for parameters, so it makes it less likely that you'll run out of register arguments and have to spill to the stack1.

  • Even if you choose to use uint_fast32_t for places where "speed matters" you'll often also have places where you need a fixed size type. For example, when passing values for external output, from external input, as part of your ABI, as part of a structure that needs a specific layout, or because you smartly use uint32_t for large aggregations of values to save on memory footprint. In the places where your uint_fast32_t and ``uint32_t` types need to interface, you might find (in addition to the development complexity), unnecessary sign extensions or other size-mismatch related code. Compilers do an OK job at optimizing this away in many cases, but it still not unusual to see this in optimized output when mixing types of different sizes.

您可以玩一些上面的例子以及更多 在godbolt上


1 明确地说,将结构体紧密地打包到寄存器中并不总是对小值有明显的优势。这意味着在使用它们之前可能需要"提取"这些小值。例如,一个简单的函数返回两个结构成员的总和,需要一个 mov rax, rdi; shr rax, 32; add edi, eax,而对于64位版本,每个参数都有自己的寄存器,只需要一个单独的addlea。但是,如果您接受“在传递时紧密打包结构体”的设计总体上是有意义的,那么较小的值将更多地利用此功能。


2
哦,没错,我读了你上面关于SysV ABI的评论,但正如你后来指出的,可能是另一个组织/文件做出了决定——不过我猜一旦这种情况发生,它就几乎已经定局了。我认为,即使忽略内存占用和矢量化的影响,在没有良好32位操作支持的平台上,纯循环计数/指令计数也倾向于更大的类型还是有问题的——因为在某些情况下,编译器在较小的类型上可以优化得更好。我在上面添加了一些例子。@PeterCordes - BeeOnRope
在某些情况下,这是一种胜利,但在其他情况下则不然。如果接收端最终只是将其存储在内存中(在确定位置后),并且结构布局除尾随填充外没有其他填充,则这很棒。对于返回值,传递指针让被调用者进行存储可能会更好,但这会改变函数签名(如果您确实想使用这些值,则不是您想要的)。像往常一样,为了性能,最佳选择取决于上下文。我认为这是一个合理的ABI选择,并且对某些结构来说只是糟糕的选择。如果您想首先使用低32位成员,那就太好了。 - Peter Cordes
这在理论上是好的,但他们的编译器(或其他人)是否真的对私有函数进行了积极的调用约定IPA?Clang做了一点点(省略调用者不使用的结构成员,即使是紧凑寄存器),但我不知道它是否会更进一步(例如将rcx用作第三个返回槽,或者如果复制构造函数没有执行任何重要操作,则在寄存器中返回非平凡可复制对象)。顺便说一句,阴影空间最适合变长参数。 MS的ABI针对具有阴影空间和int / fp竞争寄存器参数插槽的变参函数进行了优化... - Peter Cordes
1
我认为编译器通常不会拆分函数。将快速路径剥离为单独的函数是一种有用的源级优化(特别是在头文件中,它可以内联)。如果90%的输入是“什么也不做的情况”,在调用者的循环中进行过滤会带来巨大的收益。如果我没记错的话,Linux使用__attribute__((noinline))确保gcc不会内联错误处理函数,并在许多调用者和不自行内联的重要内核函数的快速路径上放置一堆push rbx / ... / pop rbx / ...。 - Peter Cordes
1
在Java中,这也非常重要,因为内联对进一步优化非常关键(特别是去虚拟化,这与C ++不同),因此通常有必要将快速路径拆分出来,并且“字节码优化”实际上是一件事情(尽管传统智慧认为它没有意义,因为JIT进行最终编译),只是为了减少字节码计数,因为内联决策基于字节码大小,而不是内联的机器代码大小(相关性可以相差数个数量级)。 - BeeOnRope
显示剩余16条评论

5
据我了解,int 最初应该是一种“本地”的整数类型,并且保证至少为16位大小-当时被认为是“合理”的大小。随着32位平台变得更加普遍,“合理”的大小已经变成了32位:
  • 现代 Windows 在所有平台上都使用32位的 int
  • POSIX 保证 int 至少为32位。
  • C#、Java 有类型 int,确保其大小恰好为32位。
但是当64位平台成为常态时,没有人将 int 扩展为64位整数,原因是:
  • 可移植性:许多代码依赖于 int 的32位大小。
  • 内存消耗:每个 int 增加一倍内存使用量可能对大多数情况来说不太合理,因为在大多数情况下,使用的数字要小得多,不到20亿。
那么,为什么你会更喜欢 uint32_t 而不是 uint_fast32_t?与 C# 和 Java 总是使用固定大小整数的原因相同:程序员编写代码时不考虑不同类型可能的大小,他们为一个平台编写并在该平台上测试代码。大多数代码隐式地依赖于特定数据类型的大小。这就是为什么 uint32_t 是大多数情况下更好的选择-它不允许任何关于其行为的歧义。
此外,在大小等于或大于32位的平台上,uint_fast32_t 真的是最快的类型吗?并不是。考虑一下 GCC 在 Windows 上为 x86_64 编译的以下代码:
extern uint64_t get(void);

uint64_t sum(uint64_t value)
{
    return value + get();
}

生成的汇编代码如下:

push   %rbx
sub    $0x20,%rsp
mov    %rcx,%rbx
callq  d <sum+0xd>
add    %rbx,%rax
add    $0x20,%rsp
pop    %rbx
retq

如果你将get()的返回值更改为uint_fast32_t(在Windows x86_64上为4字节),则会得到以下结果:

push   %rbx
sub    $0x20,%rsp
mov    %rcx,%rbx
callq  d <sum+0xd>
mov    %eax,%eax        ; <-- additional instruction
add    %rbx,%rax
add    $0x20,%rsp
pop    %rbx
retq

请注意生成的代码几乎相同,除了在函数调用后添加了一个mov %eax, %eax指令,该指令旨在将32位值扩展为64位值。
如果您只使用32位值,则不会出现此类问题,但是您可能会使用size_t变量(可能是数组大小),而这些变量在x86_64上为64位。在Linux上,uint_fast32_t为8个字节,因此情况有所不同。
许多程序员在需要返回小值(比如范围在[-32, 32]内)时使用int。如果int是平台本机整数大小,则完美地运行,但由于它不是64位平台上的本机类型,因此另一种与平台本机类型匹配的类型是更好的选择(除非它经常与较小的其他整数一起使用)。
基本上,无论标准说什么,uint_fast32_t在某些实现上都是错误的。如果您关心在某些地方生成的附加指令,则应定义自己的“本机”整数类型。或者,您可以将size_t用于此目的,因为它通常与native大小匹配(我不包括旧的和晦涩的平台,例如8086,只有可以运行Windows、Linux等平台)。
另一个显示int应该是本机整数类型的迹象是“整数提升规则”。大多数CPU只能在本机上执行操作,因此32位CPU通常只能执行32位加法,减法等操作(Intel CPU在这里是个例外)。其他大小的整数类型仅通过加载和存储指令支持。例如,8位值应使用适当的“加载8位有符号”或“加载8位无符号”指令进行加载,并且在加载后将值扩展为32位。如果没有整数提升规则,C编译器将不得不为使用小于本机类型的类型的表达式添加更多代码。不幸的是,在64位架构中,这种情况不再成立,因为编译器现在必须在某些情况下发出附加指令(如上所示)。

2
关于“没有人将int扩展为64位整数”的想法和“不幸的是,在64位架构中这种情况不再成立”是非常好的观点。然而,公平地说,关于“最快”和比较汇编代码:在这种情况下,第二个代码片段似乎由于其额外的指令而更慢,但代码长度和速度有时并不很相关。更强的比较应该报告运行时间-然而这并不容易做到。 - chux - Reinstate Monica
我非常同意uint_fast32_t在除了非常特殊的情况下变得模糊不清的有用性。我怀疑uint_fastN_t出现的主要原因是为了适应“让我们不要将unsigned作为64位,即使它在新平台上通常最快,因为太多的代码会出问题”,但“我仍然想要一个快速的至少N位类型。”如果可以的话,我会再次点赞。 - chux - Reinstate Monica
大多数64位架构可以轻松地操作32位整数。即使DEC Alpha(它是一个全新的64位架构,而不是像PowerPC64或MIPS64这样的现有32位ISA的扩展)也具有32位和64位的加载/存储功能。(但没有字节或16位的加载/存储!)大多数指令仅为64位,但它具有本地HW支持32位加/减和乘法,将结果截断为32位。(http://alasir.com/articles/alpha_history/press/alpha_intro.html)因此,将“int”设置为64位几乎没有速度提升,并且通常会导致缓存占用的速度损失。 - Peter Cordes
另外,如果您将int设置为64位,则您的uint32_t固定宽度typedef需要一个__attribute__或其他技巧,或者一些比int更小的自定义类型。 (或short,但是然后您对于uint16_t也有相同的问题。)没有人想要那样做。 32位足够宽以涵盖几乎所有内容(不像16位); 在64位机器上仅使用32位整数并不会以任何有意义的方式“低效”。 - Peter Cordes
在调用/返回边界处,它会节省偶尔的指令,因此在某些情况下速度更快。但在其他情况下速度较慢:在 Bulldozer 家族和 Silvermont/KNL 上的乘法吞吐量/延迟,以及使用 SIMD 的吞吐量(每个向量的元素减半)。它还需要 REX 前缀来增加代码大小(从而间接地减慢速度)。更不用说将它们存储在内存中(结构体/数组)所需的缓存占用成本了。 - Peter Cordes
显示剩余5条评论

4

就实际目的而言,uint_fast32_t 完全没用。在最广泛的平台(x86_64)上定义不正确,并且除非您使用的是非常低质量的编译器,否则在任何地方都没有真正提供任何优势。从概念上讲,在数据结构/数组中使用“快速”类型从未有意义 - 从类型更高效地运算所节省的任何开销都将被增加工作数据集大小的成本(缓存未命中等)所抵消。对于单个本地变量(循环计数器、temps 等),如果这更有效,则非玩具编译器通常可以直接使用生成的代码中的较大类型,并仅在必要时为了正确性而截断到名义大小(对于有符号类型,则永远不需要)。

理论上有用的一个变体是 uint_least32_t,当您需要能够存储任何 32 位值但希望可移植到缺乏精确大小的 32 位类型的计算机时。但是,实际上,您不需要担心这个问题。


3
在许多情况下,当算法处理数据数组时,提高性能的最佳方法是尽量减少缓存未命中的次数。每个元素越小,就可以将更多元素装入高速缓存。这就是为什么很多代码仍然使用32位指针在64位机器上运行的原因:它们不需要接近4 GiB的数据,但使所有指针和偏移需要8个字节而不是4个字节的成本则会非常高。
还有一些ABI和协议被指定为需要精确的32位,例如IPv4地址。这就是uint32_t的真正含义:使用恰好 32位,无论CPU是否高效。这些以前声明为long或unsigned long,在64位转换期间引起了很多问题。如果您只需要一个无符号类型来保存至少2³²-1的数字,则自第一个C标准发布以来,这一点已经得到了unsigned long的定义。但事实上,足够旧的代码假定long可以保存任何指针、文件偏移量或时间戳,足够旧的代码则假定它恰好为32位宽,编译器不能保证long与int_fast32_t相同而不会破坏太多内容。
理论上,程序使用uint_least32_t可能更具有未来性,甚至可以将uint_least32_t元素加载到uint_fast32_t变量中进行计算。一个没有uint32_t类型的实现甚至可以声明自己符合标准!(它只是不能编译许多现有程序。)但事实上,已经没有任何架构可以使int、uint32_t和uint_least32_t不同,并且目前没有使用uint_fast32_t提高性能的优势。那为什么要过度复杂化呢?
然而,请看看我们已经拥有long时所有32_t类型需要存在的原因,你会发现这些假设以前曾经弄糊我们的脸。你的代码可能最终在一个精确宽度为32位的运算速度慢于本机字长的机器上运行,那时您最好使用uint_least32_t进行存储并坚持使用uint_fast32_t进行计算。或者,如果您只是想要简单的东西,那就使用unsigned long。

但是有一些架构中,int 不是 32 位的,例如 ILP64。并不是说它们很常见。 - Antti Haapala -- Слава Україні
我认为现在不存在ILP64?几个网页声称“Cray”使用它,所有这些网页都引用了1997年的同一个Unix.org页面,但是90年代中期的UNICOS实际上做了一些更奇怪的事情,而今天的Cray使用英特尔硬件。同样的页面声称ETA超级计算机使用了ILP64,但他们很久以前就已经倒闭了。维基百科声称HAL将Solaris移植到SPARC64使用了ILP64,但他们也已经倒闭多年了。CppReference表示,ILP64仅在一些早期的64位Unices中使用。因此,它只与一些非常神秘的复古计算有关。 - Davislor
请注意,如果您今天使用英特尔数学核心库的“ILP64接口”,则int将为32位宽。类型MKL_INT将发生变化。 - Davislor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接