【问题标题】:Why doesn't the compiler optimize out these unreachable instructions?为什么编译器不优化这些无法访问的指令?
【发布时间】:2014-08-19 19:16:40
【问题描述】:

鉴于此代码:

int x;

int a (int b) {
    b = a (b);
    b += x;
    return b;
}

为什么 GCC 会返回此输出(英特尔语法):http://goo.gl/8D32F1 - Godbolt 的 GCC Explorer

a:
    sub rsp, 8
    call    a
    mov edx, DWORD PTR x[rip]
    add rsp, 8
    lea eax, [rax+rdx*8]
    add eax, edx
    ret

然后 Clang 返回此输出(AT&T 语法):http://goo.gl/Zz2rKA - Godbolt 的 Clang Explorer

a:                                      # @a
    pushq   %rax
    callq   a
    addl    x(%rip), %eax
    popq    %rdx
    ret

当部分代码明显无法访问时?因为函数的第一条语句是

b = a (b);

该函数将永远递归地调用自身(直到堆栈溢出并且您得到一个段错误)。这意味着你永远不会超出那条线,因此,其余的代码是无法访问的。可达性优化理论上应该去掉代码,对吗?


两个编译器都在 x64 上运行并带有以下标志

  • -O3 - 最大优化
  • -march=native - [不必要] 尽可能使用特定于机器的优化
  • -x c - 假设输入语言是 C

我在想他们应该返回更多类似以下内容(双关语)的东西:

GCC(英特尔语法):

a:
.L1:
    jmp .L1

Clang(AT&T 语法):

a:
.LBB0_1:
    jmp .LBB0_1

注意:这些样本是根据以前的观察结果手工编写的,可能不正确。


总体而言,由于其余代码无法访问,为什么任何一个编译器都不会将函数折叠为单个递归跳转?


编辑:

回应 Jack 关于语义等价的评论:

对于以下代码:

int j (int x) {
    while (1) {};
    x++;
    return x;
}

GCC 返回:http://goo.gl/CYSUW2

j:
.L2:
    jmp .L2

Clang 返回:

j:                                      # @j
.LBB0_1:                                # =>This Inner Loop Header: Depth=1
    jmp .LBB0_1

回应 Adam 关于爆栈的评论:

对于此代码:

int r (int x) {
    return r (x);
}

GCC 生成递归跳转:http://goo.gl/eWo2Nb

r:
.L2:
    jmp .L2

Clang 早早回归:http://goo.gl/CVJKiZ

r:                                      # @r
    ret

【问题讨论】:

  • @StephenC:但这是编译器为所欲为的借口。默默地将“吹堆栈”转换为“永远循环”是完全可以的。
  • @HotLicks - 事实上,为了优化涉及错误/病态代码的情况,几乎可以肯定编译器编写者的努力是一种浪费。对于 OP - 优化的 point 是什么......鉴于代码显然在任何一种情况下都不起作用。
  • @StephenC:没有意义。但是,检查和理解像这样毫无意义的情况可以揭示编译器在不平凡的情况下会做什么。 (例如,在这里,您可能会了解您以前不知道的 gcc 和 clang 死代码消除的限制,以及如何处理它们。)
  • 您的更新显示了尾调用消除。您错过了不能对原始代码执行优化的观点......因为它不是尾调用。 (即使忽略原始示例不会终止。)
  • 从技术角度来看,作为曾经的优化器设计者,除非编译器设置为识别/优化递归,否则它不会注意到调用是递归的,因此不会发现以下代码无法访问。即使观察到递归,处理上述情况也没有任何价值。

标签: c gcc optimization assembly clang


【解决方案1】:

您使用的编译器可能仅在单个函数框架内的块级别实现数据流分析,而不考虑递归。 (或者也许,只有有趣的递归,即尾递归。)由于递归调用不是尾调用,所以从优化的角度来看它并不有趣。

您的函数有问题:它的编译方式会炸毁堆栈。它是这样编译的,因为调用不是尾调用;将其视为一个优化是不合法的。

调用可以被认为是“伪尾调用”,因为调用之后的代码从未被调用,因此如果我们删除该代码,那么递归调用是函数执行的最后一件事。然后我们可以将堆吹代码简化为一个无限循环。但是,这实际上不能称为优化;它是用不同的错误表现形式替换一个错误表现形式。

【讨论】:

  • 嗯。我的愚蠢问题:为什么死代码消除会在递归调用生效后决定内容?
  • @tmyklebu - 因为它不合语法且难以解析。将“决定”改为“决定那个”,意思就更清楚了。
  • @tmyklebu - 答案是它不能确定它是活着的。相反,它没有注意到它已经死了。
  • @StephenC:这确实会改善评论。不幸的是,我修复它为时已晚。
  • 为了确定死代码,编译器正在分析不跨越函数调用的程序的本地图。
猜你喜欢
  • 1970-01-01
  • 2015-05-13
  • 2015-12-27
  • 1970-01-01
  • 2017-09-03
  • 1970-01-01
  • 2011-04-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多