如果你想要一个明确的答案来解释“为什么生成的代码不同”,你可能需要一位对gcc
编译器细节非常熟悉的工程师。以下是另外几个例子,你可以进行更多的实验:
unsigned char f1(unsigned char x) { return x + 5; }
unsigned char f2(unsigned char x) { return (x + 5) % 256; }
unsigned char f3(unsigned char x) { return (x + 5) % 256U; }
unsigned char f4(unsigned char x) { return (x + 5) & 0xFFU; }
对于 64 位系统,适用于 gcc 版本 4.1.2,我得到的所有这些函数在 64 位和 32 位代码中的代码都是相同的,它们实际上都包含了 movzbl。这可能是 gcc 编译 f1 时的一个 bug(在调用方面可能会进行更正)。这实际上取决于调用约定:64 位寄存器中的 8 位值是否应该被零扩展/符号扩展。我在“System V Application Binary Interface, AMD64 Architecture Processor Supplement”(2005 年 6 月)的草案版本 0.96 中没有找到对此的确切答案。gcc 编译器 4.1.2 似乎采用“宁愿安全也不要出错”的哲学,因为 movzbl 也出现在调用方面。根据我的经验,除非寄存器的部分操作需要操作这些值(这很少见),否则通常需要将这些值进行零扩展/符号扩展。
有趣的是,在我的家用编译器 gcc 版本 4.3.2 中,f2 是通过 and 操作实现的小差异。其他所有函数都只是添加 5,这强烈表明由调用方负责进行零扩展/符号扩展,而它确实做到了。但这是 32 位代码。
如果我在任何架构规范中找到有关在过大的寄存器中进行零扩展/符号扩展的值的确切答案,那么我会告诉您。我也需要在专业上知道这个问题。
为了捍卫您的 gcc 编译器,您正在考虑“小啤酒优化”。正常代码不包含这样的模数,如果编译器在某个地方将这样特殊的模数减少到 and,则很好。对于 %256(vs%256U),需要进行一些值范围分析,以确定 and 是否足够,因为模数是在“有符号”算术中完成的。显然,我的编译器最终确实得出结论,and 足够,但显然太晚以至于无法确定它是否被结果的类型子sume,而在其他情况下确实确定了这一点。这就是我们编译器工程师所谓的“阶段排序问题”。
关于寄存器中值的零扩展/符号扩展问题的更新。
我现在放弃了这个问题,并且需要与一些同事一起继续,因为我没有找到一个确切的声明表明参数 / 函数结果应该被零扩展/符号扩展。
我在上述 ABI 规范中找到了以下与此相关的内容。
布尔值在存储在内存对象中时被存储为单个字节对象,其值始终为 0(false)或 1(true)。当存储在整数寄存器中或作为堆栈上的参数传递时,寄存器的所有 8 个字节都是显著的;任何非零值都被视为 true。
所以布尔类型必须进行零扩展。
对于可能调用使用varargs或stdargs的函数(原型不完整的调用或调用包含省略号(...)的函数),%al
(注14)被用作隐藏参数来指定使用的SSE寄存器数量。 %al
的内容不需要完全匹配寄存器的数量,但必须是使用的SSE寄存器数量的上限,并且在0-8之间。
注14:请注意,%rax
的其余部分未定义,只有%al
的内容被定义。
因此,对于%al
的这种特殊用途,它不需要进行扩展。
考虑到布尔值必须进行零扩展,可以得出其他子字类型也应该进行扩展的结论。更正式地说,可以认为没有任何声明意味着不需要进行零/符号扩展。总的来说,这并不令人满意。
关于寄存器中值的零/符号扩展的更新2。
我已与同事讨论了这个问题。2012年版本0.99的最新ABI已经在布尔参数传递方面进行了修改,这些参数仅被零扩展到8位。这表明,这已被修改为与传递其他子字类型一致,即所有子字类型都不进行零/符号扩展。 AMD64架构还支持半个64位寄存器的子字寄存器,并且可以对这些子字寄存器执行操作。这可能是不以零/符号扩展方式传递参数的动机。
movzbl
。 - harold