为啥快速整数类型比其他整数类型快?
Posted
技术标签:
【中文标题】为啥快速整数类型比其他整数类型快?【英文标题】:Why are the fast integer types faster than the other integer types?为什么快速整数类型比其他整数类型快? 【发布时间】:2020-04-22 21:37:47 【问题描述】:在 ISO/IEC 9899:2018 (C18) 中,它在 7.20.1.3 下规定:
7.20.1.3 最快的最小宽度整数类型
1 以下每种类型都指定了一个整数类型,在所有至少具有指定宽度的整数类型中,该整数类型通常是最快的268)。
2 typedef 名称
int_fastN_t
指定最快的有符号整数类型,宽度至少为 N。typedef 名称uint_fastN_t
指定最快的无符号整数类型,宽度至少为 N。3 需要以下类型:
int_fast8_t
,int_fast16_t
,int_fast32_t
,int_fast64_t
,uint_fast8_t
、uint_fast16_t
、uint_fast32_t
、uint_fast64_t
此表单的所有其他类型都是可选的。
268) 不保证指定的类型在所有用途中都是最快的;如果实现没有明确的理由选择一种类型而不是另一种,它将简单地选择一些满足符号和宽度要求的整数类型。
但没有说明为什么这些“快速”整数类型更快。
为什么这些快速整数类型比其他整数类型更快?我用 C++ 标记了这个问题,因为在 cstdint
的头文件中的 C++17 中也提供了快速整数类型。不幸的是,在 ISO/IEC 14882:2017 (C++17) 中没有关于它们的解释的部分;我已经在问题正文中实现了该部分。
信息:在C语言中,它们在stdint.h
的头文件中声明。
【问题讨论】:
这里的关键点是这些整数类型不是独立的,神奇地更快的类型。它们只是该机器上该操作最快的任何正常现有类型的别名。 编译器发出 CPU 操作操作码来加载、存储、屏蔽和修改特定大小的内存位置和寄存器;这就是 CPU 看到的所有内容。操作系统与它无关。这都是编译器在做的,就像你自己指定了给定的 typedef 一样。 (我假设编译器可以在内部以不同的方式处理它——如果可能的话,可能比用户 typedef 更有效——只要行为上没有明显的差异。) @RobertS-ReinstateMonica 准确地说,这些“别名”只是typedef
语句。所以通常是在标准库级别完成的。当然,C 标准对 typedef
的用途没有真正的限制——例如,一个典型的实现是在 32 位系统上使 int_fast32_t
成为 typedef
或 int
,但假设编译器 可以 例如实现一个__int_fast
内在类型并承诺做一些花哨的优化来根据具体情况为该类型的变量选择最快的机器类型,然后库可以只是@ 987654339@ 到那个。
@RobertS-ReinstateMonica 是的,是的。您可以使用特定于架构的编译标志获得最高性能的程序,从而降低二进制文件的可移植性。
@RobertS-ReinstateMonica 在编译它for的平台上效率最高,不一定on。
【参考方案1】:
想象一个只执行 64 位算术运算的 CPU。现在想象一下如何在这样的 CPU 上实现无符号 8 位加法。要获得正确的结果,必然涉及不止一项操作。在这样的 CPU 上,64 位操作比其他整数宽度上的操作更快。在这种情况下,所有的Xint_fastY_t
都可能是 64 位类型的别名。
如果 CPU 支持窄整数类型的快速操作,因此更宽的类型不会比更窄的类型快,那么 Xint_fastY_t
将不会(不应该)是表示所有 Y 所需的更宽类型的别名位。
出于好奇,我检查了某些架构上特定实现(GNU、Linux)的大小。这些在同一架构上的所有实现中都不相同:
┌────╥───────────────────────────────────────────────────────────┐
│ Y ║ sizeof(Xint_fastY_t) * CHAR_BIT │
│ ╟────────┬─────┬───────┬─────┬────────┬──────┬────────┬─────┤
│ ║ x86-64 │ x86 │ ARM64 │ ARM │ MIPS64 │ MIPS │ MSP430 │ AVR │
╞════╬════════╪═════╪═══════╪═════╪════════╪══════╪════════╪═════╡
│ 8 ║ 8 │ 8 │ 8 │ 32 │ 8 │ 8 │ 16 │ 8 │
│ 16 ║ 64 │ 32 │ 64 │ 32 │ 64 │ 32 │ 16 │ 16 │
│ 32 ║ 64 │ 32 │ 64 │ 32 │ 64 │ 32 │ 32 │ 32 │
│ 64 ║ 64 │ 64 │ 64 │ 64 │ 64 │ 64 │ 64 │ 64 │
└────╨────────┴─────┴───────┴─────┴────────┴──────┴────────┴─────┘
请注意,尽管对较大类型的操作可能更快,但此类类型也会占用更多缓存空间,因此使用它们不一定会产生更好的性能。此外,人们不能总是相信实施一开始就做出了正确的选择。与往常一样,需要测量才能获得最佳结果。
表格屏幕截图,适用于 android 用户:
(Android 在单色字体中没有画框字符 - ref)
【讨论】:
评论不用于扩展讨论;这个对话是moved to chat。 @RobertSsupportsMonicaCellio 否。“在所有架构中都不相同”也是正确的,但从显示的数据中可以立即看出,所以我认为没有必要说明这一点。我只展示了一种实现的值,实际上其他实现会有不同的选择。例如在 Windows 上检查 x86-64。与此处显示的尺寸相比,您会发现不同的尺寸。 在 x86-64 上,32 位操作数大小至少与 64 位一样快,并且可以节省代码大小。所有 32 位操作都将结果隐式零扩展为 64 位。 Glibc 选择uint_fast32_t = uint64_t
对于 x86-64 来说是一个糟糕的选择; 64 位整数除法在 Intel CPU 上比 32 位慢得多,64 位乘法(和 popcnt)在一些较旧和低功耗的情况下更慢。更重要的是,在数组中,您浪费了两倍的内存带宽,并且每个向量丢失了两倍的 SIMD 元素。
IIRC,MUSL(另一种 libc)在 x86-64 上使用 uint_fast32_t = uint32_t
。 x86-64 上 64 位整数的唯一好处是,在将它们用作数组索引时,有时可以避免将它们符号扩展为 64 位。
所以更基本的一点是,对于一种尺寸的最快类型没有万能的答案,正如Why would uint32_t be preferred rather than uint_fast32_t? 上的 cmets 中所讨论的那样。有时 int64_t
本地临时循环计数器在 x86-64 上是最佳的,这取决于周围的代码,而 int32_t
对于数组来说通常会更快。 (我仍然认为在 x86-64 GNU 上将 fast32
类型设为 64 位是愚蠢的)【参考方案2】:
它们不是,至少不可靠。
快速类型只是常规类型的 typedef,但是如何定义它们取决于实现。它们必须至少是请求的大小,但可以更大。
确实,在某些体系结构中,某些整数类型的性能比其他的更好。例如,早期的ARM 实现具有针对 32 位字和无符号字节的内存访问指令,但它们没有针对半字或有符号字节的指令。半字和有符号字节指令是后来添加的,但它们的寻址选项仍然不太灵活,因为它们必须被硬塞到备用编码空间中。此外,ARM 上的所有实际数据处理指令都对字起作用,因此在某些情况下,可能需要在计算后屏蔽掉较小的值以给出正确的结果。
但是,缓存压力也存在竞争问题,即使加载/存储/处理较小值需要更多指令。如果减少缓存未命中的数量,较小的值可能仍会表现更好。
许多常见平台上的类型定义似乎没有经过深思熟虑。特别是,现代 64 位平台往往对 32 位整数有很好的支持,但在这些平台上,“快速”类型通常是不必要的 64 位。
此外,C 中的类型成为平台 ABI 的一部分。因此,即使平台供应商发现他们做出了愚蠢的选择,以后也很难改变这些愚蠢的选择。
忽略“快速”类型。如果您真的关心整数性能,请使用所有可用大小对您的代码进行基准测试。
【讨论】:
【参考方案3】:快速类型并不比所有其他整数类型都快——它们实际上与某些“普通”整数类型相同(它们只是该类型的别名)——无论哪种类型恰好是保持至少这么多位的值的最快速度。
它只是依赖于平台的每个快速类型是整数类型的别名。
【讨论】:
以上是关于为啥快速整数类型比其他整数类型快?的主要内容,如果未能解决你的问题,请参考以下文章