簡(jiǎn)談二進(jìn)制重排

二進(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)題:

image.png

如圖:如果存在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,如下圖:

image.png

則可能只占用了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 更大
  1. 總page faults次數(shù)減少50%;
  2. 總物理內(nèi)存占用減少50%;
  3. 相鄰頁(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)題:

  1. 前后臺(tái)切換可能更耗時(shí)
  2. cold launch可能更耗時(shí)
  3. 運(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市珊搀,隨后出現(xiàn)的幾起案子冶忱,更是在濱河造成了極大的恐慌,老刑警劉巖境析,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件囚枪,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡劳淆,警方通過(guò)查閱死者的電腦和手機(jī)链沼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)沛鸵,“玉大人括勺,你說(shuō)我怎么就攤上這事。” “怎么了疾捍?”我有些...
    開(kāi)封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵奈辰,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我拾氓,道長(zhǎng)冯挎,這世上最難降的妖魔是什么底哥? 我笑而不...
    開(kāi)封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任咙鞍,我火速辦了婚禮,結(jié)果婚禮上趾徽,老公的妹妹穿的比我還像新娘续滋。我一直安慰自己,他們只是感情好孵奶,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布疲酌。 她就那樣靜靜地躺著,像睡著了一般了袁。 火紅的嫁衣襯著肌膚如雪朗恳。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天载绿,我揣著相機(jī)與錄音粥诫,去河邊找鬼。 笑死崭庸,一個(gè)胖子當(dāng)著我的面吹牛怀浆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播怕享,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼执赡,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了函筋?” 一聲冷哼從身側(cè)響起沙合,我...
    開(kāi)封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎跌帐,沒(méi)想到半個(gè)月后灌诅,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡含末,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年猜拾,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片佣盒。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡挎袜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情盯仪,我是刑警寧澤紊搪,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站全景,受9級(jí)特大地震影響耀石,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜爸黄,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一滞伟、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧炕贵,春花似錦梆奈、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至鳖轰,卻和暖如春清酥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蕴侣。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工焰轻, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人睛蛛。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓鹦马,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親忆肾。 傳聞我的和親對(duì)象是個(gè)殘疾皇子荸频,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355