最近一直想面對大規模程序時,如何提高運算速度,100個怪物循環100次沒有問題,但是處理的過程會特別龐大,所以考慮到使用多線程,unity的單線程,而unity自帶的dots系統也不知道什么時候成熟,不想造輪子所以jobsystem真心不想用,在網上偶然間看到了一個關於鳥群算法對Computeshader的使用,查閱了很多資料后終於暫時入門:簡單說就是在顯卡上扣出一部分性能給游戲的數值做運算。
首先轉載一張經典的圖和某位原作者的話:
- SV_GroupThreadID 表示該線程在該組內的位置
- SV_GroupID 表示整個組所分配的位置
- SV_DispatchThreadID 表示該線程在所有組的線程中的位置
- SV_GroupIndex 表示該線程在該組內的索引
我自己的表述:
如果我們有一個長度為20的數組需要處理,為了同時處理者10000個數據,我們需要向顯卡申請至少10000個線程,此時就可以申請10000個線程,但是呢這10000個線程太大了,所以呢,把這10000個線程的布局成一個2維數組,但是這個二位數組還是太大,所以呢就把這個二維數組再次切割成一個1維數組,這樣整個表就切割成了一個3維數組;但是呢這個三維數組還是太大,以一個三維索引R范圍為單元,再次按照上面的方法再切割成一個三維數組G,結果就是三維單元R就是numthreads(x,y,z),而三維數組G就是Dispatch(x,y,z)。這里也利於理解,因為顯卡的處理單元過於龐大,已經超過了一個數組索引的范圍,或者顯卡的設計就是以層層陣列的方式制作的,因此這種超維度結構可以更好的索引到需要的資源,而它申請索引的時候完全是靠數字電路硬件獲取(學過計算機電子的都知道)。另外這樣分塊的好處將在下面的例子中提到。
所以再次對上面的索引做出解釋:
- SV_GroupThreadID 表示該線程在該組內的位置----------即這個三維單元內的單元數組空間中索引
- SV_GroupID 表示整個組所分配的位置----------即該線程所在的組在組空間中的索引
- SV_DispatchThreadID 表示該線程在所有組的線程中的位置----------即不將整個三維單元分組的情況下的三維索引
- SV_GroupIndex 表示該線程在該組內的索引----------即這個三維單元內的數組轉換成一個一維數組時所在的索引
例子:
對一個結構體數組內的所有結構內的數乘以2:
C#代碼:
using System.Collections; using System.Collections.Generic; using System.Diagnostics; using UnityEngine; public class MyFirstComputeShader : MonoBehaviour { public ComputeShader computeShader = null; data[] inputDatas = new data[3];//輸入數組 data[] outputDatas = new data[3];//結果數組 struct data { public float a; public float b; public float c; } private void InitData() { inputDatas = new data[3]; outputDatas = new data[3]; UnityEngine.Debug.Log("---------------cpu輸入------------------------"); for(int i = 0; i < inputDatas.Length; i++) { inputDatas[i].a = i * 3 + 1; inputDatas[i].b = i * 3 + 2; inputDatas[i].c = i * 3 + 3; UnityEngine.Debug.Log(inputDatas[i].a + "," + inputDatas[i].b + "," + inputDatas[i].c); } } private void ToComputeShader(ref data[] i,ref data[] o) { //data 數據里面float*3,而一個float的字節為4字節,所以3*4 ComputeBuffer inputBuffer = new ComputeBuffer(i.Length, 12); ComputeBuffer outputBuffer = new ComputeBuffer(o.Length, 12); //拿到核心 int k = computeShader.FindKernel("CSMain"); inputBuffer.SetData(i); //寫入gpu computeShader.SetBuffer(k, "inputDatas", inputBuffer); computeShader.SetBuffer(k, "outputDatas", outputBuffer); //計算再輸出到cpu computeShader.Dispatch(k, 1, 1, 1); outputBuffer.GetData(o); UnityEngine.Debug.Log("---------------gpu輸出------------------------"); for(int j = 0; j < o.Length; j++) { UnityEngine.Debug.Log(o[j].a + "," + o[j].b + "," + o[j].c); } //釋放內存 inputBuffer.Release(); outputBuffer.Release(); } private void Update() { if (!Input.GetKeyDown(KeyCode.Space)) return; Stopwatch sw = new Stopwatch(); sw.Start(); //計時器開始 InitData(); ToComputeShader(ref inputDatas,ref outputDatas); sw.Stop(); //計時器結束 UnityEngine.Debug.Log(sw.Elapsed); //開始到結束之間的時長 } }
computeShader代碼:
// Each #kernel tells which function to compile; you can have many kernels #pragma kernel CSMain struct data { float a; float b; float c; }; //(cpu->gpu) StructuredBuffer<data> inputDatas; //(gpu->cpu) RWStructuredBuffer<data> outputDatas; //[numthreads(3,1,1)] //void CSMain (uint3 id : SV_DispatchThreadID) //{ // //if (id > 2)return; // //計算一下 // outputDatas[id.x].a = inputDatas[id.x].a * 2; // outputDatas[id.x].b = inputDatas[id.x].b * 2; // outputDatas[id.x].c = inputDatas[id.x].c * 2; //} [numthreads(3, 1, 1)] void CSMain(uint id : SV_GroupIndex) { //計算一下 outputDatas[id].a = inputDatas[id].a * 2; outputDatas[id].b = inputDatas[id].b * 2; outputDatas[id].c = inputDatas[id].c * 2; }
結果如下:
因為我的數組長度是3,而numthreads(3,1,1),因此Dispatch(1,1,1),即申請了一個組,並且這個組內有3個線程。這里可以改進為numthreads(1,1,1),而dispatch(n,1,1),這樣n的大小就是總的線程數量。關於這個問題以及常常出現的誤區和某位仁兄討論過如下:
解決:
其實本質就是我們申請到的是整個組區域中的所有線程,因此我們應該處理一下線程id超出的情況(貌似是難免的,當數組的不整齊的時候,就會有多余的線程出現),但是不知道為什么,顯卡處理程序的時候,索引值超過緩存的長度也不會報錯。
當要處理一個圖片的時候比如圖片大小是1024*1024,那么可以(512*2,512*2,1),即numthreads(2,2,1);dispatch(512,512,1);