为什么首选uint32_t而不是uint_fast32_t?

时间:2017-10-26 16:46:34

标签: c++ c int

uint32_t似乎比uint_fast32_t更为普遍(我知道这是轶事证据)。但这对我来说似乎是违反直觉的。

几乎总是当我看到一个实现使用uint32_t时,它真正想要的是一个整数,它可以容纳最多4,294,967,295的值(通常在65,535和4,294,967,295之间的下限)。

然后使用uint32_t似乎很奇怪,因为不需要'完全32位' 保证,'最快可用> = 32位' uint_fast32_t的保证似乎是正确的想法。此外, 虽然它通常被实施,但uint32_t实际上并不能保证存在。

那么,uint32_t为什么会首选?它只是更为人所知或者是否存在技术优势?

11 个答案:

答案 0 :(得分:75)

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_tuint32_t上的算术会通过常规整数提升,并且如果没有,它将保持为uint32_t。这可能会导致错误。没有什么是完美的。

答案 1 :(得分:31)

  

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

注意:错误命名的uint32_fast_t应为uint_fast32_t

uint32_t的规格比uint_fast32_t更严格,因此功能更加一致。

uint32_t专业人士:

  • 各种算法指定此类型。 IMO - 使用的最佳理由。
  • 确切的宽度和范围。
  • 此类阵列不会浪费。
  • 无符号整数数学及其溢出更具可预测性。
  • 在其他语言的范围和数学方面进行更紧密的匹配' 32位类型。
  • 从不填充。

uint32_t缺点:

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

uint_fast32_t专业人士:

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

uint_fast32_t缺点:

  • 范围只知之甚少。例如,它可以是64位类型。
  • 此类数组可能在内存中浪费。
  • 所有答案(我的第一个),帖子和评论都使用了错误的名称uint32_fast_t。看起来很多人不需要和使用这种类型。我们甚至没有使用正确的名字!
  • 填充可能 - (罕见)。
  • 在特定情况下,"最快"类型可能真的是另一种类型。所以uint_fast32_t只是一阶逼近。

最后,最好的取决于编码目标。除非编码具有非常广泛的可移植性或某些特定的性能函数,否则请使用uint32_t

使用这些类型时还有另一个问题:它们的排名与int/unsigned

相比较

据推测uint_fastN_t至少是unsigned的等级。这没有指定,但是具有一定的可测试条件。

因此,uintN_tuint_fastN_t更可能缩小unsigned。这意味着在涉及可移植性时,使用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 :(得分:25)

  

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

愚蠢的回答:

  • 没有标准类型uint32_fast_t,拼写正确为uint_fast32_t

实际答案:

  • 许多人实际上使用uint32_tint32_t来表示其精确的语义,正好是32位,无符号环绕算术(uint32_t)或2的补码表示({{1 }})。 int32_t类型可能更大,因此不适合存储到二进制文件,在打包数组和结构中使用,或通过网络发送。此外,它们甚至可能更快。

务实的答案:

  • 许多人只是不知道(或根本就不关心)xxx_fast32_t,如评论和答案所示,并且可能假设普通uint_fast32_t具有相同的语义虽然目前许多架构仍然有16位unsigned int,但一些罕见的博物馆样本还有其他奇怪的int大小小于32。
UX回答:

  • 虽然可能比int更快,但uint32_t的使用速度较慢:输入时间较长,尤其是考虑在C文档中查找拼写和语义; - )

优雅很重要,(显然基于意见):

  • uint_fast32_t看起来很糟糕,很多程序员都喜欢定义自己的uint32_tu32类型......从这个角度来看,uint32看起来很笨拙,无法修复。毫不奇怪,它与朋友uint_fast32_t等坐在板凳上。

答案 3 :(得分:8)

一个原因是unsigned int已经&#34;最快&#34;无需任何特殊的typedef或需要包含某些东西。因此,如果您需要快速,只需使用基本的intunsigned int类型 虽然标准没有明确保证它是最快的,但间接通过声明&#34来实现这一点;简单的int具有执行环境架构所建议的自然大小&#34; < / em>在3.9.1中。换句话说,int(或其未签名的对应物)是处理器最熟悉的。

当然,您现在还不知道unsigned int的大小。你只知道至少short一样大(我似乎记得short必须至少为16位,但我无法找到现在在标准!)。通常它只是简单的4字节,但它理论上可以更大,或者在极端情况下,甚至更小(虽然我个人从未遇到过这种情况的架构,甚至没有在20世纪80年代的8位计算机......可能是一些微控制器,谁知道结果我患有痴呆症,int当时非常清楚16位。

C ++标准并不打算指定<cstdint>类型是什么或它们保证什么,它只是提及&#34;与C&#34;相同。

uint32_t,根据C标准,保证您正好得到32位。没有任何不同,没有更少,没有填充位。有时这正是你所需要的,因此它非常有价值。

uint_least32_t保证无论大小是多少,它都不能小于32位(但它可能更大)。有时候,但是很少比精确的健康或者不关心更多,这就是你想要的。

最后,uint_fast32_t在我看来有点多余,除了意图文档目的。 C标准声明&#34;指定一个通常最快的整数类型&#34; (注意单词&#34;通常&#34;)并明确提到它不需要对所有人来说都是最快的目的。换句话说,uint_fast32_tuint_least32_t大致相同,通常也是最快的,只是没有给出保证(但不保证任何一种方式)。

由于大部分时间你都不关心确切的尺寸,或者你想要完全 32(或64,有时是16)位,并且因为&#34; don&# 39;关心&#34; unsigned int类型无论如何都是最快的,这解释了为什么uint_fast32_t不经常使用。

答案 4 :(得分:6)

我没有看到uint32_t用于范围的证据。相反,在使用 uint32_t的大部分时间里,它是在各种算法中保存4个八位字节的数据,保证环绕和移位语义!

使用uint32_t代替uint_fast32_t还有其他原因:通常是它会提供稳定的ABI。另外,可以准确地知道存储器使用。这大大抵消了来自uint_fast32_t的速度增益,只要该类型与uint32_t的类型不同。

对于值&lt; 65536,已经有一个方便的类型,它被称为unsigned intunsigned short也需要至少具有该范围,但unsigned int具有本机字大小)对于值&lt ; 4294967296,还有一个名为unsigned long

最后,人们不使用uint_fast32_t,因为打字很烦人且容易输入错误:D

答案 5 :(得分:5)

有几个原因。

  1. 很多人都不知道“快速”的情况。类型存在。
  2. 输入更详细。
  3. 当您不知道该类型的实际大小时,很难推断您的计划行为。
  4. 标准实际上并没有最快地确定,也不是真正最快的类型可能非常依赖于上下文。
  5. 我没有看到任何证据表明平台开发人员在定义平台时会考虑这些类型的大小。例如在x86-64 Linux上,&#34; fast&#34;尽管x86-64具有对32位值进行快速操作的硬件支持,但所有类型都是64位。
  6. 总结一下&#34; fast&#34;类型是毫无价值的垃圾。如果您确实需要确定给定应用程序的最快类型,则需要在编译器上对代码进行基准测试。

答案 6 :(得分:5)

从正确性和编码简易性的角度来看,uint32_tuint_fast32_t有许多优点,特别是因为上面许多用户指出的更精确定义的大小和算术语义。

可能遗漏的是一个uint_fast32_t的优势 - 它可以更快,从未以任何有意义的方式实现。大多数主导64位时代的64位处理器(主要是x86-64和Aarch64)从32位架构发展而来,即使在64位模式下也具有快速 32位本机操作。因此uint_fast32_t与这些平台上的uint32_t相同。

即使某些&#34;也跑了&#34;像POWER,MIPS64,SPARC这样的平台只提供64位ALU操作,有趣的32位操作的vast majority可以在64位寄存器上完成:底部的32位将具有所需的结果(和所有主流平台至少允许你加载/存储32位)。左移是主要的问题,但在许多情况下,甚至可以通过编译器中的值/范围跟踪优化来优化左移。

我怀疑偶尔会略微放慢左移或32x32 - &gt;除了最模糊的应用之外,64乘法将超过 double 这些值的内存使用。

最后,我会注意到,虽然这种权衡主要被描述为&#34;记忆使用和矢量化潜力&#34; (赞成uint32_t)与指令数/速度(赞成uint_fast32_t) - 即使这对我来说也不清楚。是的,在某些平台上,您需要有关某些 32位操作的其他说明,但您还要保存某些指令,因为:

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

    uint_fast32_t

    4个64位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 的相同结构需要一个寄存器移动而四个存储需要执行相同的操作(并且调用者可能需要从内存中读取值)返回后):

    uint_fast32_t

    类似地,当传递结构参数时,32位值被压缩大约两倍于可用于参数的寄存器,因此它使得您不太可能用完寄存器参数并且必须溢出到堆 1

  • 即使您选择将uint32_t用于&#34;速度很重要的地方&#34;您经常也有需要固定尺寸类型的地方。例如,当传递外部输出的值,来自外部输入,作为ABI的一部分,作为需要特定布局的结构的一部分,或者因为您巧妙地使用uint_fast32_t来进行大量值的聚合以节省内存时脚印。在您的mov rax, rdi; shr rax, 32; add edi, eax和``uint32_t`类型需要接口的地方,您可能会发现(除了开发复杂性),不必要的符号扩展或其他与大小不匹配相关的代码。在许多情况下,编译器可以很好地优化它,但在混合不同大小的类型时,在优化输出中看到这一点仍然并不罕见。

您可以使用上面的一些示例以及更多on godbolt

1 为了清楚起见,将结构紧密地打包到寄存器中的惯例并不总是对较小值的明显胜利。它确实意味着可能必须提取较小的值&#34;在他们可以使用之前。例如,将两个结构成员的总和一起返回的简单函数需要add,而对于64位版本,每个参数都有自己的寄存器,只需要一个lea或{{1} }。如果你接受&#34;在通过时紧紧包装结构&#34;设计总体上有意义,然后较小的值将更有利于此功能。

答案 7 :(得分:4)

根据我的理解,int最初应该是一个“本机”整数类型,并且额外保证它应该至少为16位大小 - 当时被认为是“合理”的大小。

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

  • 现代Windows在所有平台上使用32位int
  • POSIX保证int至少为32位。
  • C#,Java的类型为int,保证正好是32位。

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

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

现在,您为什么更喜欢uint32_tuint_fast32_t?出于同样的原因,语言,C#和Java总是使用固定大小的整数:程序员不会编写考虑不同类型的可能大小的代码,他们为一个平台编写代码并在该平台上测试代码。大多数代码隐含地取决于数据类型的特定大小。这就是为什么uint32_t对大多数情况来说是更好的选择 - 它不允许任何关于其行为的歧义。

此外,uint_fast32_t在大小等于或大于32位的平台上真的是最快的类型吗?并不是的。在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指令,这意味着将32位值扩展为64位值。

如果你只使用32位值,就没有这样的问题,但你可能会使用那些size_t变量(数组大小可能是?),而x86_64则是64位。在Linux上uint_fast32_t是8个字节,因此情况不同。

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

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

显示int的另一个标志应该是原生整数类型是“整数提升规则”。大多数CPU只能对本机执行操作,因此32位CPU通常只能进行32位加法,减法等(英特尔CPU在这里是个例外)。仅通过加载和存储指令支持其他大小的整数类型。例如,8位值应加载适当的“加载8位有符号”或“加载8位无符号”指令,并在加载后将值扩展为32位。如果没有整数提升规则,C编译器必须为使用小于本机类型的类型的表达式添加更多代码。遗憾的是,由于编译器现在必须在某些情况下发出额外的指令(如上所示),因此不再适用于64位架构。

答案 8 :(得分:4)

出于实际目的,uint_fast32_t完全没用。它在最广泛的平台(x86_64)上定义不正确,并且除非你的编译器质量非常低,否则它在任何地方都没有任何优势。从概念上讲,使用&#34; fast&#34;永远都没有意义。数据结构/数组中的类型 - 通过增加工作数据集大小的成本(缓存未命中等),使得从更高效的操作类型中获得的任何节省都会相形见绌。对于单个局部变量(循环计数器,临时值等),非玩具编译器通常只能在生成的代码中使用更大的类型,如果它更有效,并且只在必要时截断到标称大小。正确性(以及签名类型,它永远不必要)。

理论上有用的一个变体是uint_least32_t,因为当您需要能够存储任何32位值,但希望可移植到缺少32位精确大小的机器时。但实际上,说起来并不是你需要担心的事情。

答案 9 :(得分:2)

在许多情况下,当算法在数据数组上工作时,提高性能的最佳方法是最小化缓存未命中数。每个元素越小,它们就越适合缓存。这就是为什么仍有大量代码在64位机器上使用32位指针的原因:它们不需要接近4 GiB的数据,但是所有指针和偏移的成本需要8个字节而不是4个会很重要的。

还有一些ABI和协议指定需要正好32位,例如IPv4地址。这就是uint32_t的真正含义:使用完全 32位,无论它是否在CPU上有效。这些曾经被声明为longunsigned long,这在64位转换期间引起了很多问题。如果你只需要一个保持数字至少为2³²-1的无符号类型,那么自从第一个C标准出来以来,这就是unsigned long的定义。但实际上,足够的旧代码假设long可以保存任何指针或文件偏移或时间戳,并且足够的旧代码假设它正好是32位宽,编译器不一定能{{1}与long相同而不会破坏太多东西。

理论上,对于程序使用int_fast32_t,甚至可能将uint_least32_t个元素加载到uint_least32_t变量进行计算,将会更具前瞻性。根本没有uint_fast32_t类型的实现甚至可以声明自己正式符合标准! (它只是无法编译许多现有的程序。)实际上,uint32_tintuint32_t不同,没有任何优势架构,没有优势,目前,表现为uint_least32_t。那为什么要复杂化呢?

然而,当我们已经拥有uint_fast32_t时,看看所有32_t类型需要存在的原因,你会发现这些假设在我们面前已经被炸毁了。你的代码最终可能会在一台机器上运行,而机器的精确宽度32位计算速度比原始字大小慢,你最好使用long进行存储,uint_least32_t进行计算宗教。或者,当你到达那座桥时,如果你只想要一些简单的东西,那就是uint_fast32_t

答案 10 :(得分:1)

直接给出答案:我认为uint32_tuint_fast32_t上使用uint_least32_t的真正原因仅仅是因为它更易于输入,而且由于较短,更易于阅读:如果您使用某些类型构造结构,而其中一些是uint_fast32_t或类似结构,则通常很难将它们与intbool或其他类型的结构很好地对齐C,它们很短(例如:charcharacter)。我当然不能用硬数据支持这一点,但是其他答案也只能猜测原因。

由于技术上的原因,我更喜欢uint32_t,所以我不认为有-当您完全需要一个精确的32位无符号整数时,此类型就是您的唯一的标准化选择。在几乎所有其他情况下,其他变体在技术上都是更可取的-特别是,uint_fast32_t如果您担心速度,uint_least32_t如果您担心存储空间。在上述两种情况下使用uint32_t可能会导致无法编译,因为该类型不需要存在。

实际上,uint32_t和相关类型存在于所有当前平台上,除了一些非常罕见的(当今)DSP或笑话实现之外,因此使用确切类型几乎没有实际风险。同样,虽然您可能会遇到固定宽度类型的速度损失,但它们(在现代cpus上)不再瘫痪了。

我认为这是为什么较短的类型在大多数情况下会由于程序员的懒惰而胜出的原因。