assembly - 向x86-64 ABI的指针添加32位偏移时是否需要符号或零扩展?

标签 assembly x86-64 compiler-optimization abi sign-extension

简介:我正在查看汇编代码以指导优化,并在将int32添加到指针时看到许多符号或零扩展。

void Test(int *out, int offset)
{
    out[offset] = 1;
}
-------------------------------------
movslq  %esi, %rsi
movl    $1, (%rdi,%rsi,4)
ret


起初,我以为我的编译器在将32位整数添加到64位整数方面遇到了挑战,但是我已经通过Intel ICC 11,ICC 14和GCC 5.3确认了这种行为。

这个thread证实了我的发现,但尚不清楚是否需要符号或零扩展名。仅当尚未设置高32位时才需要此符号/零扩展名。但是x86-64 ABI是否足够聪明以至于不能要求它?

我有点不愿意将所有指针偏移量更改为ssize_t,因为寄存器溢出会增加代码的缓存占用空间。

最佳答案

是的,您必须假设arg或返回值寄存器的高32位包含垃圾。另一方面,允许您在致电或返回自己时将垃圾留在高32位。即,负担是在接收方忽略高位,而不是在传递方清理高位。

您需要对64位进行符号或零扩展才能在64位有效地址中使用该值。在x32 ABI中,gcc对于每条修改用作数组索引的潜在负整数的指令,经常使用32位有效地址,而不是使用64位操作数大小。



标准:

x86-64 SysV ABI仅说出有关寄存器的哪些部分为_Bool(又名bool)清零的任何内容。第20页:


当类型_Bool的值返回或传递到寄存器中或当
在堆栈中,位0包含真值,位1至7应为
零(脚注14:未指定其他位,因此这些值的使用方在被截断为8位时可以依靠它为0或1)


同样,关于%al的内容包含varargs函数的FP寄存器args的数量,而不是整个%rax

关于open github issue上的这个确切问题,有一个the github page for the x32 and x86-64 ABI documents

ABI对包含args或返回值的整数或向量寄存器的高位部分的内容没有任何进一步的要求或保证,因此没有任何内容。我已经通过Michael Matz(ABI维护者之一)的电子邮件确认了这一事实:“通常,如果ABI没有说指定了某些内容,则您不能依靠它。”

他还证实了clang >= 3.6's use of an addps that could slow down or raise extra FP exceptions with garbage in high elements is a bug(这使我想起我应该报告)。他补充说,这曾经是AMD实现glibc数学函数的问题。当传递标量doublefloat args时,普通的C代码会在向量reg的高元素中留下垃圾。



标准中尚未记录的实际行为:

狭窄的函数参数,甚至_Bool / bool,都被符号化或零扩展为32位。 clang甚至使依赖于此行为的代码(since 2007, apparently)。 ICC17 doesn't do it,因此即使对于C语言,ICC和clang也都不兼容ABI。如果前6个整数args中的任何一个较窄,请不要从x86-64 SysV ABI的ICC编译代码中调用clang编译函数比32位

这不适用于返回值,仅适用于args:gcc和clang都假定它们接收到的返回值仅具有有效的数据,且该类型的宽度最大。例如,gcc将使返回char的函数在%eax的高24位中留下垃圾。

recent thread on the ABI discussion group是一个建议,用于阐明将8位和16位args扩展到32位的规则,并可能实际上修改了ABI以要求这样做。主要的编译器(ICC除外)已经做到了,但这将改变调用者和被调用者之间的合同。

这是一个示例(可以使用其他编译器进行检查,也可以对代码on the Godbolt Compiler Explorer进行调整,在该示例中,我包含了许多简单的示例,这些示例仅演示了一个难题,并且还演示了很多内容):

extern short fshort(short a);
extern unsigned fuint(unsigned int a);

extern unsigned short array_us[];
unsigned short lookupu(unsigned short a) {
  unsigned int a_int = a + 1234;
  a_int += fshort(a);                 // NOTE: not the same calls as the signed lookup
  return array_us[a + fuint(a_int)];
}

# clang-3.8 -O3  for x86-64.    arg in %rdi.  (Actually in %di, zero-extended to %edi by our caller)
lookupu(unsigned short):
    pushq   %rbx                      # save a call-preserved reg for out own use.  (Also aligns the stack for another call)
    movl    %edi, %ebx                # If we didn't assume our arg was already zero-extended, this would be a movzwl (aka movzx)
    movswl  %bx, %edi                 # sign-extend to call a function that takes signed short instead of unsigned short.
    callq   fshort(short)
    cwtl                              # Don't trust the upper bits of the return value.  (This is cdqe, Intel syntax.  eax = sign_extend(ax))
    leal    1234(%rbx,%rax), %edi     # this is the point where we'd get a wrong answer if our arg wasn't zero-extended.  gcc doesn't assume this, but clang does.
    callq   fuint(unsigned int)
    addl    %ebx, %eax                # zero-extends eax to 64bits
    movzwl  array_us(%rax,%rax), %eax # This zero-extension (instead of just writing ax) is *not* for correctness, just for performance: avoid partial-register slowdowns if the caller reads eax
    popq    %rbx
    retq


注意:movzwl array_us(,%rax,2)是等效的,但不能更小。如果我们可以依赖%rax的返回值中fuint()的高位为零,则编译器可以使用array_us(%rbx, %rax, 2)而不是add insn。



性能影响

保留high32的不确定性是有意的,我认为这是一个不错的设计决策。

进行32位运算时可以忽略高32。 A 32-bit operation zero-extends its result to 64-bit for free,因此如果您可以直接在64位寻址模式或64位操作中使用reg,则仅需要一个额外的mov edx, edi或其他内容。

某些函数不会将其args扩展到64位,因此不会节省任何insn,因此对于调用者而言,始终必须这样做会造成潜在的浪费。某些函数使用其arg的方式需要从arg的符号相反的扩展名,因此将其留给被调用者来决定如何正确执行。

但是,无论签名如何,将零扩展到64位对于大多数调用者都是免费的,并且可能是ABI设计的不错选择。由于arg regs无论如何都会被破坏,如果调用方希望在仅通过低32位的调用中保留完整的64位值,则调用者已经需要做一些额外的事情。因此,通常仅在需要64位时才花费额外费用结果,然后再将截断的版本传递给函数。在x86-64 SysV中,可以在RDI中生成结果并使用它,然后在call foo中仅查看EDI。

16位和8位操作数大小通常会导致错误的依赖关系(AMD,P4或Silvermont,以及后来的SnB系列),部分寄存器停顿(SnB之前)或较小的速度减慢(Sandybridge),因此未记录的行为要求将8和16b类型扩展到32b以进行arg传递是有道理的。有关这些微体系结构的更多详细信息,请参见Why doesn't GCC use partial registers?



对于实际代码中的代码大小而言,这可能不是什么大问题,因为微小的函数是/应该为static inline,而arg处理insns只是较大函数的一小部分。当编译器可以看到两个定义时,即使没有内联,过程间优化也可以消除调用之间的开销。 (IDK在实践中,编译器在此方面做得如何。)

我不确定将函数签名更改为使用uintptr_t是否会帮助或损害64位指针的整体性能。我不会担心标量的堆栈空间。在大多数函数中,编译器会推入/弹出足够多的调用保留寄存器(例如%rbx%rbp),以将其自身的变量保留在寄存器中。用于8B溢出而不是4B的少量额外空间可以忽略不计。

就代码大小而言,使用64位值需要在某些insn上使用REX前缀,而这些insn则不需要。如果在将32位值用作数组索引之前需要对32位值进行任何操作,则可以免费将零扩展到64位。如果需要,符号扩展总是需要额外的指令。但是,编译器可以从一开始就进行符号扩展并将其作为64位带符号值使用,以保存指令,但需要更多的REX前缀。 (签名溢出是UB,未定义为环绕,因此编译器通常可以避免使用int iarr[i]在循环内部重做符号扩展。)

在合理范围内,现代CPU通常更关心insn计数而不是insn大小。热代码通常会从具有热代码的CPU中的uop缓存中运行。尽管如此,较小的代码仍可以提高uop缓存的密度。如果您可以在不使用更多或较慢的insns的情况下节省代码大小,那么这是一个胜利,但是通常不值得牺牲其他任何东西,除非它的代码大小很多。

就像一条额外的LEA指令一样,允许[reg + disp8]寻址更多的后续指令,而不是disp32。或者在多个xor eax,eax指令之前使用mov [rdi+n], 0将imm32 = 0替换为寄存器源。 (特别是如果它允许微融合,那么相对RIP +立即数是不可能的,因为真正重要的是前端uop计数,而不是指令计数。)

关于assembly - 向x86-64 ABI的指针添加32位偏移时是否需要符号或零扩展?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36706721/

相关文章:

linux - 在 Linux 中使用汇编语言处理 GUI

assembly - 如何使用 gdb 获取汇编语言中 cmp 使用的值?

c - “b++”的汇编

x86 - 我们是否也将寄存器 RAX、RBX 等称为 R1、R2 等?

c - 我如何编译这个非常大但无聊的 C 源代码?

linux - 为什么数据和堆栈段是可执行的?

linux - 为什么我无法成功将变量内容压入堆栈?

数据段中的汇编函数数据排列

gcc - 如何防止 GCC 在链接时优化期间插入 memset?

c++ - 具有多个递归函数调用的 C++ 中的尾递归