接上篇文章,传统 PGO 主要优化目标是性能,但经常会牺牲掉包体积大小,这里引入一种兼顾大小和性能的方式。本文内容来源另一篇论文 《Efficient Profile-Guided Size Optimization for Native Mobile Applications》

1、MIP

MIP(Machine IR Profile),在 machine IR(MIR,注意 MIR 与最终机器码是不一样的) 层进行的轻量级插桩,抽取了静态元数据(static metadata,实际上像函数名字、函数指针和 profile 数据指针这些内容,在运行时是并不需要的,但去掉需要解决一致性问题),减少了 2/3 的体积消耗,而且它采集的 profile 数据与移动设备的空间优化更相关。 当然,它也有着更少的运行时开销(在 MIR 进行操作,更关注指令数的影响)。

在 IR 层面上的 PGO,会协助函数 inline 的优化。而 MIR 层面的 PGO,会协助函数布局(function ordering) 和 machine outlining 的优化。(本文下面会介绍 outline 的概念)

MIP 有几种模式可以选择开启(可多选):

  • Function entry timestamps。记录函数第一次执行的时间戳,这里的时间戳是全局的,按执行顺序不断 +1 记录。
  • Function entry call counts。记录函数的执行次数。
  • boolean coverage data for each basic block。记录代码块是否被执行过。
  • return address sampling。在每个被调用函数(callee) 的调用点(call-site),采样的方式记录返回地址值(LR 寄存器的值)。
1
2
3
int global_timestamp = 1; // Run at the entry of each instrumented function.  
if (*timestamp == 0) 
	*timestamp = global_timestamp++;

1.2、代码块

为什么代码块不记录执行次数,而只是简单地记录是否执行过?

相比于一般的程序,移动设备上的 APP,每个函数的代码块数量并不多,更多的是函数调用的指令。(下图把 APP 与 Clang 自身做对比)

所以在移动端,记录代码块的执行次数是非必要的,用来辅助是否需要 outline 优化即可,用来减少包体积。

2、Outliner

outline(外联) 即把一些相同的连续指令串(指令序列),抽出来整合成一个函数,来减少指令数,从而优化包体积。

这里介绍了三种额外的 outliner 方式。

2.1、global outliner

正常情况下,LLVM 的 machine outliner 一次只会在一个模块中运行。

在开启 Full LTO 时,(LTO 概念可以看这篇文章),所有模块被合并成一个模块进行处理,可以有全局视角去进行优化。

但当使用 Thin LTO 时,不同模块的优化就不容易进行了。这里我们会进行两次 MIR 代码生成(machine-level code generation,注意 MIR 与最终机器码是不一样的)。

  • 第一次收集关于可能进行 outline 机会的数据,每个模块的信息会最终整合到一个全局概要文件当中。这里会计算指令串中指令的 hash 值,用全局前缀树来进行存储。
  • 第二次会通过查询指令串 hash 值集合(前缀树),来判断指令串是否有重复,再判断收益是否值得被 outline。然后会通过 link-once One Definition Rule(ODR) 来让链接器去掉冗余的 outline 形成的函数。

2.2、frame outliner

每种语言都有自己的调用约定(calling convention),大多数在函数调用的时候,会包含有 prolog(函数开始时、入栈操作、保存数据) 和 epilog(函数结束时、出栈操作、还原数据)。

这些栈帧指令是可以被 outline 的,但它们的内存操作并不一致,且 LR(link register) 寄存器保存着返回地址,LLVM machine outliner 很难完整把它们抽离。prolog 会有 LR 寄存器的问题,但 epilog 就可以比较完整地抽取了。

2.3、custom outliner

Objective-C 和 Swift 由于要支持 ARC(Automatic-Reference-Counting),会使用到很多的 helper calls(插入了一些辅助运行时引用计数的指令),它们高度重复,但分配的寄存器非常混乱,导致指令串不规则。

所以 custom outliner 会在进行 machine outline 优化之前,在语义上匹配相同指令串(即非完全二进制相同,但它们的执行语义相同),进行 outline,或者先进行规则化地调整指令。

3、优化

通过把调用频繁(hot) 的函数放到一起,减少缺页中断的次数,也可以更好地利用了内存 cache 来优化性能。

而很多优化这时候会忽略了对很少调用(cold) 的函数的处理,如上篇所说,把有更多共同指令的函数排放到一起,提升压缩率,从而也可以优化包体积。

所以优化策略为:对 hot 函数紧密排列优化性能,对 cold 函数/代码块 outline 进行压缩模型操作优化包体积。

参考