問題
開發(fā)手機(jī)游戲時(shí)勾笆,常聽到身邊的人傳授經(jīng)驗(yàn):“CPU和GPU是共享一份內(nèi)存的”梧宫,但這句經(jīng)驗(yàn)到底具體指的是什么橡类,仿佛總得不到細(xì)節(jié)精確的回答蛇尚。
因此,本文嘗試以一張貼圖紋理的虛擬內(nèi)存占用為例顾画,就以下問題進(jìn)行分析和解答:
- 是否的確主存顯存共享一份貼圖虛擬內(nèi)存取劫?
- 如果問題1證實(shí)的確只有一份,紋理虛擬內(nèi)存的完整流程是怎樣研侣?Unity將該紋理文件在主存加載好紋理數(shù)據(jù)后:
2.1.直接調(diào)用圖形API傳遞該主存指針谱邪,從而GPU能直接訪問該主存中的紋理數(shù)據(jù)?
2.2. 還是需要調(diào)用圖形API將該主存中的紋理數(shù)據(jù)拷貝到另一份虛擬內(nèi)存中庶诡,以供GPU訪問惦银?拷貝完成后紋理主存部分如何處置?
術(shù)語
為清晰表達(dá)避免概念混淆灌砖,本文采取以下術(shù)語:
物理內(nèi)存(Physical Memory):具體的存儲(chǔ)硬件璧函,各種SDRAM,比如LPDDR是移動(dòng)設(shè)備常用的一種低功耗SDRAM基显。
虛擬內(nèi)存(Virtual Memory):對(duì)物理內(nèi)存的一種邏輯映射蘸吓。
系統(tǒng)內(nèi)存(System Memory/Primary Memory):CPU能讀寫的虛擬內(nèi)存。
顯存(Graphics Memory):GPU能讀寫的虛擬內(nèi)存撩幽。
另外库继,外存(External storage):外部存儲(chǔ),“硬盤”窜醉,在移動(dòng)設(shè)備一般是Flash宪萄。
iOS篇
硬件
如下4圖[1][2]所示,iPhone6只有A8里擁有一塊物理內(nèi)存(1GB LPDDR3 RAM)榨惰,且CPU/GPU晶片中并無物理內(nèi)存(SDRAM)拜英,只有物理內(nèi)存的接口(SDRAM Interface)。
且A8采取PoP封裝(Package on Package)琅催,即將CPU/GPU晶片和物理內(nèi)存豎直排列于A8芯片中居凶,將CPU/GPU晶片移除后,在下一層露出了它倆共用的一塊物理內(nèi)存藤抡。
注侠碧,晶片中有高速Cache緩存,類型為SRAM缠黍。
其他iOS設(shè)備,iPhone盛垦、iPad等湿弦,亦如此,硬件層面腾夯,它們的物理內(nèi)存都為統(tǒng)一內(nèi)存(Unified Memory)架構(gòu)颊埃,即主存和顯存都位于同樣的物理內(nèi)存硬件中。
而桌面電腦一般是分離物理內(nèi)存(Discrete Memory)架構(gòu)蝶俱。
圖形API
自2013年的AppleA7(iPhone 5s)起iOS設(shè)備便支持Metal[3]班利,考慮當(dāng)下(2018)的市場(chǎng)份額,故只討論支持Metal的情況榨呆,而不討論iOS上OpenGLES的情況罗标。
系統(tǒng)層面,Metal支持主存顯存同時(shí)訪問同一塊虛擬內(nèi)存积蜻,即MTLBuffer的options為MTLStorageModeShared[4,5,6]闯割,此情況已無主存顯存之分,Shared模式是Buffer(比如頂點(diǎn)緩存竿拆、索引緩存)的默認(rèn)創(chuàng)建模式宙拉,在iOS中Shared也是紋理緩存的默認(rèn)創(chuàng)建模式。
此時(shí)對(duì)該虛擬內(nèi)存的修改丙笋,會(huì)同時(shí)反饋到CPU和GPU上谢澈,除非CPU準(zhǔn)備好Buffer的內(nèi)容后不再修改,但一旦CPU對(duì)Buffer進(jìn)行了二次修改御板,為避免和GPU的訪問沖突锥忿,需要有一定的同步機(jī)制,比如三重緩沖(Tripple Buffering)[7]怠肋。
Pirvate模式為GPU單獨(dú)訪問的虛擬內(nèi)存敬鬓,主要用于RenderTexture等情況[9],并非當(dāng)前重點(diǎn)笙各。
分析Unity在iOS的實(shí)現(xiàn)
雖然圖形API機(jī)制如此钉答,但不同引擎內(nèi)部實(shí)現(xiàn)大相徑庭,保守起見酪惭,具體結(jié)論應(yīng)以引擎具體邏輯為準(zhǔn)希痴。
先以紋理為例者甲,Unity在iOS+Metal上從紋理文件存儲(chǔ)到最終紋理顯存春感,其二進(jìn)制流的完整流程是怎樣的?
人肉閱讀分析Unity源碼是耗時(shí)且可能不準(zhǔn)確的。結(jié)合Profiler等工具進(jìn)行分析鲫懒,會(huì)省時(shí)精確嫩实,事半功倍。這樣也可順帶對(duì)Profile工具的綜合應(yīng)用進(jìn)行介紹窥岩。所以下面甲献,先假設(shè)我們不知道Metal的機(jī)制,試從現(xiàn)象推斷出原因颂翼。
先創(chuàng)建一個(gè)名為GFXMemory的測(cè)試demo晃洒,分別有3張分辨率足夠大的4096x4096的紋理貼圖,格式分別設(shè)為RGBA32朦乏、RGB24球及、ASTC5x5,通過運(yùn)行時(shí)點(diǎn)擊對(duì)應(yīng)的區(qū)域呻疹,才單獨(dú)加載對(duì)應(yīng)貼圖吃引,顯示在屏幕中。
準(zhǔn)備做Profile測(cè)試先查證以下問題:
由于3張紋理分辨率非常大且開啟Mipmaps刽锤,其內(nèi)存占用理應(yīng)是期待紋理虛擬內(nèi)存 = 85.33MB + 64.00MB + 13.65MB = 162.98MB
镊尺,如果最終內(nèi)存穩(wěn)定后,本進(jìn)程的虛擬內(nèi)存占用約為進(jìn)程內(nèi)存 ~= 啟動(dòng)內(nèi)存 + 已加載紋理內(nèi)存
并思,即可證實(shí)紋理虛擬內(nèi)存占用的確只有一份庐氮,否則如果進(jìn)程虛擬內(nèi)存約為進(jìn)程內(nèi)存 ~= 啟動(dòng)內(nèi)存 + 2*已加載紋理內(nèi)存
,即可證實(shí)主存纺荧、顯存各持一份紋理貼圖旭愧。
Unity版本為2017.4.8f1、XCode版本為10.1宙暇、運(yùn)行設(shè)備為iPhone6s输枯。
先用Unity以Development Build進(jìn)行XCode工程導(dǎo)出,Development Build僅僅是為了能用Unity Memory Profiler進(jìn)行Profile占贫。
XCode中對(duì)Unity-iPhone工程進(jìn)行Edit Scheme桃熄,并如下圖開啟Malloc Stack,是為了在命令行對(duì)memorygraph使用malloc_history
命令查看內(nèi)存創(chuàng)建的堆棧型奥。
XCode中構(gòu)建版本瞳收,USB連接iPhone6s并在其上運(yùn)行,等待幾秒鐘待內(nèi)存穩(wěn)定后:
- 在XCode點(diǎn)擊“Debug Memory Graph”厢汹,截取得出XCode的內(nèi)存統(tǒng)計(jì)螟深,并且Export為xcode_empty.memorygraph文件
點(diǎn)擊UI加載上面3張紋理后,等待幾秒鐘待內(nèi)存穩(wěn)定后:
- 在Unity用Memory Profiler點(diǎn)擊Take Snapshot烫葬,截取得出Unity的內(nèi)存統(tǒng)計(jì)界弧,并另存為unity.memsnap3文件
- 在XCode點(diǎn)擊“Capture GPU Frame”凡蜻,截取得到當(dāng)前幀的GPU快照,并另存為xcode.gputrace文件
- 在XCode點(diǎn)擊“Debug Memory Graph”垢箕,截取得出XCode的內(nèi)存統(tǒng)計(jì)划栓,并且Export為xcode.memorygraph文件
注意上述操作都確保游戲是一次運(yùn)行針對(duì)同一進(jìn)程的4次抓取結(jié)果,從而確保內(nèi)存地址穩(wěn)定条获。
我們?cè)诿钚袌?zhí)行命令vmmap --summary ./xcode_empty.memgraph
忠荞,得到加載紋理前的虛擬內(nèi)存占用約為111.3MB,如下圖:
上圖我們應(yīng)關(guān)心“DIRTY SIZE”和“SWAPPED SIZE”委煤,前者代表已寫虛存大小、后者代表已寫待壓縮虛存大小修档。iOS和一般OS不一樣素标,不采取虛存切頁(Paging)的機(jī)制,而是采取壓縮內(nèi)存的機(jī)制萍悴。而在iOS中所謂的內(nèi)存占用(Memory Footprint)事實(shí)上是MemoryFootprint = DirtySize + CompressedSize
头遭,iOS以MemoryFootprint的大小作為Killapp的依據(jù)。注意Swapped Size是待壓縮的大小癣诱,壓縮后方為Compressed Size计维。[8]
我們?cè)賵?zhí)行命令vmmap --summary ./xcode.memgraph
,得到加載紋理后的虛擬內(nèi)存占用約為297.8MB撕予,如下圖:
從而,加載紋理額外虛擬內(nèi)存占用 = 297.9MB - 111.3MB = 186.6MB ~= 期待紋理虛擬內(nèi)存占用162.98MB
实抡,而186.6MB << 325.96MB
欠母,從而幾乎已經(jīng)證實(shí)問題1,的確主存顯存共享一份貼圖虛擬內(nèi)存吆寨。至于為何會(huì)多出186.6MB - 162.98MB ~= 23.62MB
赏淌,我們會(huì)在后面證實(shí)到。
但僅僅從內(nèi)存增幅來認(rèn)定內(nèi)存共享一份啄清,顯得還不夠精確六水。
這時(shí)有個(gè)貌似合理的猜想:“如果GPU里用到的紋理虛擬內(nèi)存地址,剛好等于MemoryGraph中對(duì)應(yīng)的紋理虛擬地址辣卒,就說明它們必然是共享一份內(nèi)存了”掷贾。
懷著這個(gè)想法,我們用XCode打開xcode.gputrace文件荣茫,搜索得出4096_rgba32的虛擬內(nèi)存地址為0x1083f5b80想帅,如下圖:
Unity Memory Profiler Editor本不支持顯示對(duì)象的Native虛擬內(nèi)存地址,簡(jiǎn)單修改其源碼啡莉,讓其在面板上顯示Unity Native Object的虛擬內(nèi)存地址港准,4096_rgba32紋理的虛擬內(nèi)存地址為0x1083f53b0紋理憎乙,如下圖:
“CPU/GPU訪問的紋理地址不一樣,這證實(shí)這張紋理不是CPU/GPU共享的叉趣!”但可惜,不能因此得出這個(gè)結(jié)論该押。
我們控制臺(tái)針對(duì)GPUTrace的地址使用命令malloc_history ./xcode.memgraph -fullStacks 0x1083f5b80
疗杉,有下圖輸出:
針對(duì)Unity Memory Profiler的地址使用命令
malloc_history ./xcode.memgraph -fullStacks 0x1083f53b0
,有下圖輸出:使用XCode再次打開xcode.memgraph蚕礼,搜索地址0x1083f5b80烟具,發(fā)現(xiàn)其類型是“AGXA9FamilyTexture”,而且對(duì)象大小僅僅只有528字節(jié)奠蹬,見下圖:
上面3圖囤躁,證實(shí)了上面的地址僅僅是紋理對(duì)象冀痕,而并非我們最關(guān)心的紋理內(nèi)容地址。比如AGXA9FamilyTexture是Metal的紋理對(duì)象狸演,Texture2D是Unity的紋理對(duì)象言蛇,紋理對(duì)象內(nèi)部有指針指向了紋理內(nèi)容。
如果我們不修改Unity源碼宵距,我們無法得知Texture2D中紋理內(nèi)容的地址腊尚。如何得知紋理內(nèi)容到底在哪呢?
留意上面vmmap --summary
命令顯示加載紋理前后的內(nèi)存占用满哪,增幅最大的內(nèi)存區(qū)域(Region)是“IOKit”婿斥,我們不妨看看里面到底是啥,通過vmmap --verbose ./xcode.memgraph | grep "IOKit"
哨鸭,有以下結(jié)果:
上面非常像我們3張紋理貼圖內(nèi)容的內(nèi)存占用大小(下面才解釋為什么64.0MB變?yōu)?5.3MB)像鸡,而左邊就是它們的虛擬內(nèi)存地址勘高。
我們嘗試用malloc_history ./xcode.memgraph --fullStacks “上述3個(gè)地址”
,發(fā)現(xiàn)都不能打印出分配它們的棧坟桅,說明它們并非使用傳統(tǒng)malloc在堆(Heap)上分配华望,如下圖。事實(shí)上IOKit是iOS的驅(qū)動(dòng)框架仅乓,該區(qū)域內(nèi)存是驅(qū)動(dòng)相關(guān)的虛擬內(nèi)存區(qū)域赖舟,通過額外的實(shí)驗(yàn)可以知道,Metal最重要的MTLBuffer分配夸楣,不管Dirty與否宾抓,都是在IOKit這個(gè)驅(qū)動(dòng)區(qū)域進(jìn)行內(nèi)存分配子漩。
但是石洗!當(dāng)我們?cè)赬Code打開xcode.memgraph后幢泼,如下圖,搜索地址“0x11c3e0000”得出該85.3MB的IOKit內(nèi)存讲衫,而引用它的缕棵,恰好就是我們上面發(fā)現(xiàn)的地址為0x1083f5b80的Metal的紋理對(duì)象!
至此涉兽,我們通過硬件分析招驴、圖形API分析和虛擬內(nèi)存Profile分析,比較折騰枷畏,終于得出以下結(jié)論:
- iOS設(shè)備中只有一塊物理內(nèi)存硬件
- 主存地址和顯存地址在同一個(gè)地址空間(Address Space)中别厘,即虛存地址空間(Virtual Address Space)
- 虛擬內(nèi)存中的確只有一份紋理內(nèi)容,而且該紋理內(nèi)容的確就是被GPU所用的紋理拥诡。
我們接著討論問題2触趴。由于問題2需要回答的是貼圖內(nèi)存走向,不能通過分析某一時(shí)刻的虛擬內(nèi)存得出結(jié)論渴肉,而要使用帶有Timeline的Profiler雕蔽,這里使用Instruments。
我們進(jìn)行3種Profiler:Timer Profiler以觀察CPU耗時(shí)情況及捕捉函數(shù)調(diào)用棧宾娜,Allocations以觀察堆內(nèi)存分配釋放情況批狐,VM Tracker以觀察所有虛擬內(nèi)存的分配釋放情況。
針對(duì)Time Profiler前塔,我們可以打開其High Frequency選項(xiàng)嚣艇,以采樣到更精細(xì)的函數(shù)調(diào)用棧。
Profile結(jié)果如下圖食零。其中3個(gè)紅框左到右分別表示加載RGBA32、RGB24寂屏、ASTC5x5時(shí)的情況贰谣。
大致觀察上圖可以發(fā)現(xiàn):
- CPU消耗尖刺(Spike):RGB24 > RGBA32 >> ASTC5x5
- 堆內(nèi)存消耗尖刺:RGB24 > RGB32 >> ASTC5x5
- 虛擬內(nèi)存消耗則整體呈現(xiàn)持續(xù)增長
我們先看最左邊RGBA32的CPU消耗情況秘豹,如下兩圖,分別為加載RGB24紋理時(shí)CPU消耗Spike的前期和后期
不需無頭緒地辛苦閱讀海量引擎代碼昌粤,有的放矢既绕,立刻可精確看出Unity在加載紋理時(shí)主要工作分兩部分:文件加載(File::Read()
)和紋理上傳(UploadTexture2DData()
)啄刹。
而且發(fā)現(xiàn)將時(shí)間線在前后期中間不管如何細(xì)分,都只出現(xiàn)了上面2個(gè)主要消耗凄贩,說明了只有這兩個(gè)工作線程在工作誓军,我們只需分析它們相信已足夠找出紋理加載的流程。我們也發(fā)現(xiàn)在整個(gè)紋理加載過程中疲扎,主線程只有非常少的Update空轉(zhuǎn)占用昵时,證實(shí)紋理加載幾乎是脫離主線程工作的。
文件加載函數(shù)椘浪粒看起來比較通用,先從紋理上傳的函數(shù)椃乔看起應(yīng)該會(huì)更快解決問題瓜挽。可發(fā)現(xiàn)其關(guān)鍵流程如下:
<具體分析略>
通過以上比較啰嗦的分析征绸,可以看出就算是在Metal進(jìn)行紋理上傳久橙,也難免有紋理內(nèi)容拷貝的過程。用[MTLDevice newTextureWithDescriptor]
創(chuàng)建紋理對(duì)象及其指向的紋理內(nèi)容空間管怠,把FileAssetUploadInstruction
的buffer
數(shù)據(jù)淆衷,加以一定處理(Crunch、紋理格式轉(zhuǎn)換等)渤弛,最終通過[MTLTexture replaceRegion]
將紋理內(nèi)容數(shù)據(jù)拷貝到了驅(qū)動(dòng)虛擬內(nèi)存IOKit區(qū)域里祝拯。
那到底這個(gè)buffer
數(shù)據(jù)到底從哪來的?當(dāng)然她肯,從上文和類名包含“File”佳头,已經(jīng)可以猜出是從外存讀取得來,但不精確證實(shí)不服氣晴氨,我們將注意力回到上面的文件加載調(diào)用棧。堆棧協(xié)助代碼閱讀,發(fā)現(xiàn)很簡(jiǎn)單:
<具體分析略>
那么command->buffer
的內(nèi)存哪里分配而來呢密浑?
由于內(nèi)存分配的CPU消耗可能很小付燥,就算是高精度的Sampler也可能在Time Profiler里找不到,這里我們明顯要求救于Allocation枝哄,如下圖肄梨,我們選擇“Call Trees”分類,框選在加載紋理時(shí)挠锥,內(nèi)存飆升時(shí)的時(shí)段峭范,發(fā)現(xiàn)132.03MB內(nèi)存是在AsyncUploadManager::ManageTextureUploadRingBufferMemory()
中分配給m_DataRingBuffer
文件讀取的緩存應(yīng)該是在堆上分配
(AsyncUploadManager::ManageTextureUploadRingBufferMemory()
圖略 )
紋理上傳過程中,最大的堆內(nèi)存分配是分配給了AyncUploadManager.m_DataRingBuffer
通過以上種種分析瘪贱,已經(jīng)掌握了不少信息和關(guān)鍵字纱控,找出答案已是臨門一腳了:
(AsyncUploadManager::AcquireWritePtr()
圖略 )
AsyncUploadManager::ScheduleAsyncRead()
從m_DataRingBuffer
申請(qǐng)紋理內(nèi)容大小的內(nèi)存空間辆毡,同時(shí)將指針賦值給asyncReadCommand->buffer
和ftuInstr->buffer
,從而文件讀取線程將紋理文件內(nèi)容寫到asyncReadCommand->buffer
指向的堆內(nèi)存甜害,渲染線程在通過ftuInstr->buffer
將紋理內(nèi)容從同一堆內(nèi)存獲取到舶掖。
至此,回答了問題2尔店。
最后的最后眨攘,上面提到的RGB24紋理的特殊情況,為什么其虛擬內(nèi)存占用大小不是64MB嚣州,而是和RGBA32一樣鲫售,都是85.3MB?結(jié)合上面已知流程该肴,分析可知情竹,原因是Metal并不支持RGB24,在運(yùn)行時(shí)都會(huì)轉(zhuǎn)為RGBA32匀哄,如下:
(metal::PixelFormat
圖略 )
這能從以下Time Profiler以及Allocation棧輕易證實(shí):
Metal不支持RGB24秦效,交給GPU使用前需要轉(zhuǎn)換為RGBA32,這能從以下Time Profiler以及Allocation棧輕易證實(shí):
(UploadTexture()
中的needConversion
圖略 )
Metal不支持RGB24涎嚼,交給GPU使用前需要轉(zhuǎn)換為RGBA32阱州,需要消耗CPU進(jìn)行一次BlitImage。
(UploadMipPyramid()
圖略 )
結(jié)論
通過Profile結(jié)果和源碼法梯,我們證實(shí)了:iOS設(shè)備中只有一塊物理內(nèi)存硬件苔货,主存地址和顯存地址在同一塊虛存地址空間中,虛存最終的確只有一份紋理內(nèi)容位于IOKit區(qū)域中立哑,而且該紋理內(nèi)容的確就是被GPU所用的紋理蒲赂。
在紋理上傳過程中,Unity先在堆內(nèi)存申請(qǐng)緩存刁憋,然后將紋理文件內(nèi)容讀進(jìn)緩存里滥嘴,然后調(diào)用圖形API將該該紋理內(nèi)容數(shù)據(jù)拷貝到IOKit虛存中,供GPU訪問至耻∪糁澹拷貝完成后緩存視乎情況從堆內(nèi)存釋放。
過程中尘颓,我們展示了在iOS中各種Profile工具的實(shí)際使用方法走触。
也介紹了一些基礎(chǔ)的內(nèi)存知識(shí)和概念。
下載實(shí)驗(yàn)工程及數(shù)據(jù)
見Github:MobileGFXMemoryTest
Android篇
打算未來才做Android的Profile實(shí)驗(yàn)和分析報(bào)告疤苹,但通過上面的分析看來互广,可以大膽預(yù)測(cè):
- Android設(shè)備也是基于ARM架構(gòu),想必各種Vendor的設(shè)備也是只有一塊物理內(nèi)存硬件;
- 上面的函數(shù)棧大多平臺(tái)無關(guān)惫皱,而且Vulkan和Metal是同一代的圖形框架像樊,所以Unity在Vulkan上的實(shí)現(xiàn)內(nèi)存流程應(yīng)該和Metal非常類似;
- 由于GLES是較老的框架旅敷,所以其內(nèi)存流程可能和Metal類似生棍,但要留意GLES具體情況,和其在驅(qū)動(dòng)內(nèi)部
gralloc
的使用情況媳谁,有沒有額外的拷貝
關(guān)鍵字
手機(jī)涂滴,GPU,顯存晴音,移動(dòng)設(shè)備柔纵,iPhone,iPad锤躁,iOS搁料,安卓,Android进苍,Mobile Device加缘,內(nèi)存鸭叙,共享內(nèi)存觉啊,物理內(nèi)存
引用
[1]ifixit - iPhone 6 Teardown
[2]Chipworks Disassembles Apple's A8 SoC
[3]Metal_(API)#Supported_GPUs
[4]Metal Best Practices Guide - Resource Options
[5]Metal - Resource Storage Mode
[6]MTLBuffer
[7]Triple Buffering
[8]iOS Memory Deep Dive
[9]Choosing a Resource Storage Mode in iOS and tvOS
[10]MTLBuffer makeTexture