将一个32位偏移量添加到x86-64 ABI的指针时,是否需要符号或零扩展?

简介:我正在查看汇编代码来指导我的优化,并将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证实了这种行为。

这个线程证实了我的发现,但是不清楚符号或零扩展是否必要。 只有在高32位还没有设置的情况下,这个符号/零扩展才是必要的。 但是,x86-64 ABI不会足够聪明,需要吗?

我有点不情愿改变我所有的指针偏移ssize_t,因为注册溢出会增加代码的caching足迹。

是的,你必须假设arg或返回值寄存器的高32位包含垃圾。 不过,你可以在打电话或者回来的时候,把垃圾留在高处。

您需要签名或零扩展到64位才能使用64位有效地址中的值。 在x32 ABI中 ,gcc经常使用32位有效地址,而不是使用64位操作数大小来修改用作数组索引的潜在负数整数。


标准:

x86-64 SysV ABI只是说什么寄存器的哪些部分归零_Bool (aka bool )。 第20页:

_Booltypes的值被返回或传递到寄存器或堆栈中时,位0包含真值,位1至7应为零(注14:其他位未指定,因此这些值的用户端可以当截取到8位时依靠它是0或1)

另外,关于%al的东西持有可变参数函数的FP寄存器参数数量,而不是整个%rax

在github页面的x32和x86-64 ABI文档中有一个关于这个确切问题的开放github问题 。

ABI没有对整数或向量寄存器的高位部分的内容进行任何进一步的要求或保证,这些寄存器保存参数或返回值,所以没有任何要求或保证。 我通过来自Michael Matz(ABI维护人员之一)的电子邮件证实了这一事实:“一般来说,如果ABI没有说明某些内容,则不能依赖它。”

他还证实,例如clang> = 3.6使用了一个addps ,可以减缓或提高额外的FPexception与高元素垃圾是一个错误 (这使我想起我应该报告)。 他补充说,这是AMD实施glibcmath函数的一个问题。 正常的C代码在传递标量doublefloat参数时可能会在向量regs的高元素中留下垃圾。


在标准中尚未logging的实际行为:

窄函数参数,甚至是_Bool / bool ,都是符号或零扩展到32位。 铛甚至使得依赖于这种行为的代码(自2007年以来,显然) 。 ICC17 不这样做 ,所以ICC和铿锵不是ABI兼容的 ,即使是C.不要从ICC编译的代码为x86-64 SysV ABI调用铿锵编译的函数,如果任何前六个整数参数比32位窄。

这不适用于返回值,只有参数:gcc和clang都假定它们接收的返回值只有有效的数据,直到types的宽度。 例如,gcc将使返回char函数在%eax的高24位中留下垃圾。

ABI讨论小组最近的一个主题是澄清将8位和16位参数扩展到32位的规则,也许实际上修改了ABI来要求这样做。 主要的编译器(ICC除外)已经这样做了,但是这将会改变呼叫者和被呼叫者之间的合同。

下面是一个例子(用其他编译器检查一下,或者调整Godbolt编译器资源pipe理器中的代码,其中包括许多简单的例子,只是演示了一个难题,以及这个例子)。

 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)将是等效的,但不能小于。 如果我们可以依赖在fuint()的返回值fuint() %rax的高位清零,编译器可以使用array_us(%rbx, %rax, 2)而不是使用add insn。


性能影响

离开高定义是故意的,我认为这是一个很好的devise决定。

在做32位操作时忽略高32是免费的。 它也免费提供零扩展(除非您可以在64位寻址模式或操作中直接使用该寄存器)。

一些函数不会保存任何insn的参数已经扩展到64位,所以对于调用者来说总是需要这样做是浪费。 一些函数使用它们的参数需要与arg的符号相反的扩展,所以把它留给被调用者来决定如何处理。 (无论签名是零是扩展到64位,对于大多数呼叫者来说都是免费的,因为无论如何,arg regs被破坏了,如果呼叫者想要在一个呼叫中只保留一个完整的64位值,低32)

16位和8位操作数大小常常导致错误的依赖关系(AMD,P4或Silvermont)或部分寄存器失速(SnB之前)或减速(之前的IvB或Haswell),因此需要8和16btypes的无证行为扩展到32b为arg通过是有道理的。


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

我不确定是否更改function签名使用64位types将有助于或伤害整体性能。 我不会担心标量的堆栈空间。 在大多数函数中,编译器推入/popup足够的调用保留寄存器(如%rbx%rbp ),以将自己的variables保存在寄存器中。 8B溢出而不是4B的微小额外空间可以忽略不计。

就代码大小而言,使用64位值需要在某些insn上使用REX前缀,否则就不需要这些前缀。 如果在将32位值用作数组索引之前需要进行任何操作,则零扩展到64位是免费的。 签名扩展总是必需的。 编译器可以对它进行签名扩展,并将其作为64位有符号值从开始保存指令,代价是需要更多的REX前缀。

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

由于EOF的注释表明编译器不能假定用于传递32位参数的64位寄存器的高32位具有任何特定的值。 这使得符号或零扩展是必要的。

防止这种情况的唯一方法是使用64位types作为参数,但是这会将要求将值扩展到调用方,这可能不会有所改进。 虽然我不会太担心注册溢出的大小,但是现在你做这个的方式可能更有可能在扩展之后,原始值将会死掉,并且这将是溢出的64位扩展值。 即使没有死亡,编译器也可能更喜欢溢出64位的值。

如果你真的关心你的内存占用,而且你不需要更大的64位地址空间,你可以看看使用ILP32types的x32 ABI ,但支持完整的64位指令集。