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