【问题标题】:Instructions to copy the low byte from an int to a char: Simpler to just do a byte load?将低字节从 int 复制到 char 的说明:只进行字节加载更简单?
【发布时间】:2020-07-08 04:20:14
【问题描述】:

我在看一本教科书,里面有一个基于 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 字节。

【问题讨论】:

  • 您需要在%rdi 中添加1 或3 个字节(取决于字节序)以获得指向*sp 低位字节的指针。
  • @Barmar 假设机器是小端,那么为什么我们需要添加 1 个字节?默认情况下地址是第一个字节,不是吗?
  • 您的方式可能会停止存储到加载转发器。
  • @Barmar:x86-64 是 little-endian。 int 的低字节是最不重要的。 OP 的两个版本都是正确的,但第二个版本具有部分注册错误依赖项。
  • @slowjams 你所做的是将作业重写为*dp = *(char*)sp;。教科书提供了原始 C 代码的直译,没有任何优化,以避免混淆读者。您的重写在功能上是等效的(假设指针不跨越页面边界),但有可能与您正在阅读的教科书章节范围之外的细微 CPU 性能问题发生冲突。

标签: c assembly x86-64 micro-optimization instructions


【解决方案1】:

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

(顺便说一句,您在 cmets 中得到的建议都是错误的:x86 是 little-endian,并且存储转发问题不太可能出现(尽管可能在某些较旧的 CPU 上可能,IDK 可能并非完全错误)。)


写入部分寄存器(小于 32 位,因此它不会隐式零扩展至完整寄存器)对某些微架构上的旧值具有错误的依赖性。即movb (%rdi), %al 在 Intel Haswell/Skylake 上解码为微融合加载+合并 ALU 操作。 (Why doesn't GCC use partial registers?。也专门针对 Intel Haswell/Skylake,this has a lot of detail。)

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

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

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


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

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

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

【讨论】:

    【解决方案2】:

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

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

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

    当您考虑到一个真正的程序由数千个 HLL 运算符组成,并且它们的各个实现都必须保持一致时,您会看到编译器有一项非常复杂的工作来确保所有内容组合在一起并且相当高效。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-03-05
      • 1970-01-01
      • 2012-08-14
      • 2017-11-11
      • 1970-01-01
      • 2011-10-28
      • 2014-04-04
      • 2014-04-28
      相关资源
      最近更新 更多