汇编为什么分段执行总是执行不了_iOS汇编教程(六)CPU 指令重排与内存屏障...
系列文章
iOS 匯編入門教程(一)ARM64 匯編基礎(chǔ)
iOS 匯編入門教程(二)在 Xcode 工程中嵌入?yún)R編代碼
iOS 匯編入門教程(三)匯編中的 Section 與數(shù)據(jù)存取
iOS 匯編教程(四)基于 LLDB 動(dòng)態(tài)調(diào)試快速分析系統(tǒng)函數(shù)的實(shí)現(xiàn)
iOS 匯編教程(五)Objc Block 的內(nèi)存布局和匯編表示
前言
具有 ARM 體系結(jié)構(gòu)的機(jī)器擁有相對(duì)較弱的內(nèi)存模型,這類 CPU 在讀寫指令重排序方面具有相當(dāng)大的自由度,為了保證特定的執(zhí)行順序來獲得確定結(jié)果,開發(fā)者需要在代碼中插入合適的內(nèi)存屏障,以防止指令重排序影響代碼邏輯[1]。
本文會(huì)介紹 CPU 指令重排的意義和副作用,并通過一個(gè)實(shí)驗(yàn)驗(yàn)證指令重排對(duì)代碼邏輯的影響,隨后介紹基于內(nèi)存屏障的解決方案,以及在 iOS 開發(fā)中有關(guān)指令重排的注意事項(xiàng)。
指令重排
簡介
以 ARM 為體系結(jié)構(gòu)的 CPU 在執(zhí)行指令時(shí),在遇到寫操作時(shí),如果未獲得緩存段的獨(dú)占權(quán)限,需要基于緩存一致性協(xié)議與其他核協(xié)商,等待直到獲得獨(dú)占權(quán)限時(shí)才能完成這條指令的執(zhí)行;再或者在執(zhí)行乘法指令時(shí)遇到乘法器繁忙的情況,也需要等待。在這些情況下,為了提升程序的執(zhí)行速度,CPU 會(huì)優(yōu)先執(zhí)行一些沒有前序依賴的指令。
一個(gè)例子
看下面一段簡單的程序:
; void acc(int *counter, int *flag);_acc:ldr x8, [x0]add x8, x8, #1str x8, [x0]ldr x9, [x1]mov x9, #1str x9, [x1]ret這段代碼將 counter 的值 +1,并將 flag 置為 1,按照正常的代碼邏輯,CPU 先從內(nèi)存中讀取 counter (x0) 的值累加后回寫,隨后讀取 flag (x1) 的值置位后回寫。
但是如果 x0 所在的內(nèi)存未命中緩存,會(huì)帶來緩存載入的等待,再或者回寫時(shí)無法獲取到緩存段的獨(dú)占權(quán),為了保證多核的緩存一致性,也需要等待;此時(shí)如果 x1 對(duì)應(yīng)的內(nèi)存有緩存段,則可以優(yōu)先執(zhí)行 ldr x9, [x1],同時(shí)由于對(duì) x9 的操作和對(duì) x1 所在內(nèi)存的操作不依賴于對(duì) x8 和 x0 所在內(nèi)存的操作,后續(xù)指令也可以優(yōu)先執(zhí)行,因此 CPU 亂序執(zhí)行的順序可能變成如下這樣:
ldr x9, [x1]mov x9, #1str x9, [x1]ldr x8, [x0]add x8, x8, #1str x8, [x0]甚至如果寫操作都需要等待,還可能將寫操作都滯后:
ldr x9, [x1]mov x9, #1ldr x8, [x0]add x8, x8, #1str x9, [x1]str x8, [x0]再或者如果加法器繁忙,又會(huì)帶來全新的執(zhí)行順序,當(dāng)然這一切都要建立在被重新排序的指令之間不能相互他們依賴執(zhí)行的結(jié)果。
副作用
指令重排大幅度提升了 CPU 的執(zhí)行速度,但凡事都有兩面性,雖然在 CPU 層面重排的指令能保證運(yùn)算的正確性,但在邏輯層面卻可能帶來錯(cuò)誤。比如常見的自旋鎖場景,我們可能設(shè)置一個(gè) bool 類型的 flag 來自旋等待某異步任務(wù)的完成,在這種情況下,一般是在任務(wù)結(jié)束時(shí)對(duì) flag 置位,如果置位 flag 的語句被重排到異步任務(wù)語句的中間,將會(huì)帶來邏輯錯(cuò)誤。下面我們會(huì)通過一個(gè)實(shí)驗(yàn)來直觀展示指令重排帶來的副作用。
一個(gè)實(shí)驗(yàn)
在下面的代碼中我們?cè)O(shè)置了兩個(gè)線程,一個(gè)執(zhí)行運(yùn)算,并在運(yùn)算結(jié)束后置位 flag,另一個(gè)線程自旋等待 flag 置位后讀取結(jié)果。
我們首先定義一個(gè)保存運(yùn)算結(jié)果的結(jié)構(gòu)體。
typedef struct FlagsCalculate { int a; int b; int c; int d; int e; int f; int g;} FlagsCalculate;為了更快的復(fù)現(xiàn)重排帶來的錯(cuò)誤,我們使用了多個(gè) flag 位,存儲(chǔ)在結(jié)構(gòu)體的 e, f, g 三個(gè)成員變量中,同時(shí) a, b, c, d 作為運(yùn)算結(jié)果的存儲(chǔ)變量:
int getCalculated(FlagsCalculate *ctx) { while (ctx->e == 0 || ctx->f == 0 || ctx->g == 0); return ctx->a + ctx->b + ctx->c + ctx->d;}為了更快的觸發(fā)未命中緩存,我們使用了多個(gè)全局變量;為了模擬加法器和乘法器繁忙,我們采用了密集的運(yùn)算:
int mulA = 15;int mulB = 35;int divC = 2;int addD = 20;void calculate(FlagsCalculate *ctx) { ctx->a = (20 * mulA - mulB) / divC; ctx->b = 30 + addD; for (NSInteger i = 0; i < 10000; i++) { ctx->a += i * mulA - mulB; ctx->a *= divC; ctx->b += i * mulB / mulA - mulB; ctx->b /= divC; } ctx->c = mulA + mulB * divC + 120; ctx->d = addD + mulA + mulB + 5; ctx->e = 1; ctx->f = 1; ctx->g = 1;}接下來我們將他們封裝在 pthread 線程的執(zhí)行函數(shù)內(nèi):
void* getValueThread(void *arg) { pthread_setname_np("getValueThread"); FlagsCalculate *ctx = (FlagsCalculate *)arg; int val = getCalculated(ctx); assert(val == -276387); return NULL;}void* calValueThread(void *arg) { pthread_setname_np("calValueThread"); FlagsCalculate *ctx = (FlagsCalculate *)arg; calculate(ctx); return NULL;}void newTest() { FlagsCalculate *ctx = (FlagsCalculate *)calloc(1, sizeof(struct FlagsCalculate)); pthread_t get_t, cal_t; pthread_create(&get_t, NULL, &getValueThread, (void *)ctx); pthread_create(&cal_t, NULL, &calValueThread, (void *)ctx); pthread_detach(get_t); pthread_detach(cal_t);}每次調(diào)用 newTest 即開始一輪新的實(shí)驗(yàn),在 flag 置位未被亂序執(zhí)行的情況下,最終的運(yùn)算結(jié)果是 -276387,通過短時(shí)間內(nèi)不斷并發(fā)執(zhí)行實(shí)驗(yàn),觀察是否遇到斷言即可判斷是否由重排引發(fā)了邏輯異常:
while (YES) { newTest();}筆者在一個(gè) iOS Empty Project 中添加上述代碼,并將其運(yùn)行在一臺(tái) iPhone XS Max 上,約 10 分鐘后,遇到了斷言錯(cuò)誤:
顯然這是由于亂序執(zhí)行導(dǎo)致的 flag 全部被提前置位,從而導(dǎo)致異步線程獲取到的執(zhí)行結(jié)果錯(cuò)誤,通過實(shí)驗(yàn)我們驗(yàn)證了上面的理論。
答疑解惑
看到這里你可能驚出一身冷汗,開始回憶起自己職業(yè)生涯中寫過的類似邏輯,也許線上有很多正在運(yùn)行,但從來沒出過問題,這又是為什么呢?
在 iOS 開發(fā)中,我們常使用 GCD 作為多線程開發(fā)的框架,這類 High Level 的多線程模型本身已經(jīng)提供好了天然的內(nèi)存屏障來保證指令的執(zhí)行順序,因此可以大膽的去寫上述邏輯而不用在意指令重排,這也是我們使用 pthread 來進(jìn)行上述實(shí)驗(yàn)的原因。
到這里你也應(yīng)該意識(shí)到,如果采用 Low Level 的多線程模型來進(jìn)行開發(fā)時(shí),一定要注意指令重排帶來的副作用,下面我們將介紹如何通過內(nèi)存屏障來避免指令重排對(duì)邏輯的影響。
內(nèi)存屏障
簡介
內(nèi)存屏障是一條指令,它能夠明確地保證屏障之前的所有內(nèi)存操作均已完成(可見)后,才執(zhí)行屏障后的操作,但是它不會(huì)影響其他指令(非內(nèi)存操作指令)的執(zhí)行順序[3]。
因此我們只要在 flag 置位前放置內(nèi)存屏障,即可保證運(yùn)算結(jié)果全部寫入內(nèi)存后才置位 flag,進(jìn)而也就保證了邏輯的正確性。
放置內(nèi)存屏障
我們可以通過內(nèi)聯(lián)匯編的形式插入一個(gè)內(nèi)存屏障:
void calculate(FlagsCalculate *ctx) { ctx->a = (20 * mulA - mulB) / divC; ctx->b = 30 + addD; for (NSInteger i = 0; i < 10000; i++) { ctx->a += i * mulA - mulB; ctx->a *= divC; ctx->b += i * mulB / mulA - mulB; ctx->b /= divC; } ctx->c = mulA + mulB * divC + 120; ctx->d = addD + mulA + mulB + 5; __asm__ __volatile__("dmb sy"); ctx->e = 1; ctx->f = 1; ctx->g = 1;}隨后繼續(xù)剛才的試驗(yàn)可以發(fā)現(xiàn),斷言不會(huì)再觸發(fā)異常,內(nèi)存屏障限制了 CPU 亂序執(zhí)行對(duì)正常邏輯的影響。
volatile 與內(nèi)存屏障
我們常常聽說 volatile 是一個(gè)內(nèi)存屏障,那么它的屏障作用是否與上述 DMB 指令一致呢,我們可以試著用 volatile 修飾 3 個(gè) flag,再做一次實(shí)驗(yàn):
typedef struct FlagsCalculate { int a; int b; int c; int d; volatile int e; volatile int f; volatile int g;} FlagsCalculate;結(jié)果最后觸發(fā)了斷言異常,這是為何呢?因?yàn)?volatile 在 C 環(huán)境下僅僅是編譯層面的內(nèi)存屏障,僅能保證編譯器不優(yōu)化和重排被 volatile 修飾的內(nèi)容,但是在 Java 環(huán)境下 volatile 具有 CPU 層面的內(nèi)存屏障作用[4]。不同環(huán)境表現(xiàn)不同,這也是 volatile 讓我們?nèi)绱速M(fèi)解的原因。
在 C 環(huán)境下,volatile 常常用來保證內(nèi)聯(lián)匯編不被編譯優(yōu)化和改變位置,例如我們通過內(nèi)聯(lián)匯編放置一個(gè)編譯層面的內(nèi)存屏障時(shí),通過 __volatile__ 修飾匯編代碼塊來保證內(nèi)存屏障的位置不被編譯器改變:
__asm__ __volatile__("" ::: "memory");總結(jié)
到這里,相信你對(duì)指令重排和內(nèi)存屏障有了更加清晰的認(rèn)識(shí),同時(shí)對(duì) volatile 的作用也更加明確了,希望本文能對(duì)大家有所幫助。
參考資料
[1]緩存一致性(Cache Coherency)入門: https://www.infoq.cn/article/cache-coherency-primer
[2]CPU Reordering – What is actually being reordered?: https://mortoray.com/2010/11/18/cpu-reordering-what-is-actually-being-reordered/
[3]ARM Information Center - DMB, DSB, and ISB: http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0489c/CIHGHHIE.html
[4]volatile 與內(nèi)存屏障總結(jié): https://zhuanlan.zhihu.com/p/43526907
總結(jié)
以上是生活随笔為你收集整理的汇编为什么分段执行总是执行不了_iOS汇编教程(六)CPU 指令重排与内存屏障...的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 小米8SE如何安装google框架-之
- 下一篇: 总结SlickEdit的快捷键,分享当前