为什么要uint32_t而不是uint_fast32_t?

看来, uint32_tuint_fast32_t更普遍(我意识到这是一个轶事证据)。 尽pipe如此,这对我来说似乎是反直觉的。

几乎总是当我看到一个实现使用uint32_t ,它真正想要的是一个整数,可以保存值高达4,294,967,295(通常是65,535和4,294,967,295之间的某个较低的边界)。

使用uint32_t似乎很奇怪,因为不需要“正好32位”的保证,并且uint_fast32_t“最快可用> = 32位”保证似乎是正确的想法。 而且,虽然它通常被实现,但uint32_t实际上并不保证存在。

那么为什么uint32_tuint32_t ? 它是只是更好的了解,还是有技术上的优势?

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

uint_fast32_t对于它在不同系统上的performance如何相比几乎没有什么保证。

如果切换到uint_fast32_t具有不同大小的平台,则所有使用uint_fast32_t代码都必须重新testing和validation。 所有的稳定性假设都将在窗外。 整个系统将以不同的方式工作。

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

uint32_t将不会以不同的方式工作(除非参见脚注)。

正确性比速度更重要。 因此,过早的正确性比不成熟的优化更好。

如果我正在为uint_fast32_t为64位或更多位的系统编写代码,则可能会针对这两种情况testing我的代码并使用它。 除了需要和机会,这样做是一个糟糕的计划。

最后,由于caching大小问题和内存带宽的限制, uint_fast32_t在存储任何时间长度或实例数量时都会比uint32慢。 今天的计算机通常比CPU绑定的内存限制更多, uint_fast32_t可能会更快隔离,但是在解决内存开销之后可能会更快。


1 @chux在评论中指出,如果unsigned大于uint32_t ,则uint32_t算术将通过通常的整数提升,如果不是,则保留为uint32_t 。 这可能会导致错误。 没有什么是完美的。

为什么很多人使用uint32_t而不是uint32_fast_t

注意:错误的uint32_fast_t应该是uint_fast32_t

uint32_tuint_fast32_t有更严格的规范,因此可以提供更一致的function。


uint32_t优点:

  • 各种algorithm指定了这种types。 IMO – 使用的最佳理由。
  • 确切的宽度和范围已知。
  • 这种arrays不会浪费。
  • 无符号整数math与溢出更容易预测。
  • 在其他语言的32位types的范围和math更紧密的匹配。
  • 从未填充。

uint32_t缺点:

  • 并不总是可用(但这是非常罕见的)。

uint_fast32_t优点:

  • 始终可用。
  • 支持32位范围的“最快”types。

uint_fast32_t缺点:

  • 范围只有最小的知道。 例如,它可能是一个64位的types。
  • 记忆中这种arrays可能是浪费的。
  • 所有的答案(我的起初),post和评论使用了错误的名字uint32_fast_t 。 看起来很多只是不需要和使用这种types。 我们甚至没有使用正确的名字!
  • 填充可能 – (罕见)。
  • 在select的情况下,“最快”types可能确实是另一种types。 所以uint_fast32_t只是一阶近似。

最后,最好取决于编码的目标,所以没有整体偏好。


当使用这些types时,还有另外一个问题:它们的等级与int/unsigned相比

据推测, uint_fastN_t至less是unsigned的等级。 这没有规定,但一定的和可testing的条件。

因此, uintN_tuint_fastN_t更可能比unsigned更窄。 这意味着在涉及可移植性时,使用uintN_tmath的代码比uint_fastN_t更可能受到整数升级的uint_fastN_t

有了这个担心:可移植性优势uint_fastN_tselectmath运算。


关于int32_t而不是int_fast32_t :在罕见的机器上, INT_FAST32_MIN可能是-2,147,483,647而不是-2,147,483,648。 大点: (u)intN_ttypes是严格指定的,并导致可移植的代码。

为什么很多人使用uint32_t而不是uint32_fast_t

愚蠢的回答:

  • 没有标准types的uint32_fast_t ,正确的拼写是uint_fast32_t

实际答案:

  • 许多人实际上使用uint32_tint32_t作为精确的语义,正好使用32位无符号uint32_tuint32_t )或2的补码表示( int32_t )。 xxx_fast32_ttypes可能较大,因此不适合存储到二进制文件,在打包的数组和结构中使用,或通过networking发送。 而且,他们甚至可能不会更快。

务实的回答:

  • 许多人只是不知道uint_fast32_t (或根本不在乎) uint_fast32_t ,如注释和答案中所示,并且可能假设plain unsigned int具有相同的语义,尽pipe许多当前体系结构仍然具有16位int s,罕见的博物馆样品有其他奇怪的诠释大小小于32。

UX答案:

  • 虽然可能比uint32_t更快,但uint_fast32_t使用起来较慢:input时间较长,特别是在C文档中查找拼写和语义;-)

优雅至关重要(明显是基于观点的):

  • uint32_t看起来不够糟糕,许多程序员更喜欢定义自己的u32uint32types…从这个angular度来看, uint_fast32_t看起来笨拙,无法修复。 毫不奇怪,它和朋友uint_least32_t等坐在板凳上。

一个原因是unsigned int已经是“最快的”,不需要任何特殊的typedefs或者需要包含某些东西。 所以,如果你需要它,只需使用基本的intunsigned inttypes。
尽pipe标准没有明确地保证它是最快的,但是它通过3.9.1中的明码具有执行环境的体系结构build议的自然大小” 间接这样做。 换句话说, int (或其未签名的对应)是处理器最适合的。

现在当然,你不知道unsigned int大小是多less。 你只知道它至lessshort一样大(我似乎记得short必须至less有16位,尽pipe现在我在标准中找不到那个)。 通常这只是简单的4个字节,但理论上它可能更大,或者在极端情况下甚至更小( 尽pipe我个人从来没有遇到这种情况的架构,甚至在20世纪80年代的8位计算机上也没有遇到过。 ..也许有些微控制器,谁知道我患了痴呆症, int是非常明确的16位)。

C ++标准并不打算指定<cstdint>types是什么或它们保证什么,它只是提到“与C中相同”。

根据C标准, uint32_t 保证你得到正好32位。 没有什么不同,毫无差别,没有填充位。 有时这正是你所需要的,因此这是非常有价值的。

uint_least32_t保证无论大小如何,都不能小于32位(但可能会更大)。 有时候,但比一个确切的witdh或“不关心”更less,这是你想要的。

最后, uint_fast32_t在我看来有点多余,除了意图文档的目的。 C标准规定“指定一个通常最快的整数types” (注意“通常”一词),并明确提到它不一定是最快的。 换句话说, uint_fast32_tuint_least32_t大致相同, 通常也是最快的,只是没有保证(但不能保证)。

由于大部分时间你不关心确切的大小,或者你想要32位(或64位,有时是16位),并且由于“无所谓” unsigned inttypes是最快的,这就解释了为什么uint_fast32_t isn经常使用。

几个原因。

  1. 许多人不知道“快”types存在。
  2. input更为详细。
  3. 当你不知道types的实际大小时,更难以推断你的程序行为。
  4. 这个标准实际上并不是最快的,也不能确定哪种types实际上是最快的。
  5. 我没有看到平台开发人员在定义他们的平台的时候会考虑这些types的大小。 例如,在x86-64 Linux上,“快速”types都是64位的,即使x86-64具有对32位值进行快速操作的硬件支持。

总之,“快”types是毫无价值的垃圾。 如果您真的需要弄清楚给定应用程序的最快types,则需要在编译器上对您的代码进行基准testing。

我还没有看到uint32_t被用于范围的证据。 相反,大多数时候我已经看到uint32_t被使用了,它将在各种algorithm中保存正好4个八位字节的数据,并保证环绕和移位语义!

还有其他的原因,使用uint32_t而不是uint_fast32_t :它通常会提供稳定的ABI。 另外,内存使用情况可以准确的知道。 无论从uint_fast32_t获得的速度uint_fast32_t只要该types与uint32_ttypes不同, 就会非常偏移。

对于<65536的值,已经有了一个方便的types,它被称为unsigned intunsigned short也是至less要有这个范围,但unsigned int是本地字的大小)对于值<4294967296, unsigned long


最后,人们不使用uint_fast32_t因为uint_fast32_t时间太长,容易错误:D

据我的理解, int最初被认为是一个“本地”整数types,并且额外保证它应该至less有16位的大小 – 当时被认为是“合理”的大小。

当32位平台变得更普遍时,我们可以说“合理”的大小已经变成了32位:

  • 现代Windows在所有平台上使用32位int
  • POSIX保证int至less是32位。
  • C#中,Java的types是int ,保证是32位的。

但是,当64位平台成为常态时,没有人将int扩展为64位整数,因为:

  • 可移植性:很多代码依赖于int是32位大小。
  • 内存消耗:在大多数情况下,每个内存使用的内存翻一番可能是不合理的,因为在大多数情况下,使用中的内存数量远远less于20亿。

现在,为什么你更喜欢uint32_t uint_fast32_t ? 出于同样的原因,C#和Java总是使用固定大小的整数:程序员不会编写考虑不同types的可能大小的代码,他们会在该平台上编写一个平台和testing代码。 大部分代码隐含地依赖于特定大小的数据types。 这就是为什么在大多数情况下uint32_t是一个更好的select – 它不允许任何含糊的行为。

而且, uint_fast32_t真的是一个大小等于或大于32位的平台上最快的types? 不是真的。 在Windows上考虑GCC for 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指令之外几乎是相同的mov %eax,%eax这意味着将32位值扩展为64位值。

如果仅使用32位值,则不存在这样的问题,但是您可能会使用size_tvariables(数组大小可能是?),而x86_64上的值为64位。 在Linux上uint_fast32_t是8个字节,所以情况是不同的。

许多程序员在需要返回小值时使用int (比如在[-32,32]范围内)。 如果int是平台的本地整数大小,这将是完美的工作,但由于它不是在64位平台上,与平台本机types匹配的另一种types是一个更好的select(除非它经常与其他小整数一起使用)。

基本上,无论什么标准说, uint_fast32_t在某些实现上被打破了。 如果您关心在某些地方生成的其他指令,则应该定义您自己的“本地”整数types。 或者你可以使用size_t来达到这个目的,因为它通常会匹配native大小(我不包括像8086这样的老式和晦涩的平台,只能运行Windows,Linux等的平台)。


显示int另一个符号应该是一个本地整数types是“整数提升规则”。 大多数CPU只能在本地执行操作,所以32位的CPU通常只能执行32位的加法,减法等操作(Intel CPU在这里是个例外)。 只有通过加载和存储指令才能支持其他大小的整数types。 例如,8位值应加载适当的“加载8位有符号”或“加载8位无符号”指令,并在加载后将值扩展到32位。 如果没有整数提升规则,C编译器将不得不为小于本机types的expression式添加更多的代码。 不幸的是,由于编译器现在不得不在某些情况下发出额外的指令(如上所示),所以这不再适用于64位体系结构。

从正确性和易于编码的angular度来看, uint32_tuint_fast32_t有许多优点,特别是由于更精确定义的大小和算术语义,正如很多用户所指出的那样。

可能错过的是那个uint_fast32_t 优点 – 它可以更快 ,从来没有以任何有意义的方式实现。 64位时代(主要是x86-64和Aarch64)占主导地位的大多数64位处理器从32位体系结构发展而来,甚至在64位模式下也具有快速的 32位本机操作。 所以uint_fast32_tuint32_t在这些平台上是一样的。

即使POWER,MIPS64,SPARC等一些“同样运行”的平台只提供64位的ALU操作, 绝大多数有趣的32位操作也可以在64位寄存器上完成:底层的32位将会有理想的结果(所有主stream平台至less允许你加载/存储32位)。 左移是主要的问题,但在许多情况下,甚至可以通过编译器中的值/范围跟踪优化来优化左移。

我怀疑偶尔稍微慢一点的左移,或者32×32 – > 64乘法将超过这些值的内存使用量的两倍 ,除了最晦涩的应用程序。

最后,我会注意到,虽然权衡在很大程度上被称为“内存使用和向量化潜力”(赞成uint32_t )与指令计数/速度(赞成uint_fast32_t ) – 即使这并不清楚。 是的,在某些平台上,您需要针对某些 32位操作的额外说明,但是您还需要保存一些说明,因为:

  • 使用较小的types通常允许编译器通过使用一个64位操作来完成两个32位操作来巧妙地组合相邻的操作。 这种“穷人vector化”的例子并不less见。 例如,创build一个常量struct two32{ uint32_t a, b; } struct two32{ uint32_t a, b; } into rax like two32{1, 2} 可以优化成单个mov rax, 0x20001而64位版本需要两个指令。 原则上,对于相邻的算术运算(相同的操作,不同的操作数)也应该是可能的,但在实践中我没有看到。
  • 较低的“内存使用”通常也会导致较less的指令,即使内存或高速caching足迹不成问题,因为任何types的结构或此types的数组都会被复制,所以您得到的复制数据是每个寄存器降压的两倍。
  • 较小的数据types通常利用更好的现代调用约定,如SysV ABI将数据结构数据高效地打包到寄存器中。 例如,您可以在寄存器rdx:rax返回最多16个字节的结构。 对于返回具有4个uint32_t值(从一个常量初始化)的结构的函数,将转换为

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

    具有4个64位uint_fast32_t的相同结构需要一个寄存器移动和四个存储器来执行相同的操作(调用者可能需要在返回后从内存读取值):

     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 

    类似地,当传递结构参数时,32位值被压缩大约两倍于可用于参数的寄存器,所以它不太可能会耗尽寄存器参数并且不得不溢出到堆栈1

  • 即使您select使用uint_fast32_t作为“速度至关重要”的地方,您通常也会在需要固定大小types的位置使用uint_fast32_t 。 例如,将外部输出的值,来自外部input的值,作为ABI的一部分,作为需要特定布局的结构的一部分,或者因为您巧妙地将uint32_t用于大型聚合值来节省内存占用量。 在uint_fast32_t和uint32_ttypes需要接口的地方,您可能会发现(除了开发复杂性之外),不必要的符号扩展或其他尺寸不匹配的代码。 在许多情况下,编译器在优化这个方面做得不错,但是当混合不同大小的types时,在优化输出中看到这一点并不罕见。

你可以玩一些上面的例子,还有更多关于godbolt的例子。


1要明确的是,将结构紧紧包装在登记簿中的惯例并不总是为较小的价值取胜。 这意味着较小的值可能需要“提取”才可以使用。 例如,一个返回两个结构成员总和的简单函数需要一个mov rax, rdi; shr rax, 32; add edi, eax mov rax, rdi; shr rax, 32; add edi, eax mov rax, rdi; shr rax, 32; add edi, eax而对于64位版本,每个参数都有自己的寄存器,只需要一个addlea 。 不过,如果您接受“紧密包装结构传递”devise总体上是有意义的,那么较小的值将更多地利用此function。

In many cases, when an algorithm works on an array of data, the best way to improve performance is to minimize the number of cache misses. The smaller each element, the more of them can fit into the cache. This is why a lot of code is still written to use 32-bit pointers on 64-bit machines: they don't need anything close to 4 GiB of data, but the cost of making all pointers and offsets need eight bytes instead of four would be substantial.

There are also some ABIs and protocols specified to need exactly 32 bits, for example, IPv4 addresses. That's what uint32_t really means: use exactly 32 bits, regardless of whether that's efficient on the CPU or not. These used to be declared as long or unsigned long , which caused a lot of problems during the 64-bit transition. If you just need an unsigned type that holds numbers up to at least 2³²-1, that's been the definition of unsigned long since the first C standard came out. In practice, though, enough old code assumed that a long could hold any pointer or file offset or timestamp, and enough old code assumed that it was exactly 32 bits wide, that compilers can't necessarily make long the same as int_fast32_t without breaking too much stuff.

In theory, it would be more future-proof for a program to use uint_least32_t , and maybe even load uint_least32_t elements into a uint_fast32_t variable for calculations. An implementation that had no uint32_t type at all could even declare itself in formal compliance with the standard! (It just wouldn't be able to compile many existing programs.) In practice, there's no architecture any more where int , uint32_t , and uint_least32_t are not the same, and no advantage, currently , to the performance of uint_fast32_t . So why overcomplicate things?

Yet look at the reason all the 32_t types needed to exist when we already had long , and you'll see that those assumptions have blown up in our faces before. Your code might well end up running someday on a machine where exact-width 32-bit calculations are slower than the native word size, and you would have been better off using uint_least32_t for storage and uint_fast32_t for calculation religiously. Or if you'll cross that bridge when you get to it and just want something simple, there's unsigned long .

For practical purposes, uint_fast32_t is completely useless. It's defined incorrectly on the most widespread platform (x86_64), and doesn't really offer any advantages anywhere unless you have a very low-quality compiler. Conceptually, it never makes sense to use the "fast" types in data structures/arrays – any savings you get from the type being more efficient to operate on will be dwarfed by the cost (cache misses, etc.) of increasing the size of your working data set. And for individual local variables (loop counters, temps, etc.) a non-toy compiler can usually just work with a larger type in the generated code if that's more efficient, and only truncate to the nominal size when necessary for correctness (and with signed types, it's never necessary).

The one variant that is theoretically useful is uint_least32_t , for when you need to be able to store any 32-bit value, but want to be portable to machines that lack an exact-size 32-bit type. Practically, speaking, however, that's not something you need to worry about.

 typedef enum { lower_bound = 13, upper_bound = 4294967295 } fast_int_that_you_need; 

Any compiler should be able to choose the smallest bit size.