如果距开始位置 <128 字节,则更快地访问结构成员?

Wad*_*Wad 3 x86 assembly micro-optimization

Anger Fog 的 C++ optimization manual,我读到:

如果成员相对于结构或类的开头的偏移量小于 128,则访问数据成员的代码会更加紧凑,因为偏移量可以表示为 8 位有符号数。如果相对于结构或类的开头的偏移量是 128 字节或更多,则偏移量必须表示为 32 位数字(指令集在 8 位和 32 位偏移量之间没有任何内容)。例子:

// Example 7.40
class S2 {
public:
int a[100]; // 400 bytes. first byte at 0, last byte at 399
int b; // 4 bytes. first byte at 400, last byte at 403
int ReadB() {return b;}
};
Run Code Online (Sandbox Code Playgroud)

这里 b 的偏移量为 400。任何通过指针或成员函数(如 ReadB)访问 b 的代码都需要将偏移量编码为 32 位数字。如果 a 和 b 交换,则可以使用编码为 8 位有符号数的偏移量访问两者,或者根本没有偏移量。这使得代码更紧凑,从而更有效地使用代码缓存。因此,建议将大数组和其他大对象放在结构或类声明的最后,最常用的数据成员放在最前面。如果不可能在前 128 个字节中包含所有数据成员,则将最常用的成员放在前 128 个字节中。

我曾尝试这样做,我看到在这个测试程序的组件输出没有什么区别,如图所示在这里

class S2 {
public:
    int a[100]; // 400 bytes. first byte at 0, last byte at 399
    int b; // 4 bytes. first byte at 400, last byte at 403
    int ReadB() { return b; }
};

// Changed order of variables a and b!
class S3 {
public:
    int b; // 4 bytes. first byte at 400, last byte at 403
    int a[100]; // 400 bytes. first byte at 0, last byte at 399
    int ReadB() { return b; }
};

int main()
{
    S3 s3; s3.b = 32;
    S2 s2; s2.b = 16;
}
Run Code Online (Sandbox Code Playgroud)

输出是

push    rbp
mov     rbp, rsp
sub     rsp, 712
mov     DWORD PTR [rbp-416], 32
mov     DWORD PTR [rbp-432], 16
mov     eax, 0
leave
ret
Run Code Online (Sandbox Code Playgroud)

显然,mov DWORD PTR用于这两种情况。

  1. 有人可以解释为什么会这样吗?
  2. 有人可以解释什么是“指令集在 8 位和 32 位偏移之间没有任何内容”(我是 ASM 的新手)以及该声明表明我应该在 ASM 中看到什么?

Nat*_*dge 6

您应该查看 asm ReadB,而不是main; 但由于它们是内联定义的,除非您调用它们(然后它将与调用函数的代码混合在一起),否则不会生成 asm。让我们将它们移出线以使其更容易。

class S2 {
public:
    int a[100];
    int b;
    int ReadB();
};

int S2::ReadB() { return b; }
Run Code Online (Sandbox Code Playgroud)

等等。

此外,仅查看 asm 代码不会向您显示指令的大小。您想查看实际的机器代码字节。在 Godbolt 中检查“输出:编译为二进制”会做到这一点;在真机上,您可以编译为目标文件并转储objdump --disassemble显示机器代码的类似反汇编工具进行。

有关更新版本,请参阅https://godbolt.org/z/bf7KjK

这些函数中的每一个都接受一个this指针 in rdi,并且需要this->b移入eax。所以它需要从内存中的 rdi 给出的地址加上b相关类中的偏移量加载一个双字。现在你可以看到:

  • b是之后a,你得到8b 87 90 01 00 00(6 个字节)mov eax, DWORD PTR [rdi+0x190]

  • b是在课程的最开始时,您将获得8b 07(2 个字节)mov eax, DWORD PTR [rdi]

  • 如果b是以前a,但一个新的后int会员other,你得到8b 47 04mov eax, DWORD PTR [rdi+0x4]

这里使用了三种不同的寻址模式,可以通过三种方式指定要加载的地址:

  • 作为寄存器(指令需要两个字节),

  • 作为一个寄存器加上一个有符号的 8 位位移(占用 1 个额外的字节),

  • 作为一个寄存器加上一个有符号的 32 位位移(占用 4 个额外的字节)。

如果必要的位移不为零但适合 8 位,则可以使用第二种形式。如果不是,那么您将陷入第三种形式,使您的代码大 3 个字节。(正如 prl 指出的那样,这并不一定会使它变慢,但它往往会变慢,因为它会消耗更多宝贵的缓存。)

“Nothing between”是指您可能希望有一种形式,例如,具有 16 位位移,这对于位移来说足够大,400但仅使用两个额外的字节。但没有。