Clang如何编译函数

我打算写一篇关于LLVM如何优化功能的文章,但是首先您需要写Clang如何将C或C ++转换为LLVM。

图片


在不深入探究Clang的情况下考虑高级方面。 我想注意Clang输出与输入之间的关系,而我们不会考虑C ++的非凡特性。 我们使用了这个小功能,我借鉴了关于循环优化的出色讲座

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_sorted.cpp文件编译为C ++,然后告诉LLVM工具链以下内容:不优化,将汇编程序输出为LLVM IR的文本表示形式。 LLVM IR非常庞大,无法快速显示或解析;如果人们不需要查看此代码,则始终首选二进制位代码格式。 是完整的LLVM IR,我们将对其进行部分复审。

让我们从文件顶部开始:

 ; ModuleID = 'is_sorted.cpp' source_filename = "is_sorted.cpp" target datalayout = "em: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 ++代码中的基本相同,只不过i32定义了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时,验证器对于调试很有用。

此基础块中的前四个指令为“ alloca”:分配堆栈内存。 前三个创建变量在编译期间隐式创建,第四个-循环变量。 以这种方式分配的变量只能通过加载和存储指令访问。 以下三个指令初始化了三个堆栈插槽,a.addr和n.addr使用传递给函数的值作为参数进行初始化,而i初始化为零。 返回值不需要初始化,任何在C和C ++中未定义的代码都必须注意这一点。 最后一条指令是到下一个基本单元的无条件转换(我们对此并不担心,大多数不必要的转换将被LLVM后端删除)。

您可能会问:为什么Clang为a和n分配堆栈插槽? 他为什么不直接使用这些值? 在此函数中,由于a和n不变,因此可以使用这种策略,但是优化程序会考虑这种情况,这超出了Calng的权限。 如果可以修改a和n,则它们应该在内存中,而不应该是SSA值,根据定义,它们只能在程序中的某一点取值。 存储单元不在SSA世界范围内,可以随时修改。 这似乎很奇怪,但是这种解决方案使您可以自然而有效地组织编译器所有部分的工作。

我认为Clang是满足SSA所有要求的简并SSA代码生成器,但这仅是因为基本单元之间的信息交换是通过内存进行的。 生成未退化的代码需要一定的关注和分析,而Clang开发人员拒绝这样做是为了区分生成和优化代码的职责。 我没有看到测量结果,但是据我了解,会生成许多内存操作,然后几乎所有的内存操作都会被优化器删除,而不会导致大量的编译时间开销,

考虑一下for循环如何转换。 一般而言,它看起来像这样:

 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使用附加代码的语义),使用slt(符号小于,签署小于),然后最终分支到for.body或for.end块的基础中。

只能从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 

前两行将S和A加载到SSA寄存器中。 然后,我扩展到64位,可以参与计算地址。 getelementptr命令(或简称gep)是LLVM命令,以其自负而著称;它甚至具有自己的帮助部分 。 与机器语言不同,LLVM不会将指针视为整数。 这有助于别名分析和其他内存优化。 此代码加载[i]和[i + 1],比较它们并根据结果执行分支。

if.then块将0保存到该函数的返回值的堆栈插槽中,并无条件跳转到该函数的输出块:

 if.then: store i1 false, i1* %retval, align 1 br label %return 

else块很简单:

 if.end: br label %for.inc 

向循环变量中添加一个的代码块也非常简单:

 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发送的更正)

Source: https://habr.com/ru/post/zh-CN431688/


All Articles