Yal*_*ang 16 assembly x86-64 abi compiler-optimization sign-extension
简介:我正在查看汇编代码来指导我的优化,并在将int32添加到指针时看到许多符号或零扩展.
void Test(int *out, int offset)
{
out[offset] = 1;
}
-------------------------------------
movslq %esi, %rsi
movl $1, (%rdi,%rsi,4)
ret
Run Code Online (Sandbox Code Playgroud)
起初,我认为我的编译器在添加32位到64位整数时遇到了挑战,但我已经用Intel ICC 11,ICC 14和GCC 5.3证实了这种行为.
这个帖子证实了我的发现,但不清楚是否需要符号或零扩展.仅当尚未设置高32位时,才需要此符号/零扩展.但x86-64 ABI难道不够聪明吗?
我有点不愿意将所有指针偏移更改为ssize_t,因为寄存器溢出会增加代码的缓存占用空间.
Pet*_*des 20
是的,您必须假设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保存FP寄存器的数量的东西是varargs的功能,而不是整体%rax.
关于x32和x86-64 ABI文档的github页面上有一个关于这个确切问题的开放github问题.
ABI没有对包含args或返回值的整数或向量寄存器的高位部分的内容提出任何进一步的要求或保证,因此没有任何要求或保证.我通过Michael Matz(ABI维护者之一)的电子邮件确认了这一事实:"一般来说,如果ABI没有说明某些内容,你就不能依赖它."
他还证实,例如clang> = 3.6使用addps可以减慢或增加额外的FP异常与高元素垃圾是一个错误(这提醒我,我应该报告).他补充说,这是一个AMD执行glibc数学函数的问题.通过标量或算法时,普通C代码可以在向量寄存器的高元素中留下垃圾.doublefloat
窄函数参数,甚至_Bool/ bool,是符号或零扩展到32位.clang甚至制作依赖于这种行为的代码(自2007年以来,显然).ICC17 没有这样做,因此ICC和clang不兼容ABI,即使对于C,也不要从ICC编译的代码中为x86-64 SysV ABI调用clang编译的函数,如果前6个整数args中的任何一个比32位窄.
这不适用于返回值,只有args:gcc和clang都假设它们收到的返回值只有有效数据,直到类型的宽度.例如,gcc将使函数返回char,将垃圾留在高24位%eax.
一个在ABI讨论小组最近线程是澄清规则8位和16位ARGS扩展到32位的建议,也许实际修改ABI要求这一点.主要的编译器(ICC除外)已经做到了,但它将改变呼叫者和被叫者之间的合同.
这是一个例子(与其他编译器一起检查或调整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
Run Code Online (Sandbox Code Playgroud)
注意:movzwl array_us(,%rax,2)相同,但不小.如果我们可以依赖于在返回值中%rax归零的高位fuint(),编译器可以使用array_us(%rbx, %rax, 2)而不是使用addinsn.
保留high32 undefined是故意的,我认为这是一个很好的设计决定.
在执行32位操作时,忽略高32是免费的. 32位操作将其结果零扩展为64位免费,因此mov edx, edi如果您可以直接在64位寻址模式或64位操作中使用reg ,则只需要一个额外的东西.
有些函数不会保存任何insn,因为它们的args已经扩展到64位,因此对于调用者来说,总是必须这样做是一种潜在的浪费.有些函数使用args的方式需要与arg的签名相反的扩展,因此将其留给被调用者以决定做什么工作.
不管签名如何,零扩展到64位对于大多数呼叫者来说都是免费的,并且可能是ABI设计选择的一个很好的选择.由于arg regs无论如何都被破坏了,如果调用者希望在一个只通过低32的调用中保持一个完整的64位值,那么调用者已经需要做一些额外的事情.因此,当你需要一个64位的时候它通常只需要额外的费用.调用之前的结果,然后将截断的版本传递给函数.在x86-64 SysV中,您可以在RDI中生成结果并使用它,然后call foo只查看EDI.
16位和8位操作数大小通常会导致错误依赖(AMD,P4或Silvermont,以及后来的SnB系列),或部分寄存器停顿(pre-SnB)或轻微减速(Sandybridge),因此无证件行为需要8和16b类型扩展到32b的arg传递是有道理的.请参阅为什么GCC不使用部分寄存器?有关这些微体系结构的更多详细信息.
这对于实际代码中的代码大小来说可能不是什么大问题,因为微小的函数是/应该的static inline,并且arg处理insn只是更大函数的一小部分.当编译器可以看到两个定义时,即使没有内联,过程间优化也可以消除调用之间的开销.(IDK编译器在实践中对此做了多少.)
我不确定改变使用的函数签名是否uintptr_t有助于或损害64位指针的整体性能.我不担心标量的堆栈空间.在大多数函数中,编译器推送/弹出足够的调用保留寄存器(如%rbx和%rbp),以使其自己的变量保持在寄存器中.8B溢出而不是4B的一点点额外空间可以忽略不计.
就代码大小而言,使用64位值需要在某些insn上使用REX前缀,否则就不需要.如果在将32位值用作数组索引之前需要任何操作,则零扩展到64位是免费的.如果需要,签名扩展总是需要额外的指令.但编译器可以签名扩展并从头开始使用它作为64位有符号值来保存指令,代价是需要更多的REX前缀.(签名溢出是UB,不限定为环绕,所以编译器可经常避免重做符号扩展的环内具有int i一个使用arr[i]).
在合理范围内,现代CPU通常更关心insn count而不是insn size.热代码通常会从拥有它们的CPU中的uop缓存运行.但是,较小的代码可以提高uop缓存中的密度.如果你可以保存代码大小而不使用更多或更慢的insn,那么这是一个胜利,但通常不值得牺牲任何其他东西,除非它是很多代码大小.
就像可能有一个额外的LEA指令允许[reg + disp8]寻址十几个后来的指令,而不是disp32.或者xor eax,eax在多个mov [rdi+n], 0指令之前用寄存器源替换imm32 = 0.(特别是如果允许微融合,那么RIP相对+立即就不可能,因为真正重要的是前端uop计数,而不是指令计数.)