iOS ARC llvm::Intrinsic优化

1,458 阅读6分钟

在 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函数拆分为一系列机器指令,也可以映射为单独一条机器指令,并直接调用相应的硬件功能。 

zhuanlan.zhihu.com/p/53659330 摘录

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.* 的函数可能得到直接硬件级别的优化提升。 

扩展阅读

reviews.llvm.org/D65902

llvm.org/docs/Extend… 

zhuanlan.zhihu.com/p/53659330   LLVM的Intrinsics函数及其实现

reviews.llvm.org/D55348 相关的代码提交

llvm.org/docs/Extend… 

www.aosabook.org/en/llvm.htm…  架构

http://llvm.org/docs/WritingAnLLVMPass.html  

https://clang.llvm.org/get_started.html