作者:John Regehr
原文地址:https://blog.regehr.org/archives/1605
我计划编写一篇关于LLVM如何优化函数的博客,但看起来首先要写清楚Clang如何将C或C++翻译为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最初设计为以C与C++为目标,这个翻译进行得相对容易。我将使用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个用于循环归纳变量。像这样的储存分配仅可以使用load及store指令访问。后3条指令初始化3个栈槽:使用作为参数传入函数的值初始化a.addr与n.addr,将i初始化为零。返回值不需要初始化:这将由任何在C或C++层面没有未定义的代码处理。最后一条指令是到后续基本块的无条件分支(这里我们不准备操心它,但大多数这些不必要的跳转可以被LLVM后端消除)。
你可能问:为什么Clang为a与n分配栈槽?为什么不直接使用这些变量?在这个函数中,因为a与n从不改变,这个策略可以工作,但注意这个事实被视为一个优化,因而在Clang的设计考量之外。在一般情形里,其中a与n可能被修改,它们必须存活在内存中,而不是作为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的:任何C或C++编译器做的都一样。
在我们的例子里,循环初始化被折叠进了入口基本块。下一个基本块是循环条件测试:
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或入口基本块到达这个基本块。这个块从内存载入i与n,递减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
前两行将a与i载入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 Wang与Alex Rosenberg的纠错)。