如果编译器的后端对于许多编程语言前端是相同的,那么不同语言的编译对象代码是否相同?

时间:2017-09-22 14:02:48

标签: assembly compilation compiler-construction compiler-optimization

我知道编译器可以有很多前端。每个前端都将用编程语言编写的代码转换为内部数据结构。

然后在该数据结构中,编译器进行了一些优化。

然后编译器的BACK-END将该数据结构转换为汇编代码,然后在汇编阶段将汇编代码转换为目标代码。

我的问题如下。

考虑到任何编程语言都被翻译成内部数据结构的事实,编译器输出的最终代码对于相同的程序逻辑是否相同,但对于不同的编程语言?

2 个答案:

答案 0 :(得分:5)

是的,这很可能。但语言之间的微妙差异可能会导致与看起来相似的来源不同。前端很少会为后端提供相同的相同的输入。对于简单的功能,它可能最终优化相同,并且通常会使用相同类型的策略。 (例如,在x86上,有多少LEA指令值得使用而不是乘法。)

e.g。在C中,签名溢出是未定义的行为,所以

n

可以假设最终终止所有可能的INT_MAX(包括i),并且i++为非负数。

如果语言的前端定义-fwrapv -fno-strict-overflow具有2的补码环绕(或带i的gcc),则==INT_MAX将来自<= INT_MAX总是n == INT_MAX。即使对于通过i的调用者,编译器也需要使asm忠实地实现源代码的行为,这使得INT_MAX可以是负的无限循环。

但由于这是C和C ++中的未定义行为,编译器可以假设程序不包含任何UB,因此没有调用者可以通过i。它可以假设int在循环内从不为负,并且循环行程计数适合i / 4。另请参阅What Every C Programmer Should Know About Undefined Behavior(clang blog)。

非负假设允许它用简单的右移实现# 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 ,而不是为负数实现C整数除法语义。

# 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], edx

来源+ asm输出on the Godbolt compiler explorer

但是如果签名环绕是定义的行为,则常量的带符号除法需要更多指令,并且数组索引必须考虑可能的包装:

unsigned

C数组索引语法只是指针+整数的糖,并不要求索引是非负的。因此调用者将指针传递到4GB数组的中间是有效的,该函数最终必须写入该数组。 (无限循环也是有问题的,但NVM也是如此。)

如您所见,语言规则中的 tiny 差异要求编译器不进行优化。语言规则之间的差异通常大于ISO C ++与g ++可以实现的C ++定义签名环绕风格之间的差异。

此外,如果“常用”类型在另一种语言中的宽度或签名不同,则后端很可能会得到不同的输入,在某些情况下这很重要。

如果我使用过unsigned,那么回绕将是C和C ++中定义的溢出行为。但是根据定义,0类型是非负的,因此在不展开的情况下,环绕的可能性不会对优化产生如此明显的影响。如果循环从大于零开始,那么回绕引入了返回x / i的可能性,以防万一(例如characters = "abcd".split('') characters.map! { |x| x + "!" } characters 除以零)。

答案 1 :(得分:1)

是的,可能以不同语言编译的代码导致相同的最终程序集。

相同或相似的代码

例如,如果两种不同语言的前端生成相同的中间代码和元数据 1 ,并且应用了相同的优化阶段,那么应该保证后端然后生成相同的代码。在C和C ++等密切相关的语言中很容易看出,相同或类似的代码通常会产生相同的代码。

这是一个简单的例子,使用C代码递增指针和C ++代码来增加引用。

C

的增量

来源

void inc(int* p) {
    (*p)++;
}

最终大会

gcc -O2

inc:
        add     DWORD PTR [rdi], 1
        ret

gccclang中使用大会here进行游戏。

C ++

类似的代码,但使用C ++引用功能而不是传递指针。

来源

void inc(int&amp; p){     的p ++; }

装配

g++-O2

inc(int&):
        add     DWORD PTR [rdi], 1
        ret

使用它here on godbolt

尽管使用了不同的语言和不同的语言特性,但在任何一种情况下生成的程序集都是相同的(C ++中的参考文献,在C ++中不可用)。

另请注意clang - 一个完全独立的工具链,使用gcc而不是inc生成的代码与add不同,但生成的代码与C之间的一致和C ++。

不同代码

更有趣的是,即使是完全不同的代码,在不同的语言中也可能产生相同的最终组装。即使前端产生非常不同的中间代码,优化传递也可能最终减少对同一输出的两个输入。但是对于任何特定的输入肯定不能保证,编译器和平台会有很大的不同。

1 通过元数据,我指的是除了中间指令本身之外的任何内容,这可能会影响代码生成。例如,某些语言可能允许更少的优化,例如内存重新排序,或者有其他不同的行为(Peter指出签名溢出)。如果所有这些差异都直接用中间语言编码,或者如果还有与每组中间代码相关联的元数据描述了优化阶段和后端必须遵守的特定语义,那么我并不清楚。