2

从 32 位 CPU 模式开始,有可用于 x86 架构的扩展地址操作数。可以指定基地址、位移、索引寄存器和比例因子。

例如,我们想遍历一个 32 位整数列表(前两个来自 32 字节长的数据结构数组,%rdi作为数据索引,%rbx作为基指针)。

addl   $8, %rdi                # skip eight values: advance index by 8
movl   (%rbx, %rdi, 4), %eax   # load data: pointer + scaled index
movl   4(%rbx, %rdi, 4), %edx  # load data: pointer + scaled index + displacement

据我所知,这种复杂的寻址适合单个机器代码指令。但是这种操作的成本是多少,它与使用独立指针计算的简单寻址相比如何:

addl  $32, %rbx      # skip eight values: move pointer forward by 32 bytes
movl  (%rbx), %eax   # load data: pointer
addl  $4, %rbx       # point next value: move pointer forward by 4 bytes
movl  (%rbx), %edx   # load data: pointer

在后一个示例中,我引入了一个额外的指令和一个依赖项。但是整数加法非常快,我获得了更简单的地址操作数,并且不再有乘法。另一方面,由于允许的缩放因子是 2 的幂,所以乘法归结为位移,这也是一个非常快的操作。尽管如此,两个加法和移位可以用一个加法代替。

这两种方法的性能和代码大小有什么区别?是否有使用扩展寻址操作数的最佳实践?

或者,从 C 程序员的角度来问,哪个更快:数组索引还是指针运算?


是否有任何用于大小/性能调整的程序集编辑器?我希望我能看到每条汇编指令的机器码大小、时钟周期的执行时间或依赖图。有成千上万的装配怪胎会从这种应用程序中受益,所以我敢打赌,这样的东西已经存在了!

4

2 回答 2

2

地址运算非常快,如果可能,应始终使用。

但这里有一些问题遗漏了。

起初你不能使用地址算术乘以 32 - 8 是最大可能的常数。

代码的第一个版本不完整,因为它需要第二条指令,即递增rbx。因此,我们有以下两种变体:

inc  rbx          
mov  eax, [8*rbx+rdi]

对比

add  rbx, 8
mov  eax, [rbx]

这样,两个变体的速度将是相同的。大小相同 - 也是 6 个字节。

所以,什么代码更好只取决于程序上下文 - 如果我们有一个寄存器已经包含所需数组单元的地址 - 使用 mov eax, [rbx]

如果我们有一个包含单元索引的寄存器和另一个包含起始地址的寄存器,那么使用第一个变体。这样,算法结束后,我们仍然会在 rdi 中得到数组的起始地址。

于 2013-09-02T06:49:58.423 回答
2

您的问题的答案取决于给定的本地程序流程环境 - 反过来,它们可能在处理器制造商和架构之间有所不同。对一两条指令进行微观分析通常是没有意义的。您有一个多级管道、多个整数单元、缓存以及更多发挥作用的东西,您需要将其纳入分析。

您可以通过查看生成的汇编代码并分析为什么序列看起来与将在其上工作的不同硬件单元的方式相同的方式来尝试逆向工程。

另一种方法是使用分析器并尝试不同的构造,以查看哪些有效,哪些无效。

您还可以下载 gcc 的源代码,看看真正酷的程序员如何评估序列以生成尽可能快的代码。有一天你可能会成为他们中的一员:-)

无论如何,我希望您会得出这样的结论:最佳序列会因处理器、编译器、优化级别和周围指令而有很大差异。如果您使用 C,源代码的质量非常重要:垃圾输入 = 垃圾输出。

于 2013-09-02T07:39:40.100 回答