深入优化GPU编程概述
? ? ?網(wǎng)上關(guān)于GPU編程優(yōu)化的文章很多,本篇博客帶領(lǐng)讀者更深入的理解GPU編程以及各個(gè)函數(shù)的運(yùn)行時(shí)間,為開(kāi)發(fā)者優(yōu)化Shader編程提供一些指導(dǎo)。除了Shader編程中的變量定義精度優(yōu)化,還有函數(shù)的優(yōu)化,下面給讀者展示如下:
在PC端執(zhí)行
Shader代碼在PC端使用的函數(shù)執(zhí)行時(shí)間:
該圖是以DX11為例,用|隔開(kāi)的數(shù)據(jù),前面的部分是計(jì)算單分量時(shí)的指令數(shù),后面的部分是計(jì)算float4時(shí)的指令數(shù)。通過(guò)上圖給給讀者總結(jié)一下:
- 反三角函數(shù)非常費(fèi)
- abs和saturate是免費(fèi)的
- 除了反三角函數(shù)外,fmod和smoothstep比預(yù)期更費(fèi)
- log,exp,sqrt(單分量)的成本實(shí)際上很低,所以由他們組合成的pow也不高
- sin,cos在DX11使用了專(zhuān)門(mén)一條單指令,成為了低成本函數(shù)
另外,絕大部分GPU是一次性計(jì)算4個(gè)分量,計(jì)算一個(gè)float4和只計(jì)算單個(gè)float耗時(shí)是一樣的。當(dāng)計(jì)算float時(shí),剩下三個(gè)分量的時(shí)長(zhǎng)會(huì)被浪費(fèi)。
在移動(dòng)端執(zhí)行
由于硬件不同,每條指令的時(shí)間成本確實(shí)可能是不一樣的。下面通過(guò)具體的Shader代碼樣例給出主流GPU的執(zhí)行時(shí)間,供參考:
左邊對(duì)應(yīng)的是每行的代碼,右邊是執(zhí)行所需時(shí)間,通過(guò)上圖可以看出:1/x, sin(x), cos(x), log2(x), exp2(x), 1/sqrt(x)這些指令的時(shí)間成本是一樣的,而且和普通的四則運(yùn)算很接近,但是sin,cos畢竟在舊硬件上成本較高,由于不清楚硬件的具體情況,還是要盡可能少用。
另外Nvidia提供了一個(gè)工具:Nvidia ShaderPerf,可以幫助讀者分析Shader代碼,網(wǎng)址:點(diǎn)擊打開(kāi)鏈接。
GPU執(zhí)行是一個(gè)多線程的,它最擅長(zhǎng)的就是對(duì)矩陣和向量的運(yùn)算,在Shader編程時(shí)盡量少用一些函數(shù)以及循環(huán)語(yǔ)句,這些都會(huì)對(duì)其執(zhí)行效率有影響。
另外為了滿足幀數(shù)的要求,我們可以將骨骼動(dòng)畫(huà)放到GPU中執(zhí)行,也就是常說(shuō)的Animation GPU Instancing以及GPU Instancing。
優(yōu)化方案
其中GPU Instancing的代碼下載地址:點(diǎn)擊打開(kāi)鏈接
Animation GPU Instancing的代碼下載地址:點(diǎn)擊打開(kāi)鏈接
? ? 當(dāng)然在使用GPU Instancing時(shí)也要注意一個(gè)問(wèn)題就是:每一次的instanced draw,肯定要傳一個(gè)instance buffer,這樣每一幀都要重新生成這個(gè)instance buffer然后調(diào)用昂貴的glBufferData,或者Dx11的map/unmap來(lái)更新這個(gè)instance buffer。這種需要每幀更新數(shù)據(jù)的buffer(一般叫Dynamic buffer)是不可避免的。
????但是也應(yīng)該盡量降低Map/Unmap的大小和次數(shù)。在d3d11中,一般會(huì)將constant buffer按更新頻率分組,通常可以分為PerFrameData, PerMaterialData, PerObjectData。相機(jī)相關(guān)的viewMatrix, projMatrix放到PerFrameData中,當(dāng)前材質(zhì)相關(guān)的屬性放到PerMaterialData中,當(dāng)前渲染對(duì)象的世界矩陣放到PerObjectData中。PerObjectData就是每次drawcall都需要Map/Unmap的,而你在instance buffer中放的數(shù)據(jù),就是本來(lái)的PerObjectData的數(shù)組。N次instance drawcall總共包含M個(gè)instance的話,其實(shí)你是將Map/Unmap的次數(shù)從M降到了N,是優(yōu)于非instance draw的。
你可以為每個(gè)object分配一個(gè)instance handle,如果2個(gè)object可以合并為instance draw的話,則他們的instance handle相同。然后,在渲染時(shí),將相同instance handle的object找出來(lái)調(diào)用instance draw。只需要遍歷一次所有的object就ok,很高效了。在GDC 會(huì)議上還有人專(zhuān)門(mén)針對(duì)GPU Buffer做了一次講座,網(wǎng)址:點(diǎn)擊打開(kāi)鏈接再介紹一下GPU Skinning,目前的Android高端手機(jī)應(yīng)該都支持OpenGLES3.0,可以使用GPU Skinning,下面我們把CPU Skinning和GPU SKinning執(zhí)行的效率對(duì)比圖給讀者展示如下:
以上數(shù)據(jù)是通過(guò)小米手機(jī)測(cè)試的:
?隨人數(shù)增加,兩者CPU負(fù)載趨一致,GPU Skinning比CPU Skinning內(nèi)存稍低;
?隨人數(shù)增加,GPU Skinning比CPU Skinning FPS高30%左右;
?75人以下,GPU Skinning?比?CPU Skinning的CPU負(fù)載稍低,內(nèi)存較低,FPS相近(此時(shí)非GPU瓶頸);
是否使用GPUSkinning策略,也取決于CPU或GPU的負(fù)載情況。如果當(dāng)前的CPU負(fù)載瓶頸,GPU較輕,可使用GPUSkinning;反之,則建議使用默認(rèn)的CPUSkinning。詳情參考網(wǎng)址:點(diǎn)擊打開(kāi)鏈接
GPU存儲(chǔ)
在GPU中會(huì)聲明一些變量,這些變量存儲(chǔ)在哪里?本節(jié)就給讀者介紹一下:
首先我們看一下integrated GPU,也就是intel,arm等和CPU處于同一個(gè)DIE的GPU,它們的存儲(chǔ)體系是如何的。首先,這些GPU自己的video memory都是從CPU可用的主存中分出來(lái)的,例如一個(gè)PC有4G的物理存儲(chǔ),分給intel核顯512MB后,就只剩下3.5G可以給CPU用了。
在這些integrated GPU中,GPU和CPU處于一個(gè)DIE中,所以很多時(shí)候GPU和CPU可以共享總線。GPU自己的video memory也是在總線上走的。除了GPU自己的video memory之外,CPU和GPU有時(shí)候需要共享一些數(shù)據(jù),例如,CPU將Vertex Buffer/Index Buffer放入主存中,然后讓GPU使用。如我們之前所說(shuō),主存是CPU的存儲(chǔ),GPU是無(wú)法看到這部分的。為了解決這個(gè)問(wèn)題,業(yè)界引入了一個(gè)叫做GART的東西,Graphics Address Remapping Table。這個(gè)東西長(zhǎng)得和CPU用來(lái)做地址翻譯的page table很像,作用也很類(lèi)似,就是將GPU用的地址重新映射到CPU的地址空間。有了GART,CPU中的主存就可以對(duì)GPU可見(jiàn)了。但是反方向呢?GPU的local memory是否對(duì)CPU可見(jiàn)?
integrated GPU的local memory是從主存中分配出來(lái),受限于主存的大小,能夠分配出來(lái)的空間并不大,一般是256M/512M,最多的也就1GB。這么點(diǎn)兒地址空間,完全可以全部映射到CPU的地址空間中。如果OS是32位系統(tǒng),可以尋址的地址空間有4G,分出256M/512M來(lái)全部映射GPU的local memory也不是多么難的事情。但是分出1G的話似乎有點(diǎn)兒過(guò)分了,所以還是建議OS上64位地址空間,這樣integrated GPU的local memory就可以全部映射到CPU地址空間中了。
對(duì)于獨(dú)立顯卡,也就是所謂的dedicated GPU,情況就又不一樣了。一般獨(dú)立的GPU都有自己獨(dú)立的存儲(chǔ)實(shí)體,就是擁有不同于主存的video memory chip。而且目前來(lái)看,這些GPU所用的video memory chip都是板載的,也就意味著無(wú)法升級(jí)和替換。這些GPU自帶的video memory有時(shí)候太大了,例如擁有4G或者6G的顯存,將之完全映射到CPU的地址空間既不現(xiàn)實(shí),也無(wú)可能。想象一下,一個(gè)帶6G顯存的顯卡,在一個(gè)32位OS上,OS整個(gè)4G的地址空間都放不下全部6G的顯存。所以,這些獨(dú)立顯卡擁有和另一套稍微有點(diǎn)兒不同的存儲(chǔ)和地址空間映射機(jī)制,來(lái)解決這個(gè)問(wèn)題。
一般的解決方法是,只映射一部分區(qū)域到CPU的地址空間,典型的大小是256MB/512MB。這段地址空間會(huì)通過(guò)PCIe的bar獲取一個(gè)CPU可見(jiàn)的地址空間,所以一般來(lái)說(shuō),同樣也是BIOS設(shè)置,并且開(kāi)機(jī)后不可變的。最近PCIe支持resize bar的技術(shù),支持這項(xiàng)技術(shù)的GPU可以動(dòng)態(tài)調(diào)整大小,因此使用起來(lái)也就更加靈活了。
除了暴露給CPU可見(jiàn)部分的video memory之外,其他部分都是CPU不可見(jiàn)的。這部區(qū)域一般被驅(qū)動(dòng)用來(lái)做一些只有GPU可見(jiàn)的資源的存儲(chǔ),例如臨時(shí)的Z buffer等。
理解GPU的存儲(chǔ)體系結(jié)構(gòu),對(duì)于深刻理解3D渲染管線,以及其他使用GPU的場(chǎng)景時(shí),資源創(chuàng)建的標(biāo)志位有著非常重要的作用。
詳情查看網(wǎng)址: 點(diǎn)擊打開(kāi)鏈接在Integrated GPU中,GPU一般和CPU共享相同的物理存儲(chǔ)。GPU自己的local memory實(shí)際上是從CPU的main memory中分配出來(lái)一塊物理連續(xù)的空間來(lái)模擬的,即所謂的Unified Memory Architecture模型。注意即使在Unified Memory Architecture,Address也并非是Unified的,GPU和CPU用的地址也不一定位于一個(gè)地址空間內(nèi)。
????對(duì)于GPU而言,它能用的存儲(chǔ)包括自己的local memory(實(shí)際上就是遠(yuǎn)在DRAM地方分出來(lái)的一部分),以及一部分通過(guò)GART可以訪問(wèn)的system memory(直接訪問(wèn)CPU的物理地址空間)。對(duì)于local memory而言,其可以完全映射到CPU的地址空間,因此,CPU要通過(guò)local memory往GPU share數(shù)據(jù)是非常簡(jiǎn)單的事情。然而local memory是global的,CPU上各自運(yùn)行的process想要使用local memory來(lái)快速傳遞數(shù)據(jù)基本上是不可能的,畢竟這種global的resource應(yīng)該由內(nèi)核來(lái)管理。CPU上各自的process想要往GPU上upload數(shù)據(jù),還得依靠GART才行。
????GART的原理非常簡(jiǎn)單,就是將GPU自己的地址空間的一個(gè)地址映射到CPU的地址空間。假設(shè)GPU的local有128MB,那么可以建立一個(gè)簡(jiǎn)單的映射表,當(dāng)GPU訪問(wèn)128M-256M的時(shí)候,將之映射到CPU地址空間內(nèi),一般是不連續(xù)的4kB或者64kB的page。這樣,CPU上的進(jìn)程將數(shù)據(jù)填寫(xiě)到自己分配的地址空間內(nèi),然后內(nèi)核通過(guò)GART,將GPU的一段地址空間映射到之前CPU上進(jìn)程寫(xiě)的地址空間,這樣,GPU就可以用另一套地址空間來(lái)訪問(wèn)相同的數(shù)據(jù)了。
再后面就是關(guān)于GPU硬件方面的知識(shí)了,詳情查看:點(diǎn)擊打開(kāi)鏈接
在這里通過(guò)一個(gè)案例給讀者介紹一下GPU的工作原理,我們開(kāi)發(fā)游戲時(shí),資源從內(nèi)存上傳到顯存時(shí),在DX11中都對(duì)應(yīng)哪些過(guò)程?
基本有三種方法可以做到這件事情。
1. 建立資源的時(shí)候通過(guò)initial data傳入,就一次。
2. Map->memcpy->Unmap。
3. UpdateSubresource。
4. 先用2把數(shù)據(jù)放入一個(gè)staging資源,再用copyresource放入default資源。
第一種可以認(rèn)為是同步的。runtime會(huì)建立一個(gè)資源,讓底層map->memcpy->unmap。map的時(shí)候資源一定不會(huì)在使用,因?yàn)槎歼€沒(méi)建立出來(lái)呢。所以這個(gè)過(guò)程很快。
第二種也可以認(rèn)為是同步的。原理同前。但在map的時(shí)候,資源可能正在被使用,所以如果沒(méi)有NO_WAIT標(biāo)志,流水線就會(huì)被block,等待資源可用的時(shí)候才完成map。
第三種是異步的。驅(qū)動(dòng)會(huì)在內(nèi)部開(kāi)一塊臨時(shí)空間,把數(shù)據(jù)拷進(jìn)去,等待資源不被占用的時(shí)候進(jìn)行map->memcpy->unmap。
第四種,看后面我說(shuō)的就自然會(huì)有解釋。
好了,所以核心就在map/unmap上。
根據(jù)不同的類(lèi)型,staging/dynamic/default,所在的位置不同(resident不同),map是做的不一樣的事情。
staging: 這個(gè)可以認(rèn)為就是一塊線性的sys mem。map就是把它指針給你而已。但dx runtime不保證每次map給你的是同一個(gè)地址。我曾經(jīng)試圖這么假設(shè)過(guò),被dx組的人無(wú)情地修理了。
dynamic: 在runtime里實(shí)際上會(huì)建立2個(gè)資源,一個(gè)sys mem一個(gè)gpu mem。map的時(shí)候給你sys mem的,在unmap之后把新數(shù)據(jù)同步到gpu mem的。
default: 在gpu mem。不能在API級(jí)別map/unmap。只能copyresource過(guò)去。
所以,這個(gè)過(guò)程就是,
app->map (runtime)->map(user mode driver)->address->memcpy->unmap (runtime)->unmap (user mode driver)->copyresource (runtime)->copyresource(user mode driver)->map(kernel mode driver)->memcpy to gpu mem (user mode driver)->unmap(kernel mode driver)。
效率方面:
3比2快,大部分時(shí)候3比2快4-8倍,偶爾慢個(gè)20%。這一點(diǎn)和nv/amd的ppt很不一樣,他們的ppt里假設(shè)是任意次序調(diào)用。一般好的圖形程序不會(huì)那樣,而仍會(huì)在每一幀開(kāi)始的時(shí)候灌幾次數(shù)據(jù),接下去全都是使用數(shù)據(jù)。
4如果遇到目標(biāo)資源正在被使用,就會(huì)讓copyresource變成異步,從而下一次map的時(shí)候會(huì)阻塞。解決方法是用多個(gè)臨時(shí)的staging資源(一般來(lái)說(shuō)3個(gè)就夠),按照ring buffer的方式使用。相當(dāng)于手動(dòng)弄成異步拷貝了。
1的話,可以作為4的備選。也就是說(shuō),如果遇到目標(biāo)資源正在被使用,就建立一個(gè)新的staging,同時(shí)把數(shù)據(jù)作為初始數(shù)據(jù)放進(jìn)去。在上面調(diào)用copyresource后就release,不會(huì)堵住流水線。
詳情查看網(wǎng)址: 點(diǎn)擊打開(kāi)鏈接最后,對(duì)于有志于成為GPU架構(gòu)師的讀者,在此給出一份學(xué)習(xí)書(shū)籍的清單供參考:
總結(jié)
以上是生活随笔為你收集整理的深入优化GPU编程概述的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: android 两个经纬度计算方位角和距
- 下一篇: rust领地柜用石镐拆吗_腐蚀Rust防