二進(jìn)制重排
二進(jìn)制重排其實(shí)并不是什么特別新穎的技術(shù)勘天。
目的
二進(jìn)制重排(layout)的目的在于將hot code聚合在一起贸宏,即使得最經(jīng)常執(zhí)行的代碼或最需要關(guān)鍵執(zhí)行的代碼(如啟動(dòng)階段的順序調(diào)用)聚合在一起脯倒,形成一個(gè)更緊湊的__TEXT段。
經(jīng)過(guò)Layout后的二進(jìn)制,其高頻或關(guān)鍵代碼排列會(huì)更緊湊铛碑,更利于優(yōu)化startup啟動(dòng)階段,以及mmap out/in(前后臺(tái)切換或函數(shù)調(diào)用)階段的速度和內(nèi)存占用虽界。
- 對(duì)于startup啟動(dòng)階段:
一個(gè)well-layout的二進(jìn)制汽烦,如果使得所有啟動(dòng)階段順序執(zhí)行的代碼按照?qǐng)?zhí)行順序排列在一起,那么整體page faults頻率和次數(shù)會(huì)減少不少莉御。在iphone 6s上撇吞,大概一次page faults平均需要0.2ms或更久俗冻。所以對(duì)于巨型app而言,更少的page faults會(huì)帶來(lái)更大的啟動(dòng)提升牍颈。
- 對(duì)于mmap in階段:
對(duì)于less-well layout的二進(jìn)制迄薄,可能會(huì)存在如下圖問(wèn)題:
如圖:如果存在funA->funB->funC->funD的順序調(diào)用過(guò)程,則上述調(diào)用過(guò)程需要4次page faults煮岁,且均在非相鄰頁(yè)發(fā)生噪奄。那么4次page faults就需要4次頁(yè)中斷,以及4次物理頁(yè)內(nèi)存的占用人乓;假設(shè)程序里存在很多這樣的調(diào)用問(wèn)題勤篮,那么就會(huì)頻繁造成mmap的碎片化,并且導(dǎo)致占用的物理頁(yè)內(nèi)存更多色罚。
而反之碰缔,如果經(jīng)過(guò)了well-layout,如下圖:
則可能只占用了1到2頁(yè)物理內(nèi)存戳护,只觸發(fā)了2次page faults金抡,且是相鄰頁(yè)的page faults;
那上述二者有什么差異呢腌且?
opt\cmp | 頁(yè)中斷 | 物理內(nèi)存 | 耗時(shí) |
---|---|---|---|
well layout | 2 | 2*4kb | 小 |
less-well layout | 4 | 4*4kb | 更大 |
- 總page faults次數(shù)減少50%;
- 總物理內(nèi)存占用減少50%;
- 相鄰頁(yè)page fault耗時(shí)遠(yuǎn)小于非相鄰頁(yè)梗肝;
將以上范圍擴(kuò)大化,對(duì)于大型app而言铺董,運(yùn)行時(shí)會(huì)涉及到很多函數(shù)調(diào)用和切換巫击,所以當(dāng)Layout不當(dāng)時(shí),以上的數(shù)據(jù)會(huì)影響更大精续。這就會(huì)導(dǎo)致幾個(gè)問(wèn)題:
- 前后臺(tái)切換可能更耗時(shí)
- cold launch可能更耗時(shí)
- 運(yùn)行時(shí)需要占用更高內(nèi)存坝锰,更容易OOM
這一點(diǎn)蘋果的上古文檔Improving Locality of Reference里也有提及。
方案
Layout方式總體而言分為如下幾種:
opt\cmp | 原理 | 適用于 | 實(shí)現(xiàn)方 |
---|---|---|---|
Basic block placement | 將hot code排列在一起重付,relayout代碼中低概率執(zhí)行的代碼塊 | 任何代碼尤其是很多分支跳轉(zhuǎn)的代碼 | 編譯器實(shí)現(xiàn) |
Basic block alignment | 使用nop指令將hot code排列在相同cache line | hot loops循環(huán) | 編譯器實(shí)現(xiàn) |
Function splitting | 將函數(shù)中低概率執(zhí)行的代碼抽出來(lái)到新的函數(shù),relayout | 復(fù)雜控制流的函數(shù) | 編譯器實(shí)現(xiàn) |
Function grouping | 將hot function緊湊排列在一起 | small hot function | 鏈接器實(shí)現(xiàn) |
對(duì)于app而言顷级,最簡(jiǎn)單可行的方案是使用linker鏈接器提供的function grouping來(lái)實(shí)現(xiàn)重排。其它都是編譯器內(nèi)部做的優(yōu)化确垫。
對(duì)于lldb而言弓颈,可采取的方案是基于linker提供的-order_file選項(xiàng)。
-order_file
-order_file提供一個(gè)參數(shù)删掀,該參數(shù)為一個(gè)文件路徑翔冀,對(duì)應(yīng)文件的格式要求如下:
- 換行符分隔
每一行是一個(gè)符號(hào),符號(hào)間以換行符分隔
- 注釋以#開(kāi)頭
#text這是一行注釋
- 默認(rèn)為函數(shù)符號(hào)名
_ZThn32_N5AISDK13AIPushManagerD0Ev
-[FMResultSet setStatement:]
- 可指定object file解決符號(hào)沖突
FileModule.o:+[FileModule load]
libhippy.a(RCTEventObserverModule.o):+[RCTEventObserverModule load]
-order_file在當(dāng)前l(fā)lvm上只支持代碼段layout爬迟,即只支持指定函數(shù)符號(hào)來(lái)進(jìn)行重排橘蜜。
而在gdb上則還有-section order等選項(xiàng)可配置特定section的符號(hào)重排菊匿。
備注:雖然man ld文檔里說(shuō)的-order_file支持literal string重排付呕,但經(jīng)過(guò)測(cè)試以及查看llvm源碼發(fā)現(xiàn)计福,目前版本的llvm并不支持。
其它方式
-order_file在iOS上只支持__text代碼段的重排徽职,而對(duì)于其余section象颖,如__cstring,__ustring,__const,__objc等都是不支持重排的。
如果想完成上述重排姆钉,最好的方式是編譯重寫一個(gè)linker说订,當(dāng)然也可以利用默認(rèn)linker的order規(guī)則來(lái)嘗試完成。我們也是基于默認(rèn)order規(guī)則完成的字符串重排潮瓶,但并沒(méi)有什么卵用陶冷,因?yàn)樽址嘏盘嵘皇呛苊黠@。
目前看毯辅,在iOS上除了基于-order_file的代碼段重排外埂伦,基本沒(méi)有別的方式可行了。當(dāng)然另外再自己改llvm編譯當(dāng)我沒(méi)說(shuō)思恐。
trace
基于-order_file完成Machine Code Layout沾谜,我們需要獲取到所有關(guān)鍵的symbol:即函數(shù)符號(hào);
獲取函數(shù)符號(hào)的方式即trace胀莹;
幾種trace方式如下:
opt\cmp | 原理 | 優(yōu)點(diǎn) | 缺點(diǎn) | 舉例 |
---|---|---|---|---|
編譯插樁 | 編譯階段結(jié)合源碼插入樁代碼記錄 | 可實(shí)現(xiàn)對(duì)任何函數(shù)調(diào)用的trace | 需要源碼構(gòu)建基跑,對(duì)于鏈接的二進(jìn)制.a無(wú)效 | XCode PGO |
運(yùn)行時(shí)插樁 | hook或動(dòng)態(tài)插樁來(lái)記錄 | 不需要源碼,可解決二進(jìn)制.a問(wèn)題 | hook無(wú)法解決c/c++問(wèn)題描焰,dtrace無(wú)法解決真機(jī)運(yùn)行問(wèn)題 | dtrace |
基于上述考量媳否,我們是采取編譯插樁+運(yùn)行時(shí)trace的結(jié)合方式,來(lái)生成更好的order_file荆秦。
編譯插樁的方式可以參考FB的方案Performance Scale 2019逆日,或者楊帝寫的 yulingtianxia/AppOrderFiles 更簡(jiǎn)單快速一些。
運(yùn)行時(shí)trace則更多涉及到msgsend hook,block hook,mod_init stub,load stub,initialize hook的一些基礎(chǔ)objc知識(shí)萄凤。
trace objc
msgSend
所有消息轉(zhuǎn)發(fā)基于msgSend所以hook msgSend以及msgSendSuper2即可block
block的本質(zhì)是如下結(jié)構(gòu)體
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室抽。
為什么沒(méi)用descriptor->reserved這個(gè)64位數(shù)?因?yàn)榘l(fā)現(xiàn)對(duì)于globalBlock這個(gè)reserved不能被使用靡努,使用后會(huì)導(dǎo)致block可能執(zhí)行多次或者h(yuǎn)ook失效坪圾。
- load/mod init
所有l(wèi)oad存在__objc_nlclasslist以及__objc_nlcatlist里,基于此去插樁惑朦,mod_init也同理兽泄。
trace string
前面提到我們也完成了字符串重排,這里也簡(jiǎn)略介紹下原理:
字符串重排要解決的是__cstring和__ustring的重排問(wèn)題漾月。__cstring是UTF8 C string病梢。__ustring是unicode string;
他們的本質(zhì)都是一個(gè)如下的結(jié)構(gòu)體:
struct __builtin_CFString {
void *isa; // point to __CFConstantStringClassReference
long flags;
const char *str;
long length;
};
在運(yùn)行時(shí)他們對(duì)應(yīng)的是__NSCFConstantString這個(gè)私有類,也就是只要hook了這個(gè)類的所有消息轉(zhuǎn)發(fā)過(guò)程蜓陌,即可完成對(duì)字符串的trace過(guò)程觅彰。
trace完畢后就利用linker的默認(rèn)排列策略來(lái)去重排字符串即可。
接入
話不多說(shuō)钮热,我們結(jié)合自己的使用場(chǎng)景填抬,完善了一個(gè)sdk,感興趣的同學(xué)可以接入使用隧期。完成生成order_file的步驟飒责,當(dāng)然它也還支持生成order_string。
demo和sdk見(jiàn) https://github.com/rhythmkay/PGOAnalyzer
結(jié)語(yǔ)
Machine Code Layout并不是什么特別新鮮的東西仆潮,它的優(yōu)化效果是有的宏蛉,但在移動(dòng)端上并不會(huì)有特別特別大的效果提升,但本著能提升一點(diǎn)是一點(diǎn)性置,所以還是有意義的檐晕,尤其是啟動(dòng)優(yōu)化,的確還是有些提升效果的蚌讼。
蘋果的那篇上古文檔Improving Locality of Reference辟灰,里面的很多概念和內(nèi)容其實(shí)還是很有價(jià)值的,只不過(guò)無(wú)法使用篡石。
總之芥喇,整個(gè)mach-o二進(jìn)制理論上可以隨意重排,想怎么來(lái)都可以做到凰萨。不外乎要么自己編譯改linker继控,要么利用linker的默認(rèn)排列,要么就是基于linker已有的order_file選項(xiàng)來(lái)胖眷。
另外對(duì)二進(jìn)制重排理論感興趣的同學(xué)武通,可以拜讀下facebook的一篇論文 Optimizing Function Placement for
Large-Scale Data-Center Applications