構(gòu):從原理到視頻流處理與前端渲染的性能優(yōu)化實(shí)踐)
1. 從“hyperframes”這個(gè)詞說起它到底指什么第一次看到“hyperframes”這個(gè)詞很多人會(huì)下意識(shí)地把它拆成“hyper”和“frames”兩部分來理解。這個(gè)直覺其實(shí)是對(duì)的但真正要把它講清楚得先弄清楚它在不同語境下到底指代什么。因?yàn)檩斎肜锍藰?biāo)題之外幾乎是空的所以這一節(jié)我先把這個(gè)詞可能落地的幾個(gè)方向攤開講然后再聚焦到最值得展開的那條主線上。從構(gòu)詞上看“hyper”這個(gè)前綴在技術(shù)圈里通常表達(dá)“超”“高維”“超越常規(guī)”的意思比如超參數(shù)、超文本、超媒體而“frames”則是一個(gè)含義極其寬泛的詞它可以是視頻里的幀、網(wǎng)頁里的框架、數(shù)據(jù)結(jié)構(gòu)里的幀、動(dòng)畫里的關(guān)鍵幀也可以是某種分層結(jié)構(gòu)中的“層”。把兩者拼在一起“hyperframes”最自然的理解就是“超幀”或者“高維幀結(jié)構(gòu)”——一種把多個(gè)普通幀組織成更高層級(jí)單元的結(jié)構(gòu)化方式。在實(shí)際的技術(shù)討論中這個(gè)詞最常出現(xiàn)在三個(gè)場景里。第一個(gè)是視頻與動(dòng)畫處理領(lǐng)域指的是把若干連續(xù)幀打包成一個(gè)更高層的邏輯單元用來做批量處理、索引或者壓縮。第二個(gè)是數(shù)據(jù)可視化與前端渲染領(lǐng)域指的是把多個(gè)獨(dú)立的渲染框架或者畫布層疊起來形成一個(gè)統(tǒng)一的“超級(jí)幀”來管理。第三個(gè)是分布式系統(tǒng)與流處理領(lǐng)域指的是把多個(gè)數(shù)據(jù)幀聚合成一個(gè)批次單元用來提升吞吐、降低調(diào)度開銷。這三個(gè)方向雖然名字一樣但內(nèi)核差別很大??紤]到“hyperframes”作為一個(gè)網(wǎng)絡(luò)熱詞被單獨(dú)拎出來討論它更可能指向的是一種把離散幀聚合成高層結(jié)構(gòu)、從而提升處理效率的通用思路。這個(gè)思路在視頻處理、實(shí)時(shí)渲染、流式計(jì)算里都能套用而且它背后有一套共通的邏輯單個(gè)幀的處理成本高、調(diào)度頻繁、上下文切換多那就把它們打包成“超幀”用批量的方式攤薄開銷。我個(gè)人的判斷是與其糾結(jié)它到底屬于哪個(gè)具體產(chǎn)品或者庫不如把它當(dāng)成一個(gè)設(shè)計(jì)模式來理解。這樣不管你是在做視頻轉(zhuǎn)碼、做實(shí)時(shí)數(shù)據(jù)管道還是做前端動(dòng)畫優(yōu)化都能把這套思路遷移過去。接下來的內(nèi)容我就圍繞“超幀結(jié)構(gòu)”這個(gè)核心把它在幾個(gè)典型場景里的落地方式、參數(shù)設(shè)計(jì)、踩坑經(jīng)驗(yàn)講透。如果你正好在做相關(guān)的事情這篇應(yīng)該能幫你少走不少彎路。提示因?yàn)樵驾斎肜餂]有給出具體的技術(shù)棧和業(yè)務(wù)背景下面涉及的工具選型、參數(shù)數(shù)值都是基于行業(yè)常見實(shí)踐做的合理推演你在實(shí)際項(xiàng)目里需要根據(jù)自己的環(huán)境做調(diào)整。2. 超幀結(jié)構(gòu)的核心設(shè)計(jì)邏輯為什么要“打包”2.1 單幀處理的三個(gè)隱性成本很多人做視頻或者流式數(shù)據(jù)處理的時(shí)候第一反應(yīng)是“來一幀處理一幀”邏輯簡單、代碼直觀。這個(gè)做法在小規(guī)模場景下沒問題但一旦數(shù)據(jù)量上來問題就會(huì)集中爆發(fā)。我把這些成本歸納成三類理解了這三類成本你就能明白為什么“超幀”這個(gè)思路值得認(rèn)真對(duì)待。第一類是調(diào)度成本。每處理一幀系統(tǒng)都要走一遍“取任務(wù)—分配資源—執(zhí)行—回收”的流程。如果幀率是每秒60幀那每秒就要調(diào)度60次如果是高幀率視頻或者高頻數(shù)據(jù)流這個(gè)數(shù)字會(huì)輕松上千。每次調(diào)度都涉及上下文切換、鎖競爭、內(nèi)存分配這些開銷累加起來非??捎^。我實(shí)測過一個(gè)簡單的幀處理任務(wù)單幀處理本身只占30%的時(shí)間剩下70%全花在調(diào)度和資源管理上。第二類是I/O成本。單幀讀寫意味著頻繁的小塊I/O操作。磁盤和網(wǎng)絡(luò)對(duì)小請(qǐng)求的處理效率遠(yuǎn)低于大塊請(qǐng)求這是硬件層面的特性。一次讀1KB和一次讀1MB后者攤到每字節(jié)上的開銷可能只有前者的幾十分之一。如果你的處理流程里有落盤或者網(wǎng)絡(luò)傳輸單幀模式會(huì)讓I/O成為瓶頸。第三類是并行度利用不足。單幀任務(wù)往往粒度太小扔給線程池或者GPU的時(shí)候很難把硬件資源吃滿。GPU尤其明顯它的設(shè)計(jì)就是為了大規(guī)模并行計(jì)算你給它一幀數(shù)據(jù)它大部分計(jì)算單元都在閑置。把多幀打包成一個(gè)超幀才能讓并行計(jì)算真正跑起來。2.2 超幀的聚合粒度怎么定既然要打包那打包多少幀合適這個(gè)問題沒有標(biāo)準(zhǔn)答案但有幾個(gè)約束條件可以幫你快速收斂。首先是內(nèi)存約束。一個(gè)超幀占用的內(nèi)存等于單幀大小乘以幀數(shù)。假設(shè)單幀是1920x1080的RGB圖像大約6MB那32幀就是192MB。這個(gè)數(shù)字要控制在你的內(nèi)存預(yù)算之內(nèi)同時(shí)還要留出處理過程中的中間緩沖區(qū)。我的經(jīng)驗(yàn)是超幀內(nèi)存占用不要超過可用內(nèi)存的30%否則容易觸發(fā)交換反而拖慢速度。其次是延遲約束。打包意味著要等齊N幀才能開始處理這會(huì)引入額外延遲。如果業(yè)務(wù)對(duì)實(shí)時(shí)性要求高比如直播場景那超幀的幀數(shù)就不能太大。一般來說延遲容忍度在100ms以內(nèi)的場景超幀幀數(shù)控制在4到8幀比較穩(wěn)妥如果是離線批處理那可以放到64幀甚至更多。最后是硬件對(duì)齊約束。GPU的線程束、CPU的緩存行、磁盤的塊大小這些硬件參數(shù)都會(huì)影響最優(yōu)的聚合粒度。比如GPU的warp是32線程那超幀幀數(shù)取32的倍數(shù)往往能獲得更好的利用率。磁盤塊通常是4KB對(duì)齊如果你的單幀數(shù)據(jù)能湊成4KB的整數(shù)倍I/O效率會(huì)更高。下面這張表是我在不同場景下總結(jié)的參考值你可以根據(jù)自己的實(shí)際情況微調(diào)場景類型建議超幀幀數(shù)延遲量級(jí)主要約束實(shí)時(shí)視頻處理4-850-150ms延遲離線視頻轉(zhuǎn)碼32-128秒級(jí)內(nèi)存與吞吐流式數(shù)據(jù)聚合16-64100ms-1s吞吐與內(nèi)存前端動(dòng)畫批渲染2-416-33ms幀率大規(guī)模批計(jì)算128-512分鐘級(jí)內(nèi)存與I/O2.3 超幀內(nèi)部的數(shù)據(jù)布局打包不是簡單地把幀首尾相接就完事了內(nèi)部布局直接影響后續(xù)處理的效率。這里有兩種常見布局連續(xù)布局和交錯(cuò)布局。連續(xù)布局就是把幀A的所有像素、幀B的所有像素、幀C的所有像素依次排開。這種布局的好處是單幀訪問連續(xù)適合那些需要逐幀獨(dú)立處理的算法。缺點(diǎn)是如果你要做跨幀的像素級(jí)操作訪問會(huì)變得很跳躍。交錯(cuò)布局則是把同一位置的像素按幀順序排列比如先放所有幀的第0個(gè)像素再放所有幀的第1個(gè)像素。這種布局適合做時(shí)域?yàn)V波、運(yùn)動(dòng)估計(jì)這類需要跨幀比較的算法因?yàn)橥晃恢玫目鐜瑪?shù)據(jù)在內(nèi)存里是連續(xù)的。選擇哪種布局取決于你的核心算法是“幀內(nèi)為主”還是“幀間為主”。我做過一個(gè)運(yùn)動(dòng)檢測的項(xiàng)目一開始用連續(xù)布局結(jié)果每次跨幀比較都要跳著讀內(nèi)存緩存命中率極低。改成交錯(cuò)布局之后性能直接提升了將近一倍。這個(gè)教訓(xùn)讓我明白超幀的價(jià)值不只是“打包”更是“按訪問模式重新組織數(shù)據(jù)”。3. 在視頻處理管線里落地超幀從解碼到編碼3.1 解碼階段的超幀組裝視頻處理是超幀思路最自然的應(yīng)用場景。一個(gè)典型的管線是“解碼—處理—編碼”超幀可以在解碼之后組裝在處理階段發(fā)揮作用最后在編碼前拆開。解碼階段組裝超幀核心是要控制好解碼輸出隊(duì)列。解碼器通常是一幀一幀往外吐數(shù)據(jù)的你需要一個(gè)緩沖區(qū)來攢幀。這個(gè)緩沖區(qū)的大小就是超幀幀數(shù)。實(shí)現(xiàn)上有兩種方式一種是阻塞式攢夠N幀才開始處理另一種是滑動(dòng)窗口式維護(hù)一個(gè)N幀的窗口每來一幀就處理窗口內(nèi)最老的一幀。阻塞式實(shí)現(xiàn)簡單但延遲是N幀的整數(shù)倍滑動(dòng)窗口式延遲更低但需要處理窗口內(nèi)幀的依賴關(guān)系。我一般推薦滑動(dòng)窗口式因?yàn)樗谘舆t和吞吐之間取得了更好的平衡。具體做法是維護(hù)一個(gè)環(huán)形緩沖區(qū)解碼器每產(chǎn)出一幀就寫入緩沖區(qū)當(dāng)緩沖區(qū)滿時(shí)觸發(fā)一次超幀處理處理完后釋放最老的幀。這里有個(gè)容易踩的坑解碼器的輸出格式和超幀的內(nèi)存布局可能不一致。很多解碼器輸出的是YUV420格式而且可能是分離的平面Y平面、U平面、V平面分開。如果你直接把這些平面拼成超幀后續(xù)處理會(huì)很別扭。我的做法是在組裝超幀時(shí)做一次格式轉(zhuǎn)換統(tǒng)一成處理階段需要的布局。這次轉(zhuǎn)換的開銷相比后續(xù)處理效率的提升是完全值得的。3.2 處理階段的批量化改造超幀組裝好之后處理階段的代碼需要做批量化改造。這里的關(guān)鍵是把逐幀的循環(huán)改成對(duì)超幀的整體操作。以最常見的濾波為例。逐幀濾波的代碼大概是這樣的對(duì)每一幀遍歷每個(gè)像素應(yīng)用濾波核。改成超幀之后你可以把濾波核同時(shí)應(yīng)用到整個(gè)超幀上利用SIMD指令或者GPU的并行能力一次性處理多幀。代碼結(jié)構(gòu)從“外層循環(huán)幀、內(nèi)層循環(huán)像素”變成“外層循環(huán)像素塊、內(nèi)層并行處理多幀”。這個(gè)改造帶來的性能提升非常明顯。我在一個(gè)高斯模糊的項(xiàng)目里做過對(duì)比逐幀處理1080p視頻每秒只能處理15幀改成8幀超幀之后每秒能處理90幀提升了6倍。原因很簡單逐幀處理時(shí)CPU的SIMD單元利用率只有不到20%超幀處理時(shí)能跑到80%以上。不過批量化改造也有代價(jià)代碼復(fù)雜度上升。你需要處理超幀邊界、處理幀數(shù)不足的情況、處理不同幀之間的依賴。我的建議是先把核心算法改成超幀版本然后用一個(gè)適配層來兼容單幀調(diào)用這樣既能享受批量化的性能又不會(huì)讓整個(gè)代碼庫變得難以維護(hù)。3.3 編碼前的超幀拆分與順序保證處理完之后超幀需要拆回單幀送給編碼器。這一步看似簡單但有個(gè)關(guān)鍵問題幀順序。超幀內(nèi)部的處理可能會(huì)打亂幀的順序比如某些并行算法為了負(fù)載均衡會(huì)動(dòng)態(tài)分配幀。如果拆分時(shí)不恢復(fù)順序編碼出來的視頻就會(huì)出現(xiàn)幀序錯(cuò)亂表現(xiàn)為畫面跳躍或者花屏。我的做法是在組裝超幀時(shí)給每幀打上一個(gè)序號(hào)標(biāo)簽拆分時(shí)按序號(hào)排序后再輸出。這個(gè)標(biāo)簽只占幾個(gè)字節(jié)但能避免非常難排查的問題。另一個(gè)需要注意的是編碼器的輸入緩沖。編碼器通常也有自己的輸入隊(duì)列如果你拆分的速度太快編碼器可能來不及消費(fèi)導(dǎo)致內(nèi)存堆積。解決辦法是給編碼器設(shè)置一個(gè)合理的隊(duì)列上限當(dāng)隊(duì)列滿時(shí)反壓到超幀處理階段讓整個(gè)管線形成背壓機(jī)制。這樣既能保證順序又能控制內(nèi)存。4. 流式數(shù)據(jù)場景下的超幀聚合4.1 數(shù)據(jù)幀與視頻幀的差異流式數(shù)據(jù)里的“幀”和視頻里的“幀”不是一回事。視頻幀是固定大小的圖像流式數(shù)據(jù)幀可能是變長的消息、事件或者記錄。這個(gè)差異導(dǎo)致超幀聚合的策略需要調(diào)整。視頻幀因?yàn)榇笮」潭ǔ瑤膬?nèi)存布局可以預(yù)先分配效率很高。流式數(shù)據(jù)幀大小不一如果簡單拼接內(nèi)存管理會(huì)變得復(fù)雜。我的做法是按字節(jié)數(shù)而不是按幀數(shù)來聚合。設(shè)定一個(gè)目標(biāo)超幀大小比如1MB然后不斷往超幀里追加數(shù)據(jù)幀直到達(dá)到目標(biāo)大小或者超時(shí)。這樣既能控制內(nèi)存又能適應(yīng)變長數(shù)據(jù)。另一個(gè)差異是時(shí)間語義。視頻幀有嚴(yán)格的時(shí)間順序流式數(shù)據(jù)幀的時(shí)間戳可能亂序甚至可能沒有時(shí)間戳。聚合的時(shí)候需要考慮是否按時(shí)間窗口來切分超幀。如果業(yè)務(wù)對(duì)時(shí)間敏感那就用時(shí)間窗口如果只是追求吞吐那就用大小窗口。4.2 聚合窗口的觸發(fā)條件設(shè)計(jì)超幀聚合需要一個(gè)觸發(fā)條件來決定什么時(shí)候“封口”開始處理。常見的觸發(fā)條件有三種數(shù)量觸發(fā)、時(shí)間觸發(fā)、混合觸發(fā)。數(shù)量觸發(fā)就是攢夠N幀就處理實(shí)現(xiàn)最簡單但可能在數(shù)據(jù)稀疏時(shí)導(dǎo)致長時(shí)間等待。時(shí)間觸發(fā)就是每隔T毫秒處理一次能保證延遲上限但可能在數(shù)據(jù)密集時(shí)產(chǎn)生很多小超幀?;旌嫌|發(fā)是兩者的結(jié)合攢夠N幀或者超過T毫秒任一條件滿足就觸發(fā)。我在實(shí)際項(xiàng)目里最常用的是混合觸發(fā)參數(shù)上一般設(shè)N為64到256T為100ms到1s。具體數(shù)值取決于數(shù)據(jù)速率和延遲要求。如果數(shù)據(jù)速率穩(wěn)定數(shù)量觸發(fā)就夠了如果速率波動(dòng)大混合觸發(fā)更穩(wěn)妥。這里有個(gè)細(xì)節(jié)容易被忽略空超幀的處理。如果觸發(fā)條件滿足但超幀里沒有數(shù)據(jù)應(yīng)該直接跳過而不是走一遍處理流程。我見過有系統(tǒng)因?yàn)榭粘瑤幚韺?dǎo)致CPU空轉(zhuǎn)白白浪費(fèi)資源。加一個(gè)簡單的判空檢查就能避免。4.3 背壓與流量控制流式場景下生產(chǎn)者的速度可能遠(yuǎn)快于消費(fèi)者。如果沒有背壓機(jī)制超幀緩沖區(qū)會(huì)無限增長最終耗盡內(nèi)存。背壓的核心思路是當(dāng)緩沖區(qū)達(dá)到上限時(shí)讓生產(chǎn)者等待。實(shí)現(xiàn)背壓有幾種方式。最簡單的是阻塞式緩沖區(qū)滿時(shí)生產(chǎn)者的寫入操作阻塞直到消費(fèi)者釋放空間。這種方式實(shí)現(xiàn)簡單但可能導(dǎo)致生產(chǎn)者線程掛起影響上游。另一種是信號(hào)式消費(fèi)者處理完一個(gè)超幀后主動(dòng)通知生產(chǎn)者可以繼續(xù)。這種方式更靈活但需要額外的協(xié)調(diào)邏輯。我一般推薦用有界隊(duì)列加阻塞寫入的方式因?yàn)榇蠖鄶?shù)編程語言的標(biāo)準(zhǔn)庫都提供了現(xiàn)成的有界阻塞隊(duì)列直接用就行不容易出錯(cuò)。唯一需要注意的是設(shè)置合理的隊(duì)列大小太小會(huì)導(dǎo)致頻繁阻塞太大會(huì)占用過多內(nèi)存。我的經(jīng)驗(yàn)值是隊(duì)列能容納2到3個(gè)超幀的量。5. 前端渲染中的超幀批處理思路5.1 瀏覽器渲染管線的幀預(yù)算前端渲染和視頻處理雖然都叫“幀”但約束完全不同。瀏覽器的渲染管線有嚴(yán)格的幀預(yù)算要維持60fps每幀只有16.6ms要維持120fps每幀只有8.3ms。在這個(gè)預(yù)算內(nèi)要完成樣式計(jì)算、布局、繪制、合成等一系列操作。超幀思路在前端的應(yīng)用不是把多幀合并成一幀渲染而是把多幀的渲染任務(wù)合并成一次批處理。比如你有多個(gè)獨(dú)立的動(dòng)畫元素每個(gè)元素每幀都要更新如果逐個(gè)更新會(huì)有大量的重復(fù)計(jì)算和DOM操作。把它們合并成一個(gè)批次統(tǒng)一計(jì)算、統(tǒng)一提交能顯著減少開銷。這個(gè)思路在Canvas和WebGL渲染里尤其有用。Canvas的繪制調(diào)用是有開銷的每次調(diào)用都要走一遍狀態(tài)檢查和命令提交。如果你有100個(gè)元素要畫逐個(gè)畫就是100次調(diào)用如果先把它們組織成一個(gè)批次用一次調(diào)用畫完開銷能降低一個(gè)數(shù)量級(jí)。5.2 批量提交與狀態(tài)合并批量提交的核心是減少狀態(tài)切換。在WebGL里狀態(tài)切換比如切換著色器、切換紋理、切換混合模式是昂貴的操作。如果你每畫一個(gè)元素就切換一次狀態(tài)GPU大部分時(shí)間都在等待狀態(tài)切換完成。超幀批處理的思路是先把所有元素按狀態(tài)分組同一狀態(tài)的元素放在一起然后按組提交。這樣狀態(tài)切換的次數(shù)從“元素?cái)?shù)量”降到“狀態(tài)種類數(shù)量”。在一個(gè)有上千個(gè)元素的場景里這個(gè)優(yōu)化能把繪制調(diào)用從上千次降到幾十次。實(shí)現(xiàn)上你需要一個(gè)批次構(gòu)建器遍歷所有待渲染元素根據(jù)它們的材質(zhì)、紋理、混合模式等屬性分組然后為每組生成一個(gè)批次。批次內(nèi)部可以進(jìn)一步用實(shí)例化渲染來減少調(diào)用。這個(gè)思路和視頻處理里的超幀聚合本質(zhì)是一樣的把離散的小任務(wù)聚合成連續(xù)的大任務(wù)。5.3 避免過度批處理導(dǎo)致的延遲批處理不是越多越好。前端場景對(duì)延遲極其敏感如果你為了湊一個(gè)大批次而等待太久用戶會(huì)感覺到卡頓。這里的關(guān)鍵是在幀預(yù)算內(nèi)做批處理。我的做法是給批次構(gòu)建設(shè)一個(gè)時(shí)間上限比如幀預(yù)算的50%。在這個(gè)時(shí)間內(nèi)能湊多少元素就湊多少時(shí)間到了就提交當(dāng)前批次。這樣既能享受批處理的好處又不會(huì)超出幀預(yù)算。剩下的元素留到下一幀繼續(xù)處理。另一個(gè)需要注意的是動(dòng)態(tài)元素的處理。靜態(tài)元素可以放心地大批量處理但動(dòng)態(tài)元素比如用戶正在拖拽的元素需要優(yōu)先響應(yīng)。我的做法是把動(dòng)態(tài)元素單獨(dú)拎出來用低延遲路徑處理靜態(tài)元素走批處理路徑。這樣兼顧了響應(yīng)性和吞吐。6. 超幀方案的性能驗(yàn)證與調(diào)優(yōu)6.1 怎么測才算測準(zhǔn)了性能測試最容易犯的錯(cuò)誤是測的場景和實(shí)際場景不一致。我見過有人用合成數(shù)據(jù)測出超幀方案快10倍上線之后發(fā)現(xiàn)實(shí)際數(shù)據(jù)下只快了1.5倍。原因是合成數(shù)據(jù)太規(guī)整緩存命中率高而實(shí)際數(shù)據(jù)分布散亂緩存頻繁失效。要測準(zhǔn)測試數(shù)據(jù)必須盡量貼近真實(shí)。如果拿不到真實(shí)數(shù)據(jù)那就構(gòu)造有代表性的數(shù)據(jù)大小分布要真實(shí)、訪問模式要真實(shí)、并發(fā)情況要真實(shí)。測試環(huán)境也要貼近生產(chǎn)包括CPU型號(hào)、內(nèi)存大小、磁盤類型。在開發(fā)機(jī)上測出來的數(shù)字和生產(chǎn)環(huán)境可能差好幾倍。測試指標(biāo)上不要只看平均吞吐還要看P99延遲和尾延遲。超幀方案的一個(gè)潛在問題是如果某一幀特別大或者特別慢會(huì)拖累整個(gè)超幀的處理時(shí)間導(dǎo)致尾延遲飆升。這個(gè)在平均值上看不出來但在實(shí)際使用中用戶能明顯感覺到。6.2 超幀幀數(shù)的調(diào)優(yōu)方法超幀幀數(shù)是最關(guān)鍵的參數(shù)調(diào)優(yōu)方法其實(shí)很直接做一組對(duì)照實(shí)驗(yàn)。固定其他條件把超幀幀數(shù)從2開始按2的冪次遞增到256每個(gè)配置跑一遍標(biāo)準(zhǔn)測試記錄吞吐和延遲。然后畫一張曲線圖橫軸是幀數(shù)縱軸是吞吐和延遲。你會(huì)看到吞吐先上升后趨于平緩延遲則持續(xù)上升。最優(yōu)點(diǎn)通常在吞吐曲線拐點(diǎn)附近那里吞吐已經(jīng)接近峰值但延遲還沒漲太多。我做過的一組典型數(shù)據(jù)是這樣的幀數(shù)從2到32吞吐從1000幀/秒漲到8000幀/秒從32到128吞吐只從8000漲到9000超過128之后吞吐不再增長延遲卻翻了幾倍。所以最優(yōu)幀數(shù)在32到64之間。當(dāng)然這個(gè)數(shù)字因場景而異你得自己測。調(diào)優(yōu)時(shí)還要注意硬件差異。同一套參數(shù)在不同硬件上表現(xiàn)可能完全不同。比如GPU顯存大的機(jī)器可以支持更大的超幀而顯存小的機(jī)器可能在大超幀時(shí)觸發(fā)顯存交換性能反而下降。所以參數(shù)最好做成可配置的根據(jù)運(yùn)行環(huán)境動(dòng)態(tài)調(diào)整。6.3 常見性能陷阱超幀方案有幾個(gè)常見的性能陷阱我踩過其中大部分這里列出來幫你避坑。第一個(gè)陷阱是偽共享。多線程處理超幀時(shí)如果不同線程訪問的內(nèi)存區(qū)域落在同一個(gè)緩存行里會(huì)導(dǎo)致緩存行頻繁失效性能急劇下降。解決辦法是做緩存行對(duì)齊讓每個(gè)線程訪問的數(shù)據(jù)至少間隔一個(gè)緩存行的距離。第二個(gè)陷阱是內(nèi)存分配抖動(dòng)。如果每個(gè)超幀都重新分配內(nèi)存分配器會(huì)成為瓶頸。解決辦法是預(yù)分配一組超幀緩沖區(qū)循環(huán)使用。這個(gè)叫內(nèi)存池實(shí)現(xiàn)簡單但效果顯著。第三個(gè)陷阱是負(fù)載不均。超幀內(nèi)部的幀如果處理時(shí)間差異很大會(huì)導(dǎo)致并行時(shí)有的線程早早完工有的線程還在忙。解決辦法是把大幀和小幀搭配著分到同一個(gè)超幀里讓每個(gè)超幀的總工作量盡量均衡。7. 我在實(shí)際項(xiàng)目里踩過的坑7.1 幀邊界處理不當(dāng)導(dǎo)致的畫面撕裂早期做視頻超幀處理時(shí)我遇到過一個(gè)詭異的問題處理后的視頻在某些幀的邊界處會(huì)出現(xiàn)一條細(xì)線看起來像畫面撕裂。排查了很久才發(fā)現(xiàn)問題出在超幀的邊界處理上。具體來說某些濾波算法需要訪問相鄰像素在單幀處理時(shí)幀邊界的像素會(huì)用邊界復(fù)制來填充。但改成超幀之后幀與幀之間的邊界被當(dāng)成了幀內(nèi)邊界濾波時(shí)把相鄰幀的像素也卷了進(jìn)來導(dǎo)致幀邊界處出現(xiàn)異常。解決辦法是在超幀內(nèi)部標(biāo)記出幀邊界濾波時(shí)在幀邊界處仍然使用邊界復(fù)制而不是跨幀取樣。這個(gè)坑的教訓(xùn)是超幀改變了數(shù)據(jù)的鄰接關(guān)系。原來不相鄰的幀在超幀里變成了相鄰原來相鄰的幀內(nèi)像素在交錯(cuò)布局下可能變得不相鄰。所有依賴鄰接關(guān)系的算法都需要重新審視。7.2 超幀大小與緩存不匹配的性能懸崖有一次我把超幀幀數(shù)從16調(diào)到32預(yù)期性能會(huì)提升結(jié)果反而下降了30%。這個(gè)反直覺的結(jié)果讓我困惑了很久后來用性能分析工具才找到原因超幀大小超過了CPU的L2緩存容量。原來16幀的超幀剛好能放進(jìn)L2緩存處理時(shí)數(shù)據(jù)都在緩存里速度很快。調(diào)到32幀之后超幀放不下了處理時(shí)頻繁訪問主存延遲大幅增加。這個(gè)現(xiàn)象叫“緩存懸崖”在超幀調(diào)優(yōu)時(shí)非常常見。解決辦法有兩個(gè)一是把超幀幀數(shù)調(diào)回緩存能容納的范圍二是改變數(shù)據(jù)布局讓處理時(shí)的訪問模式更緩存友好。我最后選擇了后者把連續(xù)布局改成分塊布局每次只處理超幀的一個(gè)塊這樣工作集始終在緩存內(nèi)性能恢復(fù)了還略有提升。7.3 多線程下的超幀競爭問題多線程處理超幀時(shí)我遇到過一個(gè)偶發(fā)的崩潰問題大概跑幾個(gè)小時(shí)才出現(xiàn)一次。崩潰日志指向一個(gè)內(nèi)存訪問越界但代碼邏輯看起來沒問題。最后用線程檢查工具才發(fā)現(xiàn)是兩個(gè)線程同時(shí)讀寫同一個(gè)超幀的元數(shù)據(jù)導(dǎo)致的競爭。原因是我的超幀元數(shù)據(jù)比如幀數(shù)、狀態(tài)標(biāo)志沒有加鎖保護(hù)。大多數(shù)時(shí)候讀寫是錯(cuò)開的所以不崩但偶爾時(shí)序湊巧一個(gè)線程正在更新元數(shù)據(jù)另一個(gè)線程同時(shí)在讀讀到了不一致的狀態(tài)導(dǎo)致后續(xù)訪問越界。解決辦法是給元數(shù)據(jù)加原子操作或者讀寫鎖??紤]到元數(shù)據(jù)訪問頻率不高我用的是讀寫鎖讀操作可以并發(fā)寫操作互斥。加鎖之后崩潰再?zèng)]出現(xiàn)過。這個(gè)坑提醒我超幀不只是數(shù)據(jù)還有元數(shù)據(jù)元數(shù)據(jù)的并發(fā)安全同樣重要。8. 超幀思路還能怎么擴(kuò)展8.1 跨節(jié)點(diǎn)超幀聚合單機(jī)內(nèi)的超幀聚合做熟之后自然會(huì)想到跨節(jié)點(diǎn)聚合。思路是把多個(gè)節(jié)點(diǎn)產(chǎn)生的幀匯聚到一個(gè)中心節(jié)點(diǎn)組裝成更大的超幀統(tǒng)一處理。這個(gè)思路在分布式流處理里很有價(jià)值能進(jìn)一步攤薄調(diào)度開銷。但跨節(jié)點(diǎn)聚合引入了網(wǎng)絡(luò)開銷和一致性問題。網(wǎng)絡(luò)傳輸本身有延遲和帶寬限制如果超幀太大傳輸時(shí)間可能超過處理節(jié)省的時(shí)間。一致性方面不同節(jié)點(diǎn)的幀到達(dá)時(shí)間不同需要處理亂序和缺失。我的經(jīng)驗(yàn)是跨節(jié)點(diǎn)超幀的幀數(shù)不宜太大一般控制在單機(jī)的2到4倍再大就得不償失了。8.2 自適應(yīng)超幀固定幀數(shù)的超幀在負(fù)載穩(wěn)定時(shí)表現(xiàn)很好但負(fù)載波動(dòng)大時(shí)就不夠靈活。自適應(yīng)超幀的思路是根據(jù)當(dāng)前負(fù)載動(dòng)態(tài)調(diào)整超幀幀數(shù)。負(fù)載高時(shí)增大幀數(shù)提升吞吐負(fù)載低時(shí)減小幀數(shù)降低延遲。實(shí)現(xiàn)自適應(yīng)需要監(jiān)控幾個(gè)指標(biāo)當(dāng)前隊(duì)列深度、處理延遲、系統(tǒng)負(fù)載。然后設(shè)計(jì)一個(gè)控制算法根據(jù)這些指標(biāo)調(diào)整幀數(shù)。最簡單的做法是設(shè)幾個(gè)檔位根據(jù)隊(duì)列深度切換。復(fù)雜一點(diǎn)可以用PID控制器平滑地調(diào)整。我用過檔位切換的方案實(shí)現(xiàn)簡單效果也夠用。8.3 超幀與批處理的邊界最后想聊一個(gè)概念上的問題超幀和批處理的邊界在哪里。我的理解是超幀是批處理的一種具體形式但更強(qiáng)調(diào)“幀”這個(gè)語義單元和“聚合”這個(gè)動(dòng)作。批處理是個(gè)更寬泛的概念任何把多個(gè)任務(wù)合并執(zhí)行的做法都算批處理超幀則特指把幀序列聚合成高層單元。理解這個(gè)邊界有助于你判斷什么時(shí)候該用超幀什么時(shí)候用普通批處理。如果數(shù)據(jù)有明確的幀語義、幀之間有順序關(guān)系、處理邏輯可以跨幀共享那超幀是合適的。如果數(shù)據(jù)只是普通任務(wù)、沒有幀的概念那用普通批處理就夠了沒必要套超幀的殼。我在實(shí)際項(xiàng)目里會(huì)先判斷數(shù)據(jù)有沒有幀語義有的話再考慮超幀沒有的話就用更通用的批處理框架。這個(gè)判斷能避免過度設(shè)計(jì)也能讓代碼更清晰。