x86-64 平台上的 int_fast8_t 大小与 int_fast16_t 大小

Ex-*_*uto 3 c 64-bit assembly x86-64 low-level

我已经了解到,在 x86-64 平台上使用任何 64 位寄存器都需要一个REX前缀,而任何小于 64 位的地址都需要一个地址大小前缀。

在 x86-64 位上:

E3rel8 是jrcxz

67 E3rel8 是jecxz

67是地址大小覆盖前缀的操作码。

sizeof(int_fast8_t)是 8 位,而其他sizeof(int_fast16_t)sizeof(int_fast32_t)(仅在 Linux 上)是 64 位。

为什么其他快速类型定义是 64 位而只有int_fast8_t8 位?

和对齐有关系吗?

Pet*_*des 13

为什么只有 int_fast8_t 是 8 位,而其他 fasttypdef 是 64 位?

因为当 x86-64 是新的、这些 C99 类型是新的时, glibc 做出了一个简单且可以说是错误的选择,并且做出了不专门针对 x86-64 的错误决定。

所有这些都int_fast16/32/64_t被定义为 long跨所有平台。这是在 1999 年 5 月AMD64 发布纸质规范之前(1999 年 10 月)完成的,开发人员大概花了一些时间来理解。(感谢 @Homer512 找到提交和历史记录。)

long是 32 位和 64 位 GNU 系统中的完整(整数)寄存器宽度。 这也是指针宽度。

对于大多数 64 位 RISC,全宽是相当自然的,尽管我不知道乘法和除法速度。这对于 x86-64 来说显然很糟糕,其中 64 位操作数大小需要额外的代码大小,但 MIPSdadduaddu代码大小相同,并且性能可能相当。(在 x86-64 之前,RISC ABI 始终将窄类型符号扩展为 64 位是很常见的,因为 MIPS 至少实际上需要非移位指令。请参阅MOVZX 缺少 32 位寄存器到 64 位寄存器了解更多历史。)

Glibc 的选择使得这些类型对于局部变量来说基本上是可以的,至少如果您不进行乘法或除法或__builtin_popcount任何其他可能需要更多位进行更多工作的操作(尤其是在没有硬件popcnt支持的情况下)。但在内存中的存储空间很重要的任何地方都不好。

如果您希望“仅在避免任何性能问题的情况下选择大于指定的大小”类型,那么 glibc 根本无法为您提供这种类型。


我似乎记得 MUSL 在 x86-64 上做出了更好的选择,就像除了32 位之外的每个fast大小都是最小大小,避免操作数大小前缀和部分寄存器的东西。fast16

fast提出了“为什么要快?”的问题,并且每个用例的答案大小并不相同。例如,在可以使用 SIMD 自动向量化的东西中,尽可能窄的整数通常是最好的,这样每个 16 字节向量指令完成的工作量是原来的两倍。在这种情况下,16 位整数是合理的。或者只是为了数组中的缓存占用空间。但不要指望fastxx_t类型会考虑“不太慢”与节省数组大小之间的权衡。

通常,窄加载/存储指令在大多数 ISA 上都适用,因此如果缓存占用空间是相关考虑因素,则应该具有局部变量和窄数组元素intint_fastxx_t但即使对于本地变量,glibc 的选择也常常很糟糕。


也许 glibc 人们只计算指令,而不是代码大小(REX 前缀)或乘法和除法的成本(对于 64 位来说,这肯定比 32 位慢或更窄,特别是在那些早期的 AMD64 CPU 上;整数除法仍然慢得多对于 Intel 上的 64 位,直到 Ice Lake https://uops.info/https://agner.org/optimize/)。

并且没有直接考虑对结构大小的影响以及由于alignof(T) == 8. (尽管 x86-64 System V ABI 中未设置类型的大小fast,因此最好不要在 ABI 边界使用它们,例如库 API 中涉及的结构。)

我真的不知道他们为什么犯了这么严重的错误,但它使得int_fastxx_t类型除了局部变量(不是大多数结构或数组)之外的任何东西都毫无用处,因为 x86-64 GNU/Linux 是大多数可移植代码的重要平台,而你不这样做不希望你的代码在那里很糟糕。

有点像 MinGW 脑残的决定返回std::random_device低质量的随机数(而不是失败,直到他们开始实现一些可用的东西)就像在其上倾倒放射性废物一样,只要可移植代码能够使用语言功能来实现预期目的目的。


使用 64 位整数的几个优点之一可能是避免在 ABI 边界(函数参数和返回值)处理寄存器高位部分的垃圾。但通常这并不重要,除非您需要将其扩展到指针宽度作为寻址模式的一部分。(在x86-64中,寻址模式下的所有寄存器都必须具有相同的宽度,例如[rdi + rdx*4]。AArch64具有这样的模式,即[x0, w1 sxt]符号扩展32位寄存器作为64位寄存器的索引。但是AArch64的机器代码格式是从头开始设计的,后来看到其他 64 位 ISA 的运行后才发现。)

例如,arr[ foo(i) ]如果返回类型填充寄存器,则可以避免指令对返回值进行零扩展。否则,需要将其符号或零扩展到指针宽度,然后才能用于寻址模式,使用movor movsxd(32 位到 64 位)或movzxor movsx(8 或 16 位到 64 位)。

或者,采用 x86-64 System V 在最多 2 个寄存器中按值传递和返回结构的方式,64 位整数不需要任何解包,因为它们本身已经位于寄存器中。例如,struct ( int32_t a,b; }将两个ints 打包到 RAX 中的返回值中,如果实际使用结果,则需要在被调用者中进行打包,在调用者中进行解包,而不仅仅是将对象表示存储到内存中的结构中。(例如mov ecx, eax,对低半部分进行零扩展 / shr rax, 32。或者只是add ebx, eax使用低半部分,然后通过移位丢弃它;您不需要将其零扩展到 64 位来将其用作 32 位整数.)

在函数内,编译器在写入 32 位寄存器后会知道值已被零扩展为 64 位。并且从内存加载,甚至符号扩展到 64 位也是免费的(movsxd rax, [rdi]而不是mov eax, [rdi])。(或者在较旧的 CPU 上几乎免费,其中内存源符号扩展仍然需要 ALU uop,而不是作为加载 uop 的一部分完成。)

因为有符号整数溢出是 UB,所以编译器能够在类似 的循环中将int( int32_t) 扩大到 64 位for (int i = 0 ; i < n ; i++ ) arr[i] += 1;,或者将其转换为 64 位指针增量。(我想知道 GCC 是否可能无法在 2000 年代初做出这些软件设计决策时做到这一点?在这种情况下,是的,浪费movsxd指令来不断将循环计数器重新扩展到 64 位将是一个有趣的考虑因素.)

但公平地说,您仍然可以在计算中使用有符号 32 位整数类型来获得符号扩展指令,如果您随后使用这些指令来索引数组,则可能会产生负结果。因此 64 位int_fast32_t避免了这些movsxd指令,但代价是在其他情况下变得更糟。也许我对此不以为然,因为我知道要避免它,例如unsigned在适当的时候使用,因为我知道它在 x86-64 和 AArch64 上免费进行零扩展。


对于实际计算,32 位操作数大小通常至少与其他任何操作数一样快,包括 imul/div 和 popcnt,并且避免部分寄存器惩罚movzx或使用 8 位或 16 位获得的额外指令。

但 8 位也不错,如果你的数字那么小,将它们扩展到 32 或 64 位就更糟糕了;程序员可能对小型化有更多的期望int_fast8_t,除非将其变大的成本要高得多。它不在 x86-64 上;是否有任何现代 CPU 的缓存字节存储实际上比字存储慢?- 是的,显然大多数非 x86,但 x86 确实使字节和 16 位字的加载/存储以及计算速度更快。

避免使用 16 位可能是件好事,在某些情况下值得付出额外 2 个字节的代价。 add ax, 12345(和其他 imm16 指令)在 Intel CPU 上有 LCP 解码停顿。加上部分注册错误依赖性(或在较旧的 CPU 上,合并停止)。


jrcxzvs.jecxz是一个奇怪的例子,因为它使用67h 地址大小前缀,而不是66h操作数大小。因为编译器从不(?)使用它。它并不像loop指令那么慢,但令人惊讶的是,即使在可以将 a 宏融合test/jz到单个 uop 的 Intel CPU 上,它也不是单 uop。

  • 在所有平台上将 `int_fast16/32/64_t` 定义为 `long` 似乎非常懒惰和糟糕。这是在 AMD64 提出之前完成的,**“long”是 32 位和 64 位 GNU 系统中的完整寄存器宽度。**对于大多数 RISC,该宽度是相当自然的,尽管我不知道乘法和除法速度。这对于 x86-64 来说显然很糟糕,其中 64 位操作数大小需要额外的代码大小,但 MIPS `daddu` 和 `addu` 基本上是等价的。 (2认同)

归档时间:

查看次数:

232 次

最近记录:

1 年,11 月 前