gcc是否对模运算进行了优化?

4
考虑下面这个简单的函数,它添加一个常数:
unsigned char f(unsigned char x) {
    return x + 5;
}

使用gcc 4.7.2编译器,加上-O3参数后,将生成以下汇编代码:

leal    5(%rdi), %eax
ret

现在,由于在C语言中无符号溢出是定义良好的行为,因此人们会认为添加模数运算应该基本上是一个nop:

unsigned char f(unsigned char x) {
    return (x + 5) % 256; // assume char is 8-bits, which is typical
}

但是生成的汇编代码多了一条指令:
leal    5(%rdi), %eax
movzbl  %al, %eax
ret

有人能解释一下为什么会这样吗?虽然我不是很熟悉汇编语言。
(注:这只是我为了理解GCC如何优化代码而制作的玩具问题。)

ICC也可以做到,无论是哪种情况,Clang都有movzbl - harold
1个回答

4

如果你想要一个明确的答案来解释“为什么生成的代码不同”,你可能需要一位对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位寄存器的子字寄存器,并且可以对这些子字寄存器执行操作。这可能是不以零/符号扩展方式传递参数的动机。


值得注意的是,C11不再允许使用简单的“AND”指令来优化intvar = intval%256;,而是要求编译器优先考虑几乎从不使用的恒等式(-n)%256 == -(n%256),而不是(n + 256)%256 == n%256。如果想要优化行为,则必须首先转换为无符号。 - supercat
@Bryan:由于我的帖子被踩了,我已经把它删除了 - 所以你可能想根据这个情况编辑你的帖子...我认为我的回答还不错,但是因为没有评论说明原因,我不知道哪里出了问题。 - Mats Petersson
@MatsPetersson 好的,我会做的。如果你问我,这是一个完全不公正的负评。 - Bryan Olivier
@MatsPetersson 不太确定发生了什么,但我认为它非常有用,引发了一些思考。 - Rufflewind
2
@PascalCuoq: 让整数向负无穷处取商会方便某些类型的代码,而且大多数处理器的除法指令都非常慢,清理代码也不会对性能产生太大影响。此外,很多嵌入式设备都具有一些硬件来方便无符号除法,但并非有符号除法。 - supercat
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接