Ex-*_*uto 3 c 64-bit assembly x86-64 low-level
我已经了解到,在 x86-64 平台上使用任何 64 位寄存器都需要一个REX
前缀,而任何小于 64 位的地址都需要一个地址大小前缀。
在 x86-64 位上:
E3
rel8 是jrcxz
67 E3
rel8 是jecxz
67
是地址大小覆盖前缀的操作码。
sizeof(int_fast8_t)
是 8 位,而其他sizeof(int_fast16_t)
和sizeof(int_fast32_t)
(仅在 Linux 上)是 64 位。
为什么其他快速类型定义是 64 位而只有int_fast8_t
8 位?
和对齐有关系吗?
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 位操作数大小需要额外的代码大小,但 MIPSdaddu
等addu
代码大小相同,并且性能可能相当。(在 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 上都适用,因此如果缓存占用空间是相关考虑因素,则应该具有局部变量和窄数组元素int
。int_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) ]
如果返回类型填充寄存器,则可以避免指令对返回值进行零扩展。否则,需要将其符号或零扩展到指针宽度,然后才能用于寻址模式,使用mov
or movsxd
(32 位到 64 位)或movzx
or movsx
(8 或 16 位到 64 位)。
或者,采用 x86-64 System V 在最多 2 个寄存器中按值传递和返回结构的方式,64 位整数不需要任何解包,因为它们本身已经位于寄存器中。例如,struct ( int32_t a,b; }
将两个int
s 打包到 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 上,合并停止)。
jrcxz
vs.jecxz
是一个奇怪的例子,因为它使用67h
地址大小前缀,而不是66h
操作数大小。因为编译器从不(?)使用它。它并不像loop
指令那么慢,但令人惊讶的是,即使在可以将 a 宏融合test/jz
到单个 uop 的 Intel CPU 上,它也不是单 uop。
归档时间: |
|
查看次数: |
232 次 |
最近记录: |