简谈二进制重排

二进制重排

二进制重排其实并不是什么特别新颖的技术。

目的

二进制重排(layout)的目的在于将hot code聚合在一起,即使得最经常执行的代码或最需要关键执行的代码(如启动阶段的顺序调用)聚合在一起,形成一个更紧凑的__TEXT段。

经过Layout后的二进制,其高频或关键代码排列会更紧凑,更利于优化startup启动阶段,以及mmap out/in(前后台切换或函数调用)阶段的速度和内存占用。

  • 对于startup启动阶段:

一个well-layout的二进制,如果使得所有启动阶段顺序执行的代码按照执行顺序排列在一起,那么整体page faults频率和次数会减少不少。在iphone 6s上,大概一次page faults平均需要0.2ms或更久。所以对于巨型app而言,更少的page faults会带来更大的启动提升。

  • 对于mmap in阶段:

对于less-well layout的二进制,可能会存在如下图问题:

image.png

如图:如果存在funA->funB->funC->funD的顺序调用过程,则上述调用过程需要4次page faults,且均在非相邻页发生。那么4次page faults就需要4次页中断,以及4次物理页内存的占用;假设程序里存在很多这样的调用问题,那么就会频繁造成mmap的碎片化,并且导致占用的物理页内存更多。

而反之,如果经过了well-layout,如下图:

image.png

则可能只占用了1到2页物理内存,只触发了2次page faults,且是相邻页的page faults;

那上述二者有什么差异呢?

opt\cmp 页中断 物理内存 耗时
well layout 2 2*4kb
less-well layout 4 4*4kb 更大
  1. 总page faults次数减少50%;
  2. 总物理内存占用减少50%;
  3. 相邻页page fault耗时远小于非相邻页;

将以上范围扩大化,对于大型app而言,运行时会涉及到很多函数调用和切换,所以当Layout不当时,以上的数据会影响更大。这就会导致几个问题:

  1. 前后台切换可能更耗时
  2. cold launch可能更耗时
  3. 运行时需要占用更高内存,更容易OOM

这一点苹果的上古文档Improving Locality of Reference里也有提及。

方案

Layout方式总体而言分为如下几种:

opt\cmp 原理 适用于 实现方
Basic block placement 将hot code排列在一起,relayout代码中低概率执行的代码块 任何代码尤其是很多分支跳转的代码 编译器实现
Basic block alignment 使用nop指令将hot code排列在相同cache line hot loops循环 编译器实现
Function splitting 将函数中低概率执行的代码抽出来到新的函数,relayout 复杂控制流的函数 编译器实现
Function grouping 将hot function紧凑排列在一起 small hot function 链接器实现

对于app而言,最简单可行的方案是使用linker链接器提供的function grouping来实现重排。其它都是编译器内部做的优化。

对于lldb而言,可采取的方案是基于linker提供的-order_file选项。

-order_file

-order_file提供一个参数,该参数为一个文件路径,对应文件的格式要求如下:

  • 换行符分隔

每一行是一个符号,符号间以换行符分隔

  • 注释以#开头
#text这是一行注释
  • 默认为函数符号名
_ZThn32_N5AISDK13AIPushManagerD0Ev
-[FMResultSet setStatement:]
  • 可指定object file解决符号冲突
FileModule.o:+[FileModule load]
libhippy.a(RCTEventObserverModule.o):+[RCTEventObserverModule load]

-order_file在当前llvm上只支持代码段layout,即只支持指定函数符号来进行重排。
而在gdb上则还有-section order等选项可配置特定section的符号重排。

备注:虽然man ld文档里说的-order_file支持literal string重排,但经过测试以及查看llvm源码发现,目前版本的llvm并不支持。

其它方式

-order_file在iOS上只支持__text代码段的重排,而对于其余section,如__cstring,__ustring,__const,__objc等都是不支持重排的。
如果想完成上述重排,最好的方式是编译重写一个linker,当然也可以利用默认linker的order规则来尝试完成。我们也是基于默认order规则完成的字符串重排,但并没有什么卵用,因为字符串重排提升不是很明显。

目前看,在iOS上除了基于-order_file的代码段重排外,基本没有别的方式可行了。当然另外再自己改llvm编译当我没说。

trace

基于-order_file完成Machine Code Layout,我们需要获取到所有关键的symbol:即函数符号;
获取函数符号的方式即trace;
几种trace方式如下:

opt\cmp 原理 优点 缺点 举例
编译插桩 编译阶段结合源码插入桩代码记录 可实现对任何函数调用的trace 需要源码构建,对于链接的二进制.a无效 XCode PGO
运行时插桩 hook或动态插桩来记录 不需要源码,可解决二进制.a问题 hook无法解决c/c++问题,dtrace无法解决真机运行问题 dtrace

基于上述考量,我们是采取编译插桩+运行时trace的结合方式,来生成更好的order_file。

编译插桩的方式可以参考FB的方案Performance Scale 2019,或者杨帝写的 yulingtianxia/AppOrderFiles 更简单快速一些。

运行时trace则更多涉及到msgsend hook,block hook,mod_init stub,load stub,initialize hook的一些基础objc知识。

trace objc

  • msgSend
    所有消息转发基于msgSend所以hook msgSend以及msgSendSuper2即可

  • block

block的本质是如下结构体

struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};

typedef void(*BlockInvokeFunction)(void *, ...);
struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved;
    BlockInvokeFunction invoke;
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

因此借助于其int32_t reserved我们完成了block hook。

为什么没用descriptor->reserved这个64位数?因为发现对于globalBlock这个reserved不能被使用,使用后会导致block可能执行多次或者hook失效。

  • load/mod init

所有load存在__objc_nlclasslist以及__objc_nlcatlist里,基于此去插桩,mod_init也同理。

trace string

前面提到我们也完成了字符串重排,这里也简略介绍下原理:
字符串重排要解决的是__cstring和__ustring的重排问题。__cstring是UTF8 C string。__ustring是unicode string;
他们的本质都是一个如下的结构体:

struct __builtin_CFString {
    void *isa; // point to __CFConstantStringClassReference
    long flags;
    const char *str;
    long length;
};

在运行时他们对应的是__NSCFConstantString这个私有类,也就是只要hook了这个类的所有消息转发过程,即可完成对字符串的trace过程。
trace完毕后就利用linker的默认排列策略来去重排字符串即可。

接入

话不多说,我们结合自己的使用场景,完善了一个sdk,感兴趣的同学可以接入使用。完成生成order_file的步骤,当然它也还支持生成order_string。

demo和sdk见 https://github.com/rhythmkay/PGOAnalyzer

结语

Machine Code Layout并不是什么特别新鲜的东西,它的优化效果是有的,但在移动端上并不会有特别特别大的效果提升,但本着能提升一点是一点,所以还是有意义的,尤其是启动优化,的确还是有些提升效果的。
苹果的那篇上古文档Improving Locality of Reference,里面的很多概念和内容其实还是很有价值的,只不过无法使用。

总之,整个mach-o二进制理论上可以随意重排,想怎么来都可以做到。不外乎要么自己编译改linker,要么利用linker的默认排列,要么就是基于linker已有的order_file选项来。

另外对二进制重排理论感兴趣的同学,可以拜读下facebook的一篇论文 Optimizing Function Placement for
Large-Scale Data-Center Applications

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