我知道编译器可以有很多前端。每个前端将以编程语言编写的代码转换为内部数据结构。
然后,编译器在该数据结构中进行一些优化。
然后,编译器的后端将该数据结构转换为汇编代码,然后在汇编阶段将汇编代码转换为目标代码。
我的问题如下。
考虑到任何编程语言都被转换成内部数据结构的事实,编译器输出的最终代码对于相同的程序逻辑但对于不同的编程语言是相同的吗?
发布于 2017-09-22 22:44:31
是的,很有可能。但是,语言之间的细微差异可能会导致外观相似的asm不同。前端很少会给后端提供完全相同的输入。对于简单的函数,它可能最终会进行同样的优化,并且通常会对事物使用相同的策略。(例如,在x86上,有多少LEA指令值得使用,而不是乘法。)
例如,在C中,带符号的溢出是未定义的行为,因此
void foo(int *p, int n) {
for (int i = 0; i <= n ; i++) {
p[i] = i/4;
}
}可以假定所有可能的n (包括INT_MAX)最终都会终止,并且i是非负的。
对于一种语言的前端,i++被定义为具有2的补语环绕(或者-fwrapv -fno-strict-overflow的gcc ),i将从==INT_MAX变成一个大的负面的,总是<= INT_MAX。编译器将需要使asm忠实地实现源代码的行为,即使对于传递n == INT_MAX的调用者也是如此,这使得这是一个无限循环,其中i可以为负。
但由于这在C和C++中是未定义的行为,编译器可以假定程序不包含任何UB,因此没有调用者可以传递INT_MAX。它可以假设i在循环中永远不是负数,并且循环trip-count适合int。另请参阅What Every C Programmer Should Know About Undefined Behavior (clang blog)。
非负假设允许它使用简单的右移位来实现i / 4,而不是为负数实现C整数除法语义。
# the p[i] = i/4; part of the inner loop from
# gcc -O3 -fno-tree-vectorize
mov edx, eax # copy the loop counter
sar edx, 2 # i / 4 == i>>2
mov DWORD PTR [rdi+rax*4], edx # store into the array源+ asm输出on the Godbolt compiler explorer。
但是如果定义了带符号的回绕行为,有符号除以常量需要更多的指令,并且数组索引必须考虑到可能的回绕:
# Again *just* the body of the inner loop, without the loop overhead
# gcc -fno-strict-overflow -fwrapv -O3 -fno-tree-vectorize
test eax, eax # set flags (including SF) according to i
lea edx, [rax+3] # edx = i+3
movsx rcx, eax # sign-extend for use in the addressing mode
cmovns edx, eax # copy if !signbit_set(i)
sar edx, 2 # i/4 = i>=0 ? i>>2 : (i+3)>>2;
mov DWORD PTR [rdi+rcx*4], edxC数组索引语法只适用于指针+整数,并且不要求索引是非负的。因此,调用者传递一个指向4 4GB数组中间位置的指针是有效的,该函数最终必须写入该数组。(无限循环也是有问题的,但NVM是这样的。)
正如您所看到的,语言规则中的微小差异要求编译器不进行优化。语言规则之间的差异通常比ISO C++和g++可以实现的定义-签名-包装风格的C++之间的差异更大。
此外,如果“常用”类型在另一种语言中具有不同的宽度或符号,则后端很可能会得到不同的输入,在某些情况下,这将是很重要的。
如果我使用的是unsigned,那么在C和C++中,环绕就是定义的溢出行为。但根据定义,unsigned类型是非负的,因此如果不展开,环绕的可能性就不会对优化产生如此明显的影响。如果循环是从大于0开始的,那么wraparound会引入返回0的可能性,以防万一(例如,x / i是被零除的部分)。
https://stackoverflow.com/questions/46366637
复制相似问题