Clang如何编译函数

作者:John Regehr

原文地址:https://blog.regehr.org/archives/1605

我计划编写一篇关于LLVM如何优化函数的博客,但看起来首先要写清楚Clang如何将CC++翻译为LLVM

这将是相当高层面的:

  • 不是关注Clang的内部,我将关注Clang的输出如何与其输入相关
  • 我们不准备看任何非平凡的C++特性

我们将使用这个小函数,它是我从这些关于循环优化的优秀课堂讲义中借来的:

1

2

3

4

5

6

bool is_sorted(int *a, int n) {

  for (int i = 0; i < n - 1; i++)

    if (a[i] > a[i + 1])

      return false;

  return true;

}

因为Clang不进行任何优化,且因为LLVM IR最初设计为以CC++为目标,这个翻译进行得相对容易。我将使用x86-64上的Clang 6.0.1(或附近的版本,因为它还没有完全发布)。

命令行是:

clang++ is_sorted.cpp -O0 -S -emit-llvm

换而言之:将这个文件is_sort.cpp作为C++编译,然后告诉剩下的LLVM工具链:没有优化,发出汇编,但不是实际发出LLVM文本IR。文本IR是笨重的,对打印或解析不是特别快;在没有涉及人时,二进制“字节码”格式总是首选。完整的IR文件在这里,我们会分几个部分来看。

从文件的开头开始,我们有:

; ModuleID = 'is_sorted.cpp'

source_filename = "is_sorted.cpp"

target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"

target triple = "x86_64-unknown-linux-gnu"

分号与行末的文本都是注释,因此第一行什么都不做,但如果你在意,一个LLVM“模块基本上是一个编译单元:一个代码与数据的容器。第二行与我们无关。第三行描述了编译器的某些选择与假设;它们与本文不太相关,但你可以在这里读到更多目标三元组是一个gcc制度,这里也与我们无关。

LLVM函数带有可选的属性:

; Function Attrs: noinline nounwind optnone uwtable

它们中的一些由前端提供,其他由优化遍在后面添加。这只是一个注释,实际属性在文件末尾指定。这些属性与代码的含义没有任何关系,因此我不准备讨论它们,但如果你感兴趣,可以在这里读到更多。

好了,最后回到函数本身:

define zeroext i1 @_Z9is_sortedPii(i32* %a, i32 %n) #0 {

zeroext表示函数的返回值(i1,一比特整数)应该由后端零扩展到ABI要求的宽度。跟着是函数的重整名,然后是参数列表,与C++代码中的基本相同,除了“int”类型被完善为32比特。#0把这个函数与文件底部的属性组联系起来。

现在来看第一个基本块:

entry:

  %retval = alloca i1, align 1

  %a.addr = alloca i32*, align 8

  %n.addr = alloca i32, align 4

  %i = alloca i32, align 4

  store i32* %a, i32** %a.addr, align 8

  store i32 %n, i32* %n.addr, align 4

  store i32 0, i32* %i, align 4

  br label %for.cond

每条LLVM指令必须存活在一个基本块内:一组仅在顶部进入在底部退出的指令。基本块的最后指令必须是一条终结符指令:直落是不允许的。每个函数必须有一个除了函数入口本身,没有前驱(来自的地方)的进入块。在解析一个IR文件时,检查这些及其他属性,并可能在编译期间由“模块验证器”多次检查。在调试一个遍发出非法IR的情形里,验证器是有用的。

入口基本块的前4条指令是“alloca”:栈内存分配。前3个用于编译期间创建的隐含变量,第4个用于循环归纳变量。像这样的储存分配仅可以使用loadstore指令访问。后3条指令初始化3个栈槽:使用作为参数传入函数的值初始化a.addrn.addr,将i初始化为零。返回值不需要初始化:这将由任何在CC++层面没有未定义的代码处理。最后一条指令是到后续基本块的无条件分支(这里我们不准备操心它,但大多数这些不必要的跳转可以被LLVM后端消除)。

你可能问:为什么Clangan分配栈槽?为什么不直接使用这些变量?在这个函数中,因为an从不改变,这个策略可以工作,但注意这个事实被视为一个优化,因而在Clang的设计考量之外。在一般情形里,其中an可能被修改,它们必须存活在内存中,而不是作为SSA值,SSA——根据定义——在程序中每处上仅有一个给定值。内存单元在SSA世界之外,可以被自由修改。这看起来令人困惑和武断,但人们发现,这些设计决策允许编译器的许多部分以自然和高效的方式表达。

我认为Clang发布退化的SSA代码:它满足SSA的所有要求,但仅因为基本块通过内存通信。发布非退化SSA要求一定程度的谨慎与分析,Clang拒绝做这些导致令人愉悦的关注分离。我没有看过测量结果,但我的理解是,生成大量的内存操作,然后几乎马上优化掉其中的许多,不是编译时开销的主要来源。

接下来让我们看一下如何翻译for循环。一般形式是:

1

2

3

for (initializer; condition; modifier) {

  body

}

这大致翻译为:

  initializer

  goto COND

COND:

  if (condition)

    goto BODY

  else

    goto EXIT

BODY:

  body

  modifier

  goto COND

EXIT:

当然这个翻译不是特定于Clang的:任何CC++编译器做的都一样。

在我们的例子里,循环初始化被折叠进了入口基本块。下一个基本块是循环条件测试:

for.cond:                                         ; preds = %for.inc, %entry

  %0 = load i32, i32* %i, align 4

  %1 = load i32, i32* %n.addr, align 4

  %sub = sub nsw i32 %1, 1

  %cmp = icmp slt i32 %0, %sub

  br i1 %cmp, label %for.body, label %for.end

作为一个有用的注释,Clang告诉我们可以从for.inc或入口基本块到达这个基本块。这个块从内存载入in,递减n(“nsw”标记保留了C++层面的,有符号溢出是未定义的事实;没有这个标记LLVM减法将具有2进制补码的语义),使用“有符号小于”比较递减后的值与i ,最后分支到for.body基本块或for.end基本块。

这个for循环的主体仅可以从for.cond基本块进入:

for.body:

  %2 = load i32*, i32** %a.addr, align 8

  %3 = load i32, i32* %i, align 4

  %idxprom = sext i32 %3 to i64

  %arrayidx = getelementptr inbounds i32, i32* %2, i64 %idxprom

  %4 = load i32, i32* %arrayidx, align 4

  %5 = load i32*, i32** %a.addr, align 8

  %6 = load i32, i32* %i, align 4

  %add = add nsw i32 %6, 1

  %idxprom1 = sext i32 %add to i64

  %arrayidx2 = getelementptr inbounds i32, i32* %5, i64 %idxprom1

  %7 = load i32, i32* %arrayidx2, align 4

  %cmp3 = icmp sgt i32 %4, %7

  br i1 %cmp3, label %if.then, label %if.end

前两行将ai载入SSA寄存器;然后将i加宽到64位,使它可以参予地址计算。getelementptr(简称为gep)是LLVM著名的巴洛克式的指针计算指令,它甚至有自己的faq。不像机器语言,LLVM不以相同的方式处理指针与整数。这有助于别名分析和其他内存优化。然后代码继续载入a[i]a[i+1],比较它们,基于结果分支。

If.then块把0保存到栈槽,作为函数返回值,并无条件分支到函数的退出块:

if.then:

  store i1 false, i1* %retval, align 1

  br label %return

else块是无关紧要的:

if.end:

  br label %for.inc

向循环归纳变量加1的块也是容易的:

for.inc:

  %8 = load i32, i32* %i, align 4

  %inc = add nsw i32 %8, 1

  store i32 %inc, i32* %i, align 4

  br label %for.cond

这个代码分支回循环条件测试。

如果循环正常终止,我们希望返回true

for.end:

  store i1 true, i1* %retval, align 1

  br label %return

最后,任何保存在返回值栈槽的内容被载入并返回:

return:

  %9 = load i1, i1* %retval, align 1

  ret i1 %9

函数底部有更多内容,但不重要。好了,本文比我预想的要长,下一篇将看一下IR层面的优化对这个函数做了什么。

(感谢Xi WangAlex Rosenberg的纠错)。

猜你喜欢

转载自blog.csdn.net/wuhui_gdnt/article/details/83652892