在 2019 年 LLVM IR 提交了使用编译器****内建的函数(intrinsic calls)替换** ARC 运行时函数的代码。**内建函数可以更容易地扩展LLVM,充分利用硬件的能力来执行原本必须在软件中编码的特殊操作。如从一个地方复制数据到另一个地方,在某些 CPU 类型中可以完全由硬件执行,但在其他类型的CPU中,必须将其编码为正常功能。
使用内建函数,LLVM 可以输出对内建函数的调用,然后由编码器将其转换为目标处理器最有效的形式,或者是专用的机器指令,或者是对实际函数的调用。 从理论上讲,你可以有单独的特殊 IR 指令来覆盖所有这些情况,但是这不是非常可扩展的。随着时间的推移,必须创建的指令数量将显著增加。
Intrinsic函数
Intrinsic 函数是编译器内建的函数,由编译器提供,类似于内联函数。但与内联函数不同的是,因为Intrinsic函数是编译器提供,而编译器与硬件架构联系紧密,因此编译器知道如何利用硬件能力以最优的方式实现这些功能。
通常函数的代码是 inline 插入,避免函数调用开销。LLVM支持 Intrinsic 函数的概念。这些函数的名称和语义可以是预先定义,也可以自定义,要求遵守特定的约定。在有些情况下,可能会调用库函数。例如,在参考文献[1]中列出的函数,都是调用libc。
总的来说,这些Intrinsic函数代表了LLVM语言的一种扩展机制,当添加到语言中时,不要求改变LLVM的任何转化过程。对其它编译器,Intrinsic函数也称为内建函数。
在LLVM中,Intrinsic函数一般是在IR级代码优化时引入的,也就是由前端产生。也可以在程序代码中写Intrinsic函数,并通过前端直接发射。这些函数名的前缀一般是保留字“llvm.”。
LLVM后端选择用最高效的形式将Intrinsic函数转换给硬件执行,可以将Intrinsic函数拆分为一系列机器指令,也可以映射为单独一条机器指令,并直接调用相应的硬件功能。
Intrinsic 函数一般是外部函数,开发者不能在自己的代码中实现,而只能调用,获得Intrinsic函数的地址是非法的
ARC 运行时函数转换为 llvm Intrinsic 函数
void llvm::UpgradeARCRuntime(Module &M) {
// This lambda converts normal function calls to ARC runtime functions to
// intrinsic calls.
auto UpgradeToIntrinsic = [&](const char *OldFunc,
llvm::Intrinsic::ID IntrinsicFunc) {
...
if (Fn->use_empty())
Fn->eraseFromParent();
};
// Unconditionally convert a call to "clang.arc.use" to a call to
// "llvm.objc.clang.arc.use".
// 执行闭包 进行转换
UpgradeToIntrinsic("clang.arc.use", llvm::Intrinsic::objc_clang_arc_use);
// Upgrade the retain release marker. If there is no need to upgrade
// the marker, that means either the module is already new enough to contain
// new intrinsics or it is not ARC. There is no need to upgrade runtime call.
if (!UpgradeRetainReleaseMarker(M))
return;
std::pair<const char *, llvm::Intrinsic::ID> RuntimeFuncs[] = {
{"objc_autorelease", llvm::Intrinsic::objc_autorelease},
...
llvm::Intrinsic::objc_arc_annotation_bottomup_bbend}};
for (auto &I : RuntimeFuncs)
UpgradeToIntrinsic(I.first, I.second);// 执行闭包 进行转换
}
支持的 ARC 相关函数,共计 18 个。
inline bool ModuleHasARC(const Module &M) {
return
M.getNamedValue("llvm.objc.retain") ||
M.getNamedValue("llvm.objc.release") ||
M.getNamedValue("llvm.objc.autorelease") ||
M.getNamedValue("llvm.objc.retainAutoreleasedReturnValue") ||
M.getNamedValue("llvm.objc.unsafeClaimAutoreleasedReturnValue") ||
M.getNamedValue("llvm.objc.retainBlock") ||
M.getNamedValue("llvm.objc.autoreleaseReturnValue") ||
M.getNamedValue("llvm.objc.autoreleasePoolPush") ||
M.getNamedValue("llvm.objc.loadWeakRetained") ||
M.getNamedValue("llvm.objc.loadWeak") ||
M.getNamedValue("llvm.objc.destroyWeak") ||
M.getNamedValue("llvm.objc.storeWeak") ||
M.getNamedValue("llvm.objc.initWeak") ||
M.getNamedValue("llvm.objc.moveWeak") ||
M.getNamedValue("llvm.objc.copyWeak") ||
M.getNamedValue("llvm.objc.retainedObject") ||
M.getNamedValue("llvm.objc.unretainedObject") ||
M.getNamedValue("llvm.objc.unretainedPointer") ||
M.getNamedValue("llvm.objc.clang.arc.use");
}
LLVM IR - 代码表象格式
IR 是 llvm 的灵魂,用来在编译中表现代码,以便自定义优化器对代码做出中间层分析和转换,它支持轻量的运行时优化、跨函数或中间过程优化、整体程序分析、深度的重构转换等等。
It is itself defined as a first class language with well-defined semantics.
define i32 @add1(i32 %a, i32 %b) {
entry:
%tmp1 = add i32 %a, %b
ret i32 %tmp1
}
define i32 @add2(i32 %a, i32 %b) {
entry:
%tmp1 = icmp eq i32 %a, 0
br i1 %tmp1, label %done, label %recurse //跳转
recurse:
%tmp2 = sub i32 %a, 1
%tmp3 = add i32 %b, 1
%tmp4 = call i32 @add2(i32 %tmp2, i32 %tmp3)
ret i32 %tmp4
done:
ret i32 %b
}
上面的 LLVM IR 对应下面 C 代码:
unsigned add1(unsigned a, unsigned b) {
return a+b;
}
// Perhaps not the most efficient way to add two numbers.
unsigned add2(unsigned a, unsigned b) {
if (a == 0) return b;
return add2(a-1, b+1);
}
可以看出,LLVM IR 是一个低级的类似精简虚拟指令集,支持简单指令的线性序列,如 add, subtract, compare, and branch。这些指令都采用三地址格式,具有相同个数的输入,并在一个不同的寄存器中返回结果。LLVM IR 支持标号,有点像是一种奇怪的汇编语言。
与大多数精简指令集不同,LLVM 是强类型的,类型系统简单(e.g., i32 is a 32-bit integer, i32** is a pointer to pointer to 32-bit integer) 与机器相关的细节被抽象掉了。例如,调用规范被抽象成了 call and ret 指令和显示参数;同时 LLVM IR 不使用固定命名的寄存器组,可以使用无限多的临时名称寄存器,以%开始。
LLVM IR 是一门语言,且以三种同构 isomorphic 的形式定义的:
- 上面的文本格式,内存数据结构,由优化器本身检查和修改
- 一个高效且密集的磁盘二进制 bitcode 格式
- LLVM 项目还提供了将磁盘格式从文本转换为二进制的工具:llvm-as 将文本 .ll 文件,汇编成 .bc 文件,其中包含bitcode goop ; llvm-dis 进一步将 .bc 文件转换为.ll文件。
LLVM IR 很有趣,可能是编译器优化器的“完美世界”:与编译器的前端和后端不同,优化器不受特定源语言或特定目标机器的约束。另一方面,它必须很好地服务于两者:它必须设计为易于前端生成,并且具有足够的表达能力,以允许对实际目标执行重要的优化。与前端和后端不同,优化器不局限于特定语言或特定目标机器。LLVM IR 要保持对上下游友好、高效,采用前端容易产生且易于自己做出优化的格式,保留足够的信息允许后端做出重要的优化。
IR 生成
LLVM CodeGen 会负责将前端的输出 AST 自顶向下遍历逐步翻译成 IR,作为后端的输入。
#import <Foundation/Foundation.h>
@interface NSObject1 : NSObject
@property (strong) id test2;
@end
@implementation NSObject1
@end
int main(int argc, char * argv[])
{
NSObject1 *sdfsdf = [NSObject1 new];
sdfsdf.test2 = [NSObject new];
return 0;
}
clang -S -fobjc-arc -emit-llvm main.m -o main.ll IR码生成
//clang++ -S -mllvm --x86-asm-syntax=intel main.m 汇编码生成
不使用内建函数
$ clang -v
Apple LLVM version 10.0.1 (clang-1001.0.46.4)
Target: x86_64-apple-darwin18.7.0
Thread model: posix
InstalledDir: /Applications/
直接调用 objc_storeStrong
使用内建函数
创建工程
cd llvm-project mkdir build cd build
cmake -DLLVM_ENABLE_PROJECTS=clang -G "Xcode" ../llvm
Replace the compiler. There are two files that need to be replaced: the Clang binary, and **libclang.dylib. **Go to
/Applications/Xcode.app/->/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr. clang is in/bin, libclang.dylib is in/lib.
小结
使用新版本的 Clang 对 strong 产生的 IR 码与 Apple LLVM version 10.0.1 (clang-1001.0.46.4) 明显不同。采用 @llvm.objc.* 后,在从 IR 为各个目标机器生成机器码时,形如 @llvm.objc.* 的函数可能得到直接硬件级别的优化提升。
扩展阅读
zhuanlan.zhihu.com/p/53659330 LLVM的Intrinsics函数及其实现
reviews.llvm.org/D55348 相关的代码提交
www.aosabook.org/en/llvm.htm… 架构
http://llvm.org/docs/WritingAnLLVMPass.html
https://clang.llvm.org/get_started.html