LLVM & 启动优化(二进制重排)

what is LLVM:

LLVM是构架编译器(compiler)的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本。LLVM计划启动于2000年,最初由University of Illinois at Urbana-Champaign的Chris Lattner主持开展。2006年Chris Lattner加盟Apple Inc.并致力于LLVM在Apple开发体系中的应用。Apple也是LLVM计划的主要资助者。
目前LLVM已经被苹果iOS开发工具、Google、Facebook等各大公司采用。

传统编译器的设计

传统编译器.png

  • Frontend 编译器前端的任务是解析源代码。它会进行词法分析、语法分析、语义分析检查源代码是否存在错误,然后构建成语法抽象树(language-specific Abstract Syntax Tree)。而LLVM的前端还会把AST树转换为中间代码IR(LLVM Intermedaite Representaion), 后面的2个阶段都使用这个中间表示。

  • Optimizer 优化器的任务优化代码,比如去除无用的变量或者无用的计算,来提高代码运行效率。

  • Backend 后端 是把优化后的代码转换为目标机器码(target instruction set)。Backend目标是生成充分可以利用目标机器体系结构的native code。

中间代码IR:IR是起到一个桥接作用的。LLVM的第一个阶段输出结果为IR,第二个阶段接收IR进行操作,然后输出IR给第三个阶段。这样针对不同语言,不同架构的机器,就可以单独去设计前端或后端了。


LLVM架构.png

注:OC代码的编译器前端是Clang,swift代码的编译器前端是Swift,他俩的编译器后端都是llvm(这里的llvm是编译器后端的名称,不是整个LLVM架构)。

Clang是LLVM的一个子项目。它是基于LLVM架构的轻量级编译器。它的诞生就是为了替代GCC,提供更快的编译速度。它是负责编译C、C++、OC代码的编译器,它属于整个LLVM架构中的编译器前端。对于开发者来说,研究Clang可以给我们带来很多好处。
编译流程:
通过这个命令可以打印出源码的编译阶段:clang -ccc-print-phases main.m

  1. 输入文件:找到源文件
  2. 预处理阶段:这个过程包括处理宏的替换,头文件的导入。
  3. 编译阶段:进行词法分析、语法分析、语义分析等构件语法树(AST)最终生成IR。
  4. 后端:这里LLVM会通过一个个的Pass去优化,没个Pass做一些优化事情,最终生成汇编代码。
  5. 生成目标文件(.o文件)
  6. 链接:链接需要的动态库和静态库,生成可执行文件。
  7. 通过不同的架构,生成对应的可执行文件。(mach-o文件)

注:xcode7以后开启bitCode,苹果会进一步优化,生成.bc的中间代码。也就是在第二个阶段的Optimizer优化过程中会做进一步优化。

启动优化:

查看App的冷启动时间

启动阶段的耗时.png

pre-main阶段的耗时:(main函数之前)

  • dylib loading time是指动态库的加载时间(dyld要链接动态库),对于这个耗时的建议是能使用苹果自身的framework就用原生的(苹果自身的framework有做了各种优化,链接会快很多),如果是自己封装的framework的话,建议不要多余6个(超过太多个的可以考虑进行合并)。
  • rebase/binding、Objc setup 这两个是系统本身做一下类的加载注册的耗时。这一块能优化的空间比较小。除非你项目中的oc类少一些咯,比如随着项目需求的迭代,可能有一下类是弃用了的,最好就删掉。
  • initializer 这只要是指类的load方法的耗时。我们知道如果一个类有写了load方法,那么这个类的创建就会被提前到main之前了。所以项目中的oc类能不写load方法就不写咯。

main函数之后阶段的耗时:(从main函数开始到第一个界面这阶段)
main函数之后的耗时主要就和项目的业务息息相关了。主要的优化方式有:

  • 在main函数到页面展示出来这个过程中最好不要有耗时的操作在主线程中。尽可能利用多线程。(通过小工具可以检测时间)
  • 类的加载尽可能用懒加载的方式。
  • storyboard和xib是比较耗时的。尽可能在启动阶段的页面用代码来写,不要用storyboard或xib。(因为storyboard和xib也是要解析成代码去执行的,这中间就多了一个解析的操作)
二进制重排(pre-main阶段优化)

1、概念
众所周知操作系统有虚拟内存与物理内存的概念。在很久以前,还没有虚拟内存概念的时候,程序寻址用的都是物理地址。程序能寻址的范围是有限的,这取决于CPU的地址线条数。比如在32位平台下,寻址的范围是2^32也就是4G。并且这是固定的,如果没有虚拟内存,且每次开启一个进程都给4G的物理内存,就可能会出现很多问题:(1) 因为我的物理内存时有限的,当有多个进程要执行的时候,都要给4G内存,很显然你内存小一点,这很快就分配完了,于是没有得到分配资源的进程就只能等待。当一个进程执行完了以后,再将等待的进程装入内存。这种频繁的装入内存的操作是很没效率的。(2)由于指令都是直接访问物理内存的,那么我这个进程就可以修改其他进程的数据,甚至会修改内核地址空间的数据,这是不安全的。(3)因为内存时随机分配的,所以程序运行的地址也是不确定的。
于是针对上面会出现的各种问题,虚拟内存就出来了。
虚拟内存可以认为是每个进程都认为自己拥有4G的空间,这只是每个进程认为的,但是实际上,在虚拟内存对应的物理内存上,可能只对应的一点点的物理内存,实际用了多少内存,就会对应多少物理内存。进程得到的这4G虚拟内存是一个连续的地址空间(这也只是进程认为),而实际上,它通常是被分隔成多个物理内存碎片,还有一部分存储在外部磁盘存储器上,在需要时进行数据交换。

那么进程开始要访问一个地址,它可能会经历下面的过程:

  1. 每次我要访问地址空间上的某一个地址,都需要把地址翻译为实际物理内存地址
  2. 所有进程共享这整一块物理内存,每个进程只把自己目前需要的虚拟地址空间映射到物理内存上
  3. 进程需要知道哪些地址空间上的数据在物理内存上,哪些不在(可能这部分存储在磁盘上),还有在物理内存上的哪里,这就需要通过页表来记录(分页技术
  4. 页表的每一个表项分两部分,第一部分记录此页是否在物理内存上,第二部分记录物理内存页的地址(如果在的话)(在iOS系统上一个页表16k大小。)
  5. 当进程访问某个虚拟地址的时候,就会先去看页表,如果发现对应的数据不在物理内存上,就会发生缺页异常(page fault
  6. 缺页异常的处理过程,操作系统立即阻塞该进程,并将硬盘里对应的页换入内存,然后使该进程就绪,如果内存已经满了,没有空地方了,那就找一个页覆盖,至于具体覆盖的哪个页,就需要看操作系统的页面置换算法是怎么设计的了。


    虚拟内存到物理内存映射过程
分页加载

注:macho文件中内存地址是虚拟内存。程序lldb断点的时候打印出来的内存地址是物理内存地址。

ASLR

ASLR(Address space layout randomization)地址空间布局随机化 是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的。接上一个虚拟地址和物理地址的内容,因为虚拟地址是固定的,所以黑客就很容易的根据虚拟地址去访问内存。为了解决这个问题,ASLR就出现了。虚拟地址不在是直接映射到物理内存的,而是会加上一个随机的地址偏移值。这样就没办法只通过虚拟地址去访问内存了,可以理解为加密算法中的加盐的概念。
在理解了虚拟内存、物理内存、分页技术和ASLR后,二进制重排就是在这种情况下诞生了。

pageFault现象

在上面虚拟内存映射到物理内存的过程中,在第4步,会出现缺页中断(pagefault)现象。当出现缺页中断的时候,操作系统立即阻塞该进程,并将硬盘里对应的页换入内存,然后使该进程就绪,如果内存已经满了,没有空地方了,那就找一个页覆盖。这是个耗时的操作,因为会阻塞进程。而在iOS系统中,当出现pagefault的时候,系统还会对该页进行签名验证,意味着在iOS系统中这个耗时更久了。那么如果我们在启动的过程中出现了很多pagefault的话,那么启动的时间就会大大的延长。so如何在启动的过程中去减少pagefault的出现呢?如果我们App在启动的过程中需要的函数都排在了一个(多个)页表中,这样启动的时候就可以一次加载这些页表,那么是不是就可以减少pagefault的情况呢。将启动代码重新排列在可执行文件的前面,这个重新排列的操作就叫二进制重排。

那么如何进行二进制重排?(下面将已一个我之前写的app项目为例来演示这个过程。)
  1. 如何查看App启动的时间 (添加环境变量DYLD_PRINT_STATISTICS)


    启动时间

    启动时间打印
  1. 如何查看App的启动的page fault(缺页终端)次数


    xcode的instrument工具

    查看page fault次数

从上图中可以看出,总共140.08ms中,page fault占据了121.42ms,将近80%。说明这一块有很大的优化空间。

查看项目默认的符号表.png
项目默认的符号表位置.png

从这个符号表文件可以看出,默认的方法和函数排列的顺序是这样的,比如我们的_main函数就被排到了很后面(6132行)。如下图


符号排列

_main函数默认的符号位置

正如上面说的,二进制重排是我们人为的来排列这些符号的先后顺序,尤其是那些启动的时候就要调用的方法函数,直接排到前面去,分在前一张或前几张页表里,那么就可以减少page falut的次数了。那么问题来了哪些符号是app启动的时候就要调动的呢?他们的顺序是啥?这个怎么确定呢?

Clang插桩 来hook所有的符号和他们的顺序。 别逼逼,直接showCode:

  • 首先在Xcode中添加配置(-fsanitize-coverage=func,trace-pc-guard)


    添加配置

    配置效果.png
  • 添加函数,就是通过这2个函数来hook所有的符号的。

void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                                    uint32_t *stop) {
  static uint64_t N;  // Counter for the guards.
  if (start == stop || *start) return;  // Initialize only once.
  printf("INIT: %p %p\n", start, stop);
  for (uint32_t *x = start; x < stop; x++)
    *x = ++N;  // Guards should start from 1.
}

 void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  if (!*guard) return;  // Duplicate the guard check.
  // If you set *guard to 0 this code will not be called again for this edge.
  // Now you can get the PC and do whatever you want:
  //   store it somewhere or symbolize it and print right away.
  // The values of `*guard` are as you set them in
  // __sanitizer_cov_trace_pc_guard_init and so you can make them consecutive
  // and use them to dereference an array or a bit vector.
  void *PC = __builtin_return_address(0);
  char PcDescr[1024];
  // This function is a part of the sanitizer run-time.
  // To use it, link with AddressSanitizer or other sanitizer.
  __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
  • 最后得到的符号顺序,输出到一个xxx.order 的文件中。


    重排好的order文件.png
  • 把我们重排的xxx.order 文件放到项目的根目录下,并在xcode中Order File 里配置order的执行路径。这样xcode就会把我们自己排序好的符号打包进去,App启动时虚拟内存到物理内存中间的页表里的符号就会按这个order先后顺序来。如下图


    文件的位置

    order的配置路径
  • 最后编译一下,再来看一下启动时间和page fault 的耗时。如图


    重排后的启动时间

    重排后的page fault.png

注:当然上面的所有操作都是为了拿到order文件,所以这个操作只要在发包之前做一次就可以了。

总结:

二进制重排解决的是对pre-main之前的优化,通过对比可以看出优化的效果看出是毫秒级别的优化,其实肉眼根本看不出来效果的差异的....
相比较于二进制重排的优化,还不如项目里垃圾代码写少一些,效果可能更好,哈哈哈哈哈!
故二进制重排是在业务代码已经优化得不能再优化了(没优化空间)的情况下进行的,把启动再优化几十上百毫秒。

不要指望着它能给你带来多么明显的优化效果,你代码本来写的就垃圾,上帝也救不了你的,哈哈哈哈哈,没错,说的就是我!

最后安利一下抖音团队关于二进制重排的文章,也就是抖音把这个技术带火了🔥 。抖音研发实践:基于二进制文件重排的解决方案 APP启动速度提升超15%

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,444评论 4 365
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,867评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,157评论 0 248
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,312评论 0 214
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,673评论 3 289
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,802评论 1 223
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,010评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,743评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,470评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,696评论 2 250
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,187评论 1 262
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,538评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,188评论 3 240
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,127评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,902评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,889评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,741评论 2 274