将低字节从 int 复制到 char 的说明:只进行字节加载更简单?

Instructions to copy the low byte from an int to a char: Simpler to just do a byte load?

我在看一本教科书,里面有一个基于 C 代码编写 x86-64 汇编代码的练习

//Assume that the values of sp and dp are stored in registers %rdi and %rsi

int *sp;
char *dp;
*dp = (char) *sp;

答案是:

//first approach

movl (%rdi), %eax    //Read 4 bytes
movb %al, (%rsi)     //Store low-order byte

我能理解,但只是想知道我们不能首先做一些简单的事情:

//second approach

movb (%rdi), %al    //Read one bytes only rather than read all four bytes
movb %al, (%rsi)     //Store low-order byte

与第一种方法相比,第二种方法不是更简洁明了吗?第一种方法有点不必要,因为我们只关心 %rdi 的低字节,而对其高 3 个字节并不真正感兴趣。

(OP 将问题从一个带有示例的更笼统的问题更改为一个非常具体的问题,这可以解释为什么这个答案对于当前问题看起来很有趣。)

对您问题的更笼统的回答是,对于您打算编译为机器代码的 HLL 中的任何操作,通常有很多方法可以编写机器指令来执行该操作。

一个好的编译器会知道其中的许多变体。它的问题是,对于程序中的所有操作,为每个操作符选择通常更有效的变体,以便将它们拼接在一起以实现工作程序。例如,如果一个 HLL 操作的实现将其结果留在寄存器中,并且后续 HLL 操作应该使用该结果,那么编译器会选择第一个运算符和第二个运算符的实现,其中第一个运算符将值保留在一个寄存器中,第二个恰好将该寄存器用作输入,否则程序将无法运行。

当你考虑到一个真实的程序由数千个 HLL 运算符组成,并且它们各自的实现必须全部一致时,你会发现编译器有一项非常复杂的工作,以确保一切都适合并且相当有效。

是的,你的字节加载方式是正确的,但它不是实际上在大多数 CPU 上更有效。
TL:DR:当您有同样方便但不这样做的选项时,通常会避免写入字节或 16 位寄存器。

(顺便说一句,你在评论中得到的建议都是错误的:x86 是小端,存储转发问题的可能性很小(尽管可能在一些较旧的 CPU 上,IDK 可能并非完全错误) .)


写入部分寄存器(窄于 32 位,因此它不会隐式零扩展到完整寄存器)对某些微体系结构的旧值有错误的依赖性。即 movb (%rdi), %al 在英特尔 Haswell/Skylake 上解码为微融合加载+合并 ALU 操作。 (. Also for Intel Haswell/Skylake specifically, .)

movzbl (%rdi), %eax 只进行零扩展字节加载会更有效。

或者因为我们可以假设到 (%rdi) 的最后一个存储是双字或更宽的(所以如果它仍在运行中,存储转发将是有效的), 实际上用 movl (%rdi), %eax 进行双字加载是最有效的。这避免了可能的部分寄存器惩罚,并且机器代码大小比 movzbl 更小(越小越好,作为在 uops 方面其他相等选项之间的决胜局)。此外,一些旧的 AMD CPU 运行 movzbl 比 dword mov 加载效率稍低。 (比如零扩展需要一个ALU端口)。

(大多数 CPU 运行 movzbl 在加载端口中“免费”,有些还 运行 movsbl 在加载端口中进行符号扩展而不需要任何 ALU端口,特别是 Intel Sandybridge 系列。)


商店转发不是问题: 所有(?)当前的 CPU 都可以从双字存储有效地转发到任何单个字节的字节重新加载,并且绝对是低字节,尤其是当双字存储对齐时(如 C int 将对齐)。参见 https://blog.stuffedcow.net/2014/01/x86-memory-disambiguation/

当然,如果您稍后需要将 char 值符号或零扩展到寄存器中,请按此方式加载。

或者甚至更好,正如@Ira 指出的那样,如果您正在优化此代码以及存储到 *sp 的内容,理想情况下您可以只使用寄存器中的任何内容并优化掉 store/reload。 (在 C 语言中,任何其他线程异步更改该内存是未定义的行为,因为它是 int *、非易失性或 _Atomic int*。)