目的
GroupMemoryBarrierWithGroupSync
とgroupshared
についての理解を共有出来たらなと思ってこの記事を書いています.
本題に入る前に必要な予備知識をいくつか説明しますが,理解している人は全然飛ばして本題の説明部分まで行ってください.
予備知識
まず導入としてComputeShader内で使用されるセマンティクスについてサンプルを通して説明を行っていきます.
SV_DispatchThreadID
実行中のThreadを一意に特定することが出来るIDを返します.
こんな感じのComputeShaderを定義したとします.
#pragma kernel calc
RWStructuredBuffer<float> buffer;
[numthreads(128,1,1)]
void calc(uint3 id : SV_DispatchThreadID)
{
uint idx = id.x;
buffer[idx] = (float)idx;
}
void Start(){
readBuffer = new float[n];
buffer = new ComputeBuffer(n, Marshal.SizeOf(typeof(float)));
kernelID = kernel.FindKernel("calc");
kernel.SetBuffer(kernelID, "buffer", buffer);
kernel.Dispatch(kernelID, SIMULATION_BLOCK_SIZE, 1, 1);
buffer.GetData(readBuffer);
for(int i = 0; i <readBuffer.Length; i++){
Debug.Log(i.ToString() + " : " + readBuffer[i]);
}
}
これを実行するとちゃんと一意に特定できるようなIDになっていることが確認できます.
SV_GroupID
現在実行されているkernelが所属するThreadGroupのID(3次元)です
#pragma kernel calc
RWStructuredBuffer<float3> buffer;
[numthreads(128,1,1)]
void calc(uint3 id : SV_DispatchThreadID, uint3 Gid : SV_GroupID){
uint idx = id.x;
buffer[idx] = Gid;
}
void Start(){
readBuffer = new Vector3[n];
buffer = new ComputeBuffer(n, Marshal.SizeOf(typeof(Vector3)));
kernelID = kernel.FindKernel("calc");
kernel.SetBuffer(kernelID, "buffer", buffer);
kernel.Dispatch(kernelID, SIMULATION_BLOCK_SIZE, 1, 1);
buffer.GetData(readBuffer);
for(int i = 0; i <readBuffer.Length; i++){
Debug.Log(i.ToString() + " : " + readBuffer[i].ToString());
}
}
そしてkernelを実行した結果を確認してみると,以下のようになります.
画像では見えてない部分もありますが(0.0, 0.0, 0.0) ~ (7.0, 0.0, 0.0)まであってX成分のみ変化してます.
なぜこのような結果となるかというと,今回128*8のbufferがあり,Group内にあるThreadは128なので8Group存在するわけです.なので上のような結果になるわけです.
SV_GroupThreadID
Group内でのThreadIDを返します.
#pragma kernel calc
RWStructuredBuffer<float3> buffer;
[numthreads(128,1,1)]
void calc(uint3 id : SV_DispatchThreadID, uint3 GTid : SV_GroupThreadID)
{
uint idx = id.x;
buffer[idx] = GTid;
}
Group内のThread数は[numthreads(128, 1, 1)]
で定義したように128なので0 ~ 127で繰り返し表現されます.
SV_GroupIndex
Group内でindexであるSV_GroupThreadIDを1D 化したindexが返ってきます.
#pragma kernel calc
RWStructuredBuffer<float> buffer;
[numthreads(128,1,1)]
void calc(uint3 id : SV_DispatchThreadID, uint GI : SV_GroupIndex)
{
uint idx = id.x;
buffer[idx] = GI;
}
SV_GroupIndex = SV_GroupThreadID.x + numthreads.x * SV_GroupThreadID.y + numthreads.x * numthreads.y * SV_GroupThreadID.z
みたいな計算が行われているので今回でいうと
SV_GroupIndex = SV_GroupThreadID.x + 128 * SV_GroupThreadID.y + 128 * 0 * SV_GroupThreadID.z
となります.
ここからが本題
GroupMemoryBarrierWithGroupSync
とgroupshared
について説明を行っていきます.
GroupMemoryBarrierWithGroupSync
: 同一Group内の他スレッドの処理がこのメソッドが呼ばれる部分までの処理が終わるまでの処理を待って同期をとることを可能とします.
groupshared
: 変数に対する記憶域修飾子でこれが付いた変数はGroupThread内で共有することが出来るようになります.さっきの例でいうと(1.0, 0.0, 0.0)とか(7.0, 0.0, 0.0)など同一のSV_GroupIDを持っているThreadでは共有できるってことですね.
どんな時に有効?
c#側でbufferの中身を定義する際に同一GroupThread内のBufferの値をすべて足し合わせるとThreadGroupIDになるようにしてみました.
void Start()
{
readBuffer = new float[n];
buffer = new ComputeBuffer(n, Marshal.SizeOf(typeof(float)));
float[] temp = new float[n];
for(int i = 0; i < n; i++)
{
temp[i] = Mathf.Floor(i / 128) / 128.0f;
}
buffer.SetData(temp);
kernelID = kernel.FindKernel("calc");
kernel.SetBuffer(kernelID, "buffer", buffer);
kernel.Dispatch(kernelID, SIMULATION_BLOCK_SIZE, 1, 1);
buffer.GetData(readBuffer);
for(int i = 0; i <readBuffer.Length; i++)
{
Debug.Log(i.ToString() + " : " + readBuffer[i].ToString());
}
}
そしてcomputeshader側では同一GroupThread内のThreadで処理負荷が異なる環境を設定するためGroupIDが100以上のThreadに関して無駄にforループの処理を加えました.
#pragma kernel calc
RWStructuredBuffer<float> buffer;
groupshared float sharedbuffer[128];
[numthreads(128,1,1)]
void calc(
uint3 Gid : SV_GroupID,
uint3 id : SV_DispatchThreadID,
uint GI : SV_GroupIndex)
{
if (GI < 100) {
for(int i = 0; i < 10000; i++){}
}
sharedbuffer[GI] = buffer[GI + Gid.x * 128];
//GroupMemoryBarrierWithGroupSync();
float sum = 0.0;
[loop]
for (int j = 0; j < 128; j++) {
sum += sharedbuffer[j];
}
buffer[id.x] = sum;
}
これを解決するため,コメントアウトされているGroupMemoryBarrierWithGroupSync()
を使用すると...
うまくいってますね
GroupMemoryBarrierWithGroupSync()
を使うことでちゃんとメソッドが呼ばれるまでの処理が同期がとれるまで一度ブロックされていることが分かりますね.
本題まで長くなってしまいましたが,ここまで読んでいただいてありがとうございます,なにか不明な点や,誤っている点がありましたら教えてください.
まとめ
つまりGroupMemoryBarrierWithGroupSync()
使用する場面としては同一GroupThread内でThreadごとに処理の重さが違うけどその後にgroupshared
変数に値を格納して演算結果を欲しいときって感じです.
補足と経験談
自分は研究分野として流体シミュレーションを扱っているのですが,その計算のなかである粒子とその近傍粒子についての関係を計算する場面があります.ベーシックな方法として総当たりしてその中から近傍(一定半径内)の粒子を選んで計算するっていう方法があります.粒子(Threadに相当)によってその近傍の粒子数って変わってくるのでThreadごとの計算量って変わってくるんです.なのでGroupMemoryBarrierWithGroupSync()
を使ってあげないと正しい計算結果が得られなかったりします.まぁ実際目で見る分にはあまり齟齬がない場合も多いんですが,ちゃんとやったほうがいいですよね...頑張ります.