我的问题是:
考虑到任何编程语言都被翻译成该内部数据结构,那么对于不同的编程语言但相同的程序逻辑,编译器输出的最终代码是否相同?
是的,很可能会出现这种情况。但是,语言之间微小的差异可能会导致看起来相似的源代码生成不同的汇编代码。前端很少能够完全按照后端的要求提供输入。对于简单函数,它们可能会被优化为相同的结果,并且通常会使用相同的策略。(例如,在x86上,值得使用多少LEA指令而不是乘法。)
例如,在C语言中,有符号溢出是未定义行为,因此
void foo(int *p, int n) {
for (int i = 0; i <= n ; i++) {
p[i] = i/4;
}
}
但是由于在C和C++中这是未定义行为,编译器可以假设程序不包含任何UB,因此没有调用者可以传递INT_MAX
。它可以假设循环内的i
永远不会为负数,并且循环次数适合于int
。参见每个C程序员都应该知道的未定义行为(clang博客)。
# 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
源代码和汇编输出 在Godbolt编译器探索者上查看。
但是如果定义了有符号数的环绕行为,那么用一个常数进行有符号数除法需要更多的指令,并且数组索引必须考虑到可能的环绕情况。# 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
是的,不同语言编译出的代码最终可能会产生相同的汇编代码。
例如,如果两种不同语言的前端生成相同的中间代码和元数据1,并且应用了相同的优化阶段,则可以保证后端生成相同的代码。在C和C++等密切相关的语言中,往往相同或类似的代码会产生相同的代码,这一点非常容易理解。
以下是一个使用C代码递增指针和使用C++代码递增引用的简单示例。
void inc(int* p) {
(*p)++;
}
在gcc
中使用-O2
inc:
add DWORD PTR [rdi], 1
ret
请在 gcc
和 clang
中自己尝试使用汇编 这里。
类似的代码,但使用 C++ 的引用特性而不是传递指针。
void inc(int& p) { p++; }
在 g++
中使用 -O2
inc(int&):
add DWORD PTR [rdi], 1
ret
在这里使用godbolt进行玩耍。
无论是使用不同的语言还是不同的语言特性(例如C++中的引用,而在C++中不可用),产生的汇编代码都是相同的。
请注意,即使是完全独立的工具链clang
和gcc
也会生成不同的代码-在使用inc
而非add
时,但生成的代码在C和C++之间保持一致。
更有趣的是,即使是使用不同语言的截然不同的代码,也可能产生相同的最终汇编代码。即使前端产生非常不同的中间代码,优化过程也可能将两个输入最终降至相同的输出。当然,并不能保证对于任何特定的输入都是如此,并且它将因编译器和平台而异。
1 通过元数据,我指的是除中间指令本身以外的任何可能影响代码生成的内容。例如,某些语言可能允许更少的优化,如内存重排序,或具有其他行为差异(彼得指出有符号溢出)。目前还不清楚所有这些差异是否直接编码在中间语言中,或者是否每个中间代码束都有相关的元数据描述特定的语义,优化阶段和后端必须遵守。