LLVM为什么会分配冗余变量?

11

这是一个带有枚举定义和main函数的简单C文件:

enum days {MON, TUE, WED, THU};

int main() {
    enum days d;
    d = WED;
    return 0;
}

它被转译为以下LLVM IR:

define dso_local i32 @main() #0 {
  %1 = alloca i32, align 4
  %2 = alloca i32, align 4
  store i32 0, i32* %1, align 4
  store i32 2, i32* %2, align 4
  ret i32 0
}

%2 显然是变量 d,它被赋值为 2。如果直接返回零,%1 对应的是什么?


1
你使用了哪些标志来生成这个IR? - arrowd
1
我认为这与 main 函数之前的初始化有关 (https://godbolt.org/z/kEtS-s)。该链接展示了汇编代码如何映射到源代码。 - Pradeep Kumar
2
@PradeepKumar:确实,如果您将函数的名称更改为除main以外的其他名称,则神秘的额外变量将消失。有趣的是,如果您完全省略return语句(在C中对于main是合法的,并且等同于return 0;),它也会消失。 - Nate Eldredge
enum 看起来完全是一个误导; 如果 main 函数只包含 return 0;return 17;,你也会看到一个不必要的变量(无论哪种情况下,这个额外的变量都被设置为零)。这个额外的变量最终也会出现在汇编代码中。 - Nate Eldredge
1
@macleginn:我不太确定。如果你将main声明为int main(int argc, char **argv),你会看到argcargv被复制到堆栈上,但神秘的零变量仍然存在于它们之外。 - Nate Eldredge
显示剩余4条评论
3个回答

6
这个%1寄存器是由clang生成的,用于处理函数中的多个返回语句。想象一下,你正在编写一个计算整数阶乘的函数。不要像这样:
int factorial(int n){
    int result;
    if(n < 2)
      result = 1;
    else{
      result = n * factorial(n-1);
    }
    return result;
}

你可能会这样做

int factorial(int n){
    if(n < 2)
      return 1;
    return n * factorial(n-1);
}

为什么?因为Clang将插入包含返回值的变量result。太好了。这就是使用%1变量的原因。看一下稍微修改过的代码的ir。
修改后的代码,
enum days {MON, TUE, WED, THU};

int main() {
    enum days d;
    d = WED;
    if(d) return 1;
    return 0;
}

IR,

define dso_local i32 @main() #0 !dbg !15 {
    %1 = alloca i32, align 4
    %2 = alloca i32, align 4
    store i32 0, i32* %1, align 4
    store i32 2, i32* %2, align 4, !dbg !22
    %3 = load i32, i32* %2, align 4, !dbg !23
    %4 = icmp ne i32 %3, 0, !dbg !23
    br i1 %4, label %5, label %6, !dbg !25

 5:                                                ; preds = %0
   store i32 1, i32* %1, align 4, !dbg !26
   br label %7, !dbg !26

 6:                                                ; preds = %0
  store i32 0, i32* %1, align 4, !dbg !27
  br label %7, !dbg !27

 7:                                                ; preds = %6, %5
  %8 = load i32, i32* %1, align 4, !dbg !28
  ret i32 %8, !dbg !28
}

现在你看到了%1派上用场了吧?大多数只有一个返回语句的函数都会被 LLVM 的这个 Pass 去掉这个变量。


2

为什么这很重要-实际问题是什么?

我认为你正在寻找更深层次的答案:LLVM的架构基于相当简单的前端和许多传递。前端必须生成正确的代码,但不一定是好的代码。他们可以做最简单的有效工作。

在这种情况下,Clang生成了几个指令,结果发现没有用途。这通常不是问题,因为LLVM中的某些部分将摆脱多余的指令。 Clang相信会发生这种情况。 Clang不需要避免发出死代码;它的实现可能专注于正确性、简洁性、可测试性等方面。


2
因为Clang已经完成了语法分析,但LLVM甚至还没有开始优化。
Clang前端已生成IR(中间表示),而不是机器代码。这些变量是SSA(单一静态赋值);它们尚未绑定到寄存器,实际上在优化后也永远不会被绑定,因为它们是多余的。
那段代码是源码的某种字面表示。这就是Clang交给LLVM进行优化的内容。基本上,LLVM从那里开始优化。事实上,对于版本10和x86_64,llc -O2最终将生成:
main: # @main
  xor eax, eax
  ret

我理解这个过程的步骤。我想知道为什么一开始会生成这个IR。 - macleginn
你可能认为编译器只有一个单一的过程。实际上,它是由一系列的步骤组成,从Clang前端开始生成IR。它甚至没有生成这个文本IR,而是通过clang -emit-llvm -S file.cpp命令请求生成。Clang实际上生成了一个可序列化的二进制位码版本的IR。LLVM被构建为多个步骤,每个步骤都会接收和优化IR。第一个LLVM步骤从Clang接收IR。之所以使用IR,是因为你可以用Fortran FE替换Clang,以支持另一种语言,但使用相同的优化器和代码生成器。 - Olsonist

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