【问题标题】:Why would uint32_t be preferred rather than uint_fast32_t?为什么首选 uint32_t 而不是 uint_fast32_t?
【发布时间】:2018-04-08 03:40:01
【问题描述】:

似乎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?它只是更广为人知还是有技术优势?

【问题讨论】:

  • 简单的答案,也许他们需要一个正好是 32 位的整数?
  • 首先我听说uint32_fast_t,如果我理解正确的话,它是至少 32位(意味着它可能更多?听起来误导我) .我目前在我的项目中使用uint32_t 和朋友,因为我正在打包这些数据并通过网络发送它,我希望发送者和接收者确切地知道这些字段有多大。听起来这可能不是最强大的解决方案,因为平台可能无法实现uint32_t,但我的所有人显然都这样做了,所以我对我正在做的事情很好。
  • @yano:对于网络,你还应该关心字节顺序/字节序——uint32_t 不会给你这个(遗憾的是没有uint32_t_beuint32_t_le,这将更适合uint32_t 目前是最佳选择的几乎所有可能的情况)。
  • @Brendan - 关于 _be 和 _le,htonl() 和 ntohl() 会提供相同的功能吗?
  • @Brendan 这是一个非常重量级的对象,可以隐藏在标准 int 中,所有这些都是原始类型。我原则上同意你的观点,这应该在某个地方的标准中处理,但我认为这可能不是这个地方

标签: c++ c int


【解决方案1】:

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。这可能会导致错误。没有什么是完美的。

【讨论】:

  • "uint32_t 保证在任何支持它的平台上都具有相同的属性。"当unsigneduint32_t 宽,然后uint32_t 在一个平台上经历通常的整数促销而在另一个平台上则没有时,就会出现角落问题。然而,有了uint32_t,这些整数数学问题就大大减少了。
  • @chux 在乘法时可能会导致 UB,因为提升更喜欢 signed int 并且有符号整数溢出是 UB。
  • 虽然这个答案是正确的,但它非常淡化了关键细节。简而言之,uint32_t 用于该类型的机器表示的确切细节很重要的地方,而uint_fast32_t 用于计算速度最重要的地方,(无)符号和最小范围很重要,以及表示的细节是非必要的。还有uint_least32_t,表示(无)符号和最小范围最重要,紧凑性比速度更重要,精确表示不是必需的。
  • @JohnBollinger 这一切都很好,但没有在实现超过 1 个变体的实际硬件上进行测试,可变大小类型是一个陷阱。人们使用uint32_t 而不是其他类型的原因是他们通常没有这样的硬件来进行测试。 (int32_t 在较小程度上也是如此,甚至intshort 也是如此)。
  • 一个极端情况的例子:让unsigned short==uint32_tint==int48_t。如果您计算类似(uint32_t)0xFFFFFFFF * (uint32_t)0xFFFFFFFF 的内容,则操作数将提升为signed int 并触发有符号整数溢出,这是未定义的行为。 See this question.
【解决方案2】:

为什么很多人使用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-bit、others)。
    例如:使用非 2 补码的平台。 old 2200

uint_fast32_t 专业人士:

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

uint_fast32_t 缺点:

  • 范围仅知之甚少。例如,它可以是 64 位类型。
  • 这种类型的数组可能会浪费内存。
  • 所有答案(一开始也是我的),帖子和 cmets 使用了错误的名称 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 类型被严格指定并导致可移植代码。

【讨论】:

  • 支持 32 位范围的最快类型 => 真的吗?这是 RAM 以 CPU 速度运行的时代的遗迹,如今,PC 上的平衡已经发生了巨大变化,因此(1)从内存中提取 32 位整数的速度是提取 64 位整数的两倍和(2)矢量化指令32 位整数的运算量是 64 位整数的两倍。真的还是最快的吗?
  • 有些事情最快,有些事情比较慢。 当您考虑数组与需要零扩展时,对于“什么是最快的整数大小”,没有万能的答案。在 x86-64 System V ABI 中,uint32_fast_t 是一个 64 位类型,因此它节省了偶尔的符号扩展,并在使用 64 位整数或指针时允许 imul rax, [mem] 而不是单独的零扩展加载指令。但这就是您以双倍缓存占用空间和额外代码大小(REX 前缀在所有内容上)的代价所获得的全部。
  • 此外,在大多数 x86 CPU 上,64 位除法比 32 位除法慢得多,并且一些(如 Bulldozer-family、Atom 和 Silvermont)的 64 位乘法比 32 慢。 -family 也有较慢的 64 位 popcnt。请记住,仅将这种类型用于 32 位值是安全的,因为它在其他架构上更小,因此您无需为此付出任何代价。
  • 我希望作为所有 C 和 C++ 应用程序的加权平均值,在 x86 上创建 uint32_fast_t 是一个糟糕的选择。更快的操作很少,而且它们发生时的好处大多是微不足道的:@PeterCordes 提到的imul rax, [mem] 案例的差异是veryvery small:融合域中的单个 uop,未融合域中的零。在大多数有趣的场景中,它甚至不会添加一个循环。平衡这与 内存使用和更差的矢量化相比,很难看到它经常获胜。
  • @PeterCordes - 有趣但也很糟糕:)。这会让fast_t 变得更糟int:它不仅在不同平台上有不同的大小,而且根据优化决策和不同文件中的不同大小,它也会有不同的大小!实际上,我认为即使对整个程序进行优化也无法正常工作:C 和 C++ 中的大小是固定的,因此 sizeof(uint32_fast_t) 或任何直接决定它的东西都必须始终返回相同的值,所以它会 编译器很难进行这样的转换。
【解决方案3】:

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

愚蠢的回答:

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

实用答案:

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

务实的回答:

  • 许多人只是不知道(或根本不关心)uint_fast32_t,正如 cmets 和 answers 中所展示的那样,并且可能假设普通的 unsigned int 具有相同的语义,尽管许多当前架构仍然有 16 -bit ints 和一些稀有的博物馆样本有其他奇怪的小于 32 的 int 大小。

用户体验答案:

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

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

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

【讨论】:

  • +1 用于用户体验。我猜它比std::reference_wrapper 好,但有时我想知道标准委员会是否真的希望使用它标准化的类型......
【解决方案4】:

一个原因是unsigned int 已经是“最快的”,不需要任何特殊的类型定义或包含一些东西。因此,如果您需要快速,只需使用基本的 intunsigned int 类型。
虽然该标准没有明确保证它是最快的,但它间接通过在 3.9 中声明“Plain ints 具有执行环境架构所建议的自然大小”来做到这一点.1。换句话说,int(或其未签名的对应物)是处理器最熟悉的。

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

C++ 标准并不费心指定 <cstdint> 类型是什么或它们保证什么,它只是提到“与 C 中的相同”。

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

uint_least32_t 保证无论大小是多少,它都不能小于 32 位(但也可以更大)。有时,但比确切的 witdh 或“不在乎”要少得多,这就是您想要的。

最后,在我看来,uint_fast32_t 有点多余,除了意图文档。 C 标准规定“指定一个通常最快的整数类型”(注意“通常”这个词)并明确提到它不需要在所有用途中都是最快的。换句话说,uint_fast32_tuint_least32_t 差不多,通常也是最快的,只是不保证(但不保证任何一种方式)。

因为大多数时候你要么不关心确切的大小,要么确切地想要 32(或 64,有时 16)位,并且因为“不关心”@987654335 @ 类型无论如何是最快的,这解释了为什么 uint_fast32_t 不那么常用。

【讨论】:

  • 我很惊讶你不记得 8 位处理器上的 16 位 int,我不记得那些日子使用更大的处理器。如果没有记忆,分段 x86 架构的编译器也使用 16 位 int
  • @MarkRansom:哇,你是对的。我非常确信int 在 68000 上是 32 位(我想到的,作为一个例子)。不是……
  • int 在过去是最快的类型,最小宽度为 16 位(这就是 C 具有整数提升规则的原因),但在今天的 64 位架构中,这不是真的了。例如,8 字节整数比 x86_64 位上的 4 字节整数快,因为对于 4 字节整数,编译器必须插入额外的指令,将 4 字节值扩展为 8 字节值,然后再与其他 8 字节值进行比较。
  • "unsigned int" 在 x64 上不一定是最快的。奇怪的事情发生了。
  • 另一种常见的情况是long,由于历史原因,需要是32位的,而int现在要求不超过long,所以int可能需要即使 64 位更快,也要保持 32 位。
【解决方案5】:

几个原因。

  1. 许多人不知道存在“快速”类型。
  2. 打字更冗长。
  3. 如果您不知道类型的实际大小,就很难推断您的程序行为。
  4. 该标准实际上并没有确定最快,实际上什么类型实际上最快也不能真正取决于上下文。
  5. 我没有看到任何证据表明平台开发人员在定义他们的平台时会考虑这些类型的大小。例如,在 x86-64 Linux 上,“快速”类型都是 64 位的,即使 x86-64 具有对 32 位值的快速操作的硬件支持。

总而言之,“快速”类型是毫无价值的垃圾。如果您确实需要确定对于给定应用程序哪种类型最快,您需要在编译器上对代码进行基准测试。

【讨论】:

  • 从历史上看,有些处理器具有 32 位和/或 64 位内存访问指令,但没有 8 位和 16 位。所以 int_fast{8,16}_t 在 20 多年前还不是完全愚蠢的。 AFAIK 最后一个这样的主流处理器是原始的 DEC Alpha 21064(第二代 21164 得到了改进)。可能仍然存在嵌入式 DSP 或仅进行字访问的任何东西,但可移植性通常不是这类事情的一个大问题,所以我不明白你为什么要货物崇拜 fast_t on那些。还有手工打造的 Cray“一切都是 64 位”机器。
  • 类别 1b:许多人并不关心“快速”类型的存在。那是我的类别。
  • 第 6 类:许多人不相信“快速”类型是最快的。我属于那个类别。
【解决方案6】:

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

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

即使 POWER、MIPS64、SPARC 等一些“同时运行”的平台仅提供 64 位 ALU 操作,有趣的 32 位操作vast majority 也可以在 64 位寄存器上完成:底部32 位将有理想的结果(所有主流平台至少允许您加载/存储 32 位)。左移是主要的问题之一,但即使在许多情况下也可以通过编译器中的值/范围跟踪优化来优化。

我怀疑偶尔稍慢的左移或 32x32 -> 64 乘法会超过 这些值的内存使用量,除了最晦涩的应用程序之外。

最后,我要指出,虽然权衡主要被描述为“内存使用和矢量化潜力”(有利于 uint32_t)与指令数/速度(有利于 uint_fast32_t) - 即使这样也不是我不清楚。是的,在某些平台上,一些 32 位操作需要额外的说明,但您还需要保存一些说明,因为:

  • 使用较小的类型通常允许编译器通过使用一个 64 位操作完成两个 32 位操作来巧妙地组合相邻的操作。这种类型的“穷人矢量化”的例子并不少见。例如,将常量 struct two32{ uint32_t a, b; } 创建成 rax 就像 two32{1, 2} can be optimized 成单个 mov rax, 0x20001 而 64 位版本需要两条指令。原则上这对于相邻的算术运算(相同的运算,不同的操作数)也应该是可能的,但我在实践中还没有看到。
  • 较低的“内存使用”通常也会导致指令更少,即使内存或缓存占用不成问题,因为任何类型的结构或这种类型的数组都会被复制,因此每个复制的寄存器都会物有所值.
  • 较小的数据类型通常利用更好的现代调用约定,例如 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。

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

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


1 需要明确的是,将结构紧密打包到寄存器中的惯例并不总是对较小的值有利。这确实意味着较小的值可能必须先“提取”才能使用。例如,一个返回两个结构成员之和的简单函数需要一个mov rax, rdi; shr rax, 32; add edi, eax,而对于 64 位版本,每个参数都有自己的寄存器,只需要一个addlea。尽管如此,如果您接受“通过时紧密包装结构”的设计总体上是有意义的,那么较小的值将更多地利用此功能。

【讨论】:

  • x86-64 Linux 上的 glibc 使用 64 位 uint_fast32_t,这是 IMO 的错误。 (显然 Windows uint_fast32_t 在 Windows 上是 32 位类型。)在 x86-64 Linux 上是 64 位,这就是为什么我永远不会推荐任何人使用 uint_fast32_t:它针对低指令数(函数参数和返回值)进行了优化永远不需要零扩展来用作数组索引)而不是在主要重要平台之一上的整体速度或代码大小。
  • 哦,对了,我在上面阅读了您关于 SysV ABI 的评论,但正如您稍后指出的那样,可能是由不同的组/文档决定的 - 但我想一旦发生这种情况,它几乎就在石头。我认为,即使在没有良好 32 位操作支持的平台上,纯循环计数/指令计数有利于更大的类型,甚至忽略内存占用效应和矢量化,这也是值得怀疑的——因为仍然存在编译器可以更好地优化较小类型的情况。我在上面添加了一些示例。 @PeterCordes
  • SysV 在返回 pair<int,bool>pair<int,int> 时将多个结构成员打包到同一个寄存器中会花费 更多 指令。如果两个成员都不是编译时常量,则通常不仅仅是 OR,调用者必须解包返回值。 (bugs.llvm.org/show_bug.cgi?id=34840 LLVM 优化了私有函数的返回值传递,并且应该将 32 位 int 视为获取整个 rax,因此 booldl 中是独立的,而不是需要一个 64 位常量来 @ 987654355@它。)
  • 我认为编译器通常不会拆分函数。将快速路径作为单独的函数剥离是一种有用的源代码级优化(尤其是在可以内联的标头中)。如果 90% 的输入是“什么都不做的情况”,那就太好了;在调用者的循环中进行过滤是一个巨大的胜利。 IIRC,Linux使用__attribute__((noinline))正是为了确保gcc不会内联错误处理函数,并将一堆push rbx/.../pop rbx/...放在一些重要内核的快速路径上具有许多调用者且自身不内联的函数。
  • 在 Java 中它也很重要,因为内联是进一步优化的关键(尤其是与 C++ 不同的普遍存在的去虚拟化),因此通常需要在此处拆分快速路径和“字节码”优化”实际上是一件事(尽管传统观点认为它没有意义,因为 JIT 进行最终编译)只是为了让字节码倒计时,因为内联决策基于字节码大小,而不是内联机器码大小(并且相关性可以因数量级而异)。
【解决方案7】:

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

使用uint32_t而不是uint_fast32_t还有其他原因:通常是因为它会提供稳定的ABI。此外,可以准确地知道内存使用情况。这在很大程度上抵消了 uint_fast32_t 的速度增益,只要该类型与 uint32_t 的类型不同。

对于 unsigned int(unsigned short 也需要至少具有该范围,但 unsigned int 是本机字长)对于值 unsigned long。


最后,人们不使用uint_fast32_t,因为它打字太长而且容易打错:D

【讨论】:

  • @ikegami:你用short 编辑改变了我的意图。 intshort 不同时可能是 fast 的。
  • 那么你的最后一句话是完全错误的。声称你应该使用unsigned int 而不是uint16_fast_t 意味着你声称比编译器更了解。
  • 另外,我很抱歉改变了你的文字意图。那不是我的意图。
  • unsigned long 如果您的平台有 64 位 longs 并且您只需要数字 <2^32,则不是一个好的选择。
  • @ikegami:“unsigned int”类型将始终表现为无符号类型,即使在提升时也是如此。在这方面它优于uint16_tuint_fast16_t。如果uint_fast16_t 比普通整数类型更松散地指定,这样它的范围对于地址未被占用的对象不需要保持一致,这可以在内部执行 32 位算术但具有 16- 位运算的平台上提供一些性能优势位数据总线。但是,该标准不允许这种灵活性。
【解决方案8】:

据我了解,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 在大多数情况下是更好的选择 - 它不允许对其行为有任何歧义。

此外,uint_fast32_t 在大小等于或大于 32 位的平台上真的是最快的类型吗?并不真地。考虑一下 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个字节,所以情况不同。

许多程序员在需要返回小值时使用int(比如说在[-32,32] 范围内)。如果int 是平台本机整数大小,这将非常有效,但由于它不在 64 位平台上,因此与平台本机类型匹配的另一种类型是更好的选择(除非它经常与其他较小的整数一起使用) .

基本上,不管标准是什么,uint_fast32_t 在某些实现上都被破坏了。如果您关心在某些地方生成的附加指令,您应该定义自己的“本机”整数类型。或者您可以为此使用size_t,因为它通常会匹配native 的大小(我不包括像8086 这样的老旧平台,只包括可以运行Windows、Linux 等的平台)。


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

【讨论】:

  • 关于“没有人将 int 扩展为 64 位整数,因为”和“不幸的是,这不再适用于 64 位架构”的想法是非常好的观点。公平地说,“最快”和比较汇编代码:在这种情况下,第二个代码 sn-p 的额外指令似乎较慢,但代码长度和速度有时并没有很好的相关性。更强的比较会报告运行时间——但这并不容易做到。
  • 我并不容易衡量第二个代码的缓慢性,英特尔 CPU 可能做得非常好,但更长的代码也意味着大量的缓存污染。偶尔单条指令可能不会有什么坏处,但 uint_fast32_t 的用处变得模棱两可。
  • 我非常同意uint_fast32_t 的用处变得模棱两可,除了非常特殊的情况。我怀疑uint_fastN_t 的驱动原因根本是为了适应“我们不要将unsigned 用作64 位,即使它在新平台上通常最快,因为太多代码会中断”但“我仍然想要一个快速至少 N 位类型。”如果可以的话,我会再给你紫外线。
  • 大多数 64 位架构可以轻松地对 32 位整数进行操作。甚至 DEC Alpha(它是一个全新的 64 位架构,而不是对现有 32 位 ISA(如 PowerPC64 或 MIPS64)的扩展)具有 32 位和 64 位加载/存储。 (但不是字节或 16 位加载/存储!)。大多数指令仅为 64 位,但它具有对 32 位加/减和乘法的原生硬件支持,可将结果截断为 32 位。 (alasir.com/articles/alpha_history/press/alpha_intro.html) 因此,将int 设为 64 位几乎不会提高速度,而且通常会因缓存占用而降低速度。
  • 另外,如果您将int 设为64 位,您的uint32_t 固定宽度类型定义将需要__attribute__ 或其他hack,或者一些小于int 的自定义类型。 (或short,但uint16_t 也有同样的问题。)没人想要这样。 32 位对于几乎所有东西来说都足够宽(不像 16 位);在 64 位机器上以任何有意义的方式使用 32 位整数并不是“低效”。
【解决方案9】:

出于实际目的,uint_fast32_t 完全没用。它在最广泛的平台 (x86_64) 上定义不正确,除非您有一个质量非常低的编译器,否则它在任何地方都没有真正提供任何优势。从概念上讲,在数据结构/数组中使用“快速”类型是没有意义的——你从更高效的类型中获得的任何节省都将与增加大小的成本(缓存未命中等)相形见绌。您的工作数据集。对于单个局部变量(循环计数器、临时变量等),非玩具编译器通常可以在生成的代码中使用更大的类型,如果这样更有效的话,并且只有在需要正确性时才截断到标称大小(并且使用签名类型,它从来没有必要)。

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

【讨论】:

    【解决方案10】:

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

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

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

    然而,当我们已经拥有 long 时,看看所有 32_t 类型需要存在的原因,你会发现这些假设在我们之前已经被打破了。您的代码很可能有一天会在精确宽度的 32 位计算比本机字长慢的机器上运行,您最好使用uint_least32_t 进行存储,使用uint_fast32_t 进行虔诚的计算。或者,如果您在到达时会越过那座桥并且只想要一些简单的东西,那就是unsigned long

    【讨论】:

    • 但有些架构int 不是 32 位,例如 ILP64。并不是说它们很常见。
    • 我认为 ILP64 不存在现在时态?几个网页声称“Cray”使用它,所有这些都引用了 1997 年的同一个 Unix.org 页面,但 UNICOS 在 90 年代中期实际上做了一些更奇怪的事情,而今天的 Crays 使用英特尔硬件。同一页面声称 ETA 超级计算机使用 ILP64,但它们很久以前就停业了。维基百科声称 HAL 的 Solaris 到 SPARC64 的端口使用了 ILP64,但它们也已经停业多年。 CppReference 说 ILP64 只在少数早期的 64 位 Unices 中使用。所以它只与一些非常深奥的逆向计算有关。
    • 请注意,如果您今天使用英特尔数学内核库的“ILP64 接口”,int 将是 32 位宽。 MKL_INT 类型将会改变。
    【解决方案11】:

    直接回答:我认为使用uint32_t 而不是uint_fast32_tuint_least32_t 的真正原因仅仅是因为它更容易输入,而且由于更短,更易于阅读:如果您使用某些类型创建结构,其中一些是 uint_fast32_t 或类似的,那么通常很难将它们与 intbool 或 C 中的其他类型很好地对齐,这些类型很短(例如:@ 987654327@ 与 character)。我当然不能用硬数据来支持这一点,但其他答案也只能猜测原因。

    至于更喜欢uint32_t 的技术原因,我认为没有 - 当你绝对需要一个精确的 32 位无符号整数时,那么这种类型是你的 only 标准化选择。在几乎所有其他情况下,其他变体在技术上更可取 - 具体而言,uint_fast32_t 如果您关心速度,uint_least32_t 如果您关心存储空间。在这两种情况下使用 uint32_t 都有可能无法编译,因为该类型不需要存在。

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

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

    【讨论】:

    • 嗯,有一些不一致...或权衡。如果易于输入很重要,为什么不用u32 而不是uint32_t?不知道是不是更难读...
    • C 中没有 u32 类型,所以它不是一个选项。它也不是问题中的一个选项。但是,很多代码确实定义了 U32 类型,大概是为了便于键入,但代价是在某处维护 typedef 的标头。
    • 由于u32 不是保留标识符或关键字,您始终可以使用typedef u32 uint32_t;,但代价是声明可能与其他人的代码冲突。如果不想维护声明的成本,建议给JTC1/SC22/WG14,就像C99之前的时代一样,当时也没有uint32_t。不过,不太可能采用纯同义词。 IIRC u32 在几十年前就已经很流行了,因此选择将 uint32_t 而不是 u32 纳入标准似乎也很重要。
    猜你喜欢
    • 2013-11-15
    • 2021-10-12
    • 1970-01-01
    • 2023-03-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多