为什么首选uint32_t而不是uint_fast32_t?

Joo*_*ost 79 c c++ int

它似乎uint32_tuint_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受青睐?是简单地知道还是比其他技术有优势?

Yak*_*ont 76

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

uint_fast32_t 相比之下,它几乎不能保证它在不同系统上的表现.

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

当写你的代码,你甚至可以不进入到一个uint_fast32_t系统,是不是在大小32位.

uint32_t 将不会有不同的工作(除了脚注).

正确性比速度更重要.因此,过早的正确性比过早优化更好.

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

最后,uint_fast32_t当您将其存储任意长度的时间或实例数量可能比uint32缓存大小问题和内存带宽更慢.今天的计算机通常受内存限制,而不是CPU绑定,并且uint_fast32_t可以更快地隔离,但在考虑内存开销之后不会.


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

  • "uint32_t保证在任何支持它的平台上具有相同的属性." 当`unsigned`比`uint32_t`更宽,然后在一个平台上的`uint32_t`经历通常的整数促销而在另一个平台上没有通过整数促销时,有一个角落问题.然而,使用`uint32_t`,这些整数数学问题显着减少. (15认同)
  • @chux一个可以在乘法时引起UB的角点情况,因为提升更喜欢*signed int*和有符号整数溢出是UB. (2认同)
  • 尽管这个答案就目前而言是正确的,但它在很大程度上忽略了关键细节。简而言之,uint32_t用于表示类型的机器表示的确切细节,而uint_fast32_t用于表示计算速度最重要的(无符号数和最小范围)以及表示细节。是不必要的。还有[uint_least32_t],其中(无)符号性和最小范围最重要,紧凑性比速度更重要,并且精确的表示不是必需的。 (2认同)

chu*_*ica 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位,其它).
    例如:平台使用非2的补码.老2200

uint_fast32_t 优点:

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

uint_fast32_t 缺点:

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

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


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

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

因此,uintN_t更可能是更uint_fastN_tunsigned.这意味着使用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类型被严格指定并导致可移植代码.

  • 有些东西最快,其他东西慢.**当您考虑数组与需要零扩展时,对于"什么是最快的整数大小"没有一个通用的答案.**在x86-64 System V ABI中,`uint32_fast_t`是64-位类型,因此它可以保存偶尔的符号扩展,并且当使用64位整数或指针时,允许使用`imul rax,[mem]`而不是单独的零扩展加载指令.但是,这就是你获得的缓存占用空间和额外代码大小(REX前缀为所有内容)的价格. (4认同)
  • *支持32位范围的最快型*=>真的吗?这是RAM以CPU速度运行的时间遗留物,现在平衡在PC上发生了巨大变化,因此(1)从内存中拉出32位整数的速度是拉动64位整数和(2)矢量化指令的两倍在32位整数上,它们的容量是64位整数的两倍.它真的还是最快的吗? (2认同)
  • 我希望作为所有C和C++应用程序的加权平均值,在x86上制作`uint32_fast_t`是一个糟糕的选择.更快的操作很少,而且它们发生的好处大多是微不足道的:@ peeCordes提到的`imul rax,[mem]`案例的差异是_very_,_very_ small:融合中的单个uop域和未融合域中的零.在大多数有趣的场景中,它甚至不会添加一个循环.平衡了_double_内存使用,以及更糟糕的矢量化,很难看到它经常获胜. (2认同)
  • @PeterCordes - 有趣但也很糟糕:)。它会使 `fast_t` 变成更糟糕的 `int`:它不仅在不同平台上有不同的大小,而且根据优化决策和不同文件中的不同大小,它会有不同的大小!实际上,我认为即使对整个程序进行优化,它也无法工作:C 和 C++ 中的大小是固定的,因此 `sizeof(uint32_fast_t)` 或任何直接确定它的东西都必须始终返回相同的值,所以它会编译器_非常_难以进行这样的转换。 (2认同)

chq*_*lie 25

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

傻回答:

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

实际答案:

  • 许多人实际上使用uint32_t或者使用int32_t精确的语义,正好32位,无符号环绕arithmetic(uint32_t)或2的补码表示(int32_t).所述xxx_fast32_t类型可以是较大的,因此不宜存储二进制文件,在堆积阵列和结构使用,或发送在网络上.此外,他们甚至可能不会更快.

务实的答案:

  • 许多人只是不知道(或根本就不关心)uint_fast32_t,正如评论和答案中所证明的那样,并且可能假设unsigned int具有相同的语义,尽管许多当前的架构仍然具有16位int且一些罕见的博物馆样本具有其他奇怪的int大小小于32.

UX回答:

  • 虽然可能比快uint32_t,uint_fast32_t是慢的使用方法:它需要更长的时间来输入,尤其是占查找拼写和语法的C文档中;-)

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

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


Dam*_*mon 8

一个原因是,unsigned int已经"最快",不需要任何特殊的typedef或需要包含某些东西.因此,如果您需要快速,只需使用基础intunsigned int类型.
虽然标准没有明确保证它是最快的,但它通过在3.9.1中说明"普通内容具有执行环境的体系结构所建议的自然大小"间接地这样做.换句话说,(或其未签名的对应物)是处理器最舒适的.int

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

C++标准并不打算指定<cstdint>类型是什么或它们保证什么,它只是提到"与C中相同".

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

uint_least32_t保证无论大小如何,它都不能小于32位(但它可能会更大).有时,但是比精确的或者"不关心"更少,这就是你想要的.

最后,uint_fast32_t在我看来,除了意图文档目的之外,有点多余.C标准规定"指定通常最快的整数类型"(注意"通常"一词)并明确提到它不需要为所有目的最快.换句话说,uint_fast32_t它几乎是一样的uint_least32_t,通常也是最快的,只是没有给出保证(但不保证任何一种方式).

由于大多数的时候你要么不关心的确切大小,或者您想正是 32(或64,有时16)位,因为"不关心" unsigned int的类型是最快的,无论如何,这解释了为什么uint_fast32_t是不是这样经常使用.

  • 我很惊讶你不记得8位处理器上的16位`int`,我记不起那些使用过大的东西.如果内存服务,分段x86架构的编译器也使用16位`int`. (3认同)

Ant*_*ala 6

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

还有其他原因可以uint32_t代替uint_fast32_t:通常它会提供稳定的ABI.另外,可以准确地知道存储器使用.无论速度增益是什么uint_fast32_t,只要该类型与其不同,这就非常有用uint32_t.

对于值<65536,已经有一个方便的类型,它被调用unsigned int(unsigned short需要至少具有该范围,但是unsigned int具有本机字大小)对于值<4294967296,还有另一个被调用unsigned long.


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


plu*_*ash 5

几个原因.

  1. 很多人不知道存在"快速"类型.
  2. 打字更加冗长.
  3. 当您不知道类型的实际大小时,更难以推断您的程序行为.
  4. 该标准实际上并不是最快的,也不是真正最快的类型可能非常依赖于上下文.
  5. 我没有看到平台开发人员在定义平台时对这些类型的大小进行任何考虑的证据.例如,在x86-64 Linux上,"快速"类型都是64位,即使x86-64具有对32位值的快速操作的硬件支持.

总之,"快速"类型是毫无价值的垃圾.如果您确实需要确定给定应用程序的最快类型,则需要在编译器上对代码进行基准测试.


小智 5

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

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

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

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

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

现在,你为什么uint32_t愿意uint_fast32_t?出于同样的原因,C# 和 Java 语言始终使用固定大小的整数:程序员编写代码时不会考虑不同类型的可能大小,他们会为一个平台编写代码并在该平台上测试代码。大多数代码隐式依赖于数据类型的特定大小。这就是为什么uint32_t在大多数情况下这是一个更好的选择 - 它不允许其行为有任何歧义。

而且,uint_fast32_t大小真的等于或大于 32 位的平台上最快的类型吗?并不真地。考虑 GCC 在 Windows 上为 x86_64 编译的代码:

extern uint64_t get(void);

uint64_t sum(uint64_t value)
{
    return value + get();
}
Run Code Online (Sandbox Code Playgroud)

生成的程序集如下所示:

push   %rbx
sub    $0x20,%rsp
mov    %rcx,%rbx
callq  d <sum+0xd>
add    %rbx,%rax
add    $0x20,%rsp
pop    %rbx
retq
Run Code Online (Sandbox Code Playgroud)

现在,如果将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
Run Code Online (Sandbox Code Playgroud)

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 位加法、减法等(Intel CPU 是一个例外)。其他大小的整数类型仅通过加载和存储指令支持。例如,应该使用适当的“加载8位有符号”或“加载8位无符号”指令加载8位值,并在加载后将值扩展为32位。如果没有整数提升规则,C 编译器将不得不为使用小于本机类型的类型的表达式添加更多代码。不幸的是,这对于 64 位架构来说不再适用,因为编译器现在必须在某些情况下发出额外的指令(如上所示)。

  • 关于“没有人将 int 扩展为 64 位整数,因为”和“不幸的是,这在 64 位架构中不再适用”的想法是_非常好的观点_。公平地说,关于“最快”和比较汇编代码:在这种情况下,第二个代码片段似乎因其额外指令而较慢,但代码长度和速度有时并没有很好的相关性。更强大的比较将报告运行时间 - 但这并不容易做到。 (2认同)

Bee*_*ope 5

从正确性和编码的容易性的观点来看,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位操作的其他说明,但您还需要保存一些指令,因为:

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

    ret_constant32():
        movabs  rax, 8589934593
        movabs  rdx, 17179869187
        ret
    
    Run Code Online (Sandbox Code Playgroud)

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

    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
    
    Run Code Online (Sandbox Code Playgroud)

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

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

您可以使用上面的一些示例以及更多关于godbolt的内容.


1需要明确的是,将结构紧密地包装到寄存器中的惯例并不总是对较小值的明显胜利.它确实意味着在可以使用之前可能必须"提取"较小的值.例如,一个简单的函数将两个结构成员的总和一起返回需要一段mov rax, rdi; shr rax, 32; add edi, eax时间,对于64位版本,每个参数都有自己的寄存器,只需要一个add或者lea.如果你接受"通过时紧密包装结构"设计总体上是有意义的,那么较小的值将更多地利用这个功能.

  • 哦,对,我在上面读到你关于SysV ABI的评论,但正如你后来指出的那样可能是一个不同的组/文件决定了它 - 但我想一旦发生这种情况,它几乎是一成不变的.我认为即使在没有良好的32位操作支持的平台上,纯循环计数/指令计数也支持更大类型甚至忽略内存占用效应和矢量化甚至是值得怀疑的 - 因为仍然存在编译器可以更好地优化较小类型的情况.我在上面添加了一些例子.@PeterCordes (2认同)

归档时间:

查看次数:

8183 次

最近记录:

6 年,4 月 前