5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Unity][ComputeShader]GroupMemoryBarrierWithGroupSyncとgroupsharedについて

Posted at

目的

GroupMemoryBarrierWithGroupSyncgroupsharedについての理解を共有出来たらなと思ってこの記事を書いています.
本題に入る前に必要な予備知識をいくつか説明しますが,理解している人は全然飛ばして本題の説明部分まで行ってください.

予備知識

まず導入としてComputeShader内で使用されるセマンティクスについてサンプルを通して説明を行っていきます.

SV_DispatchThreadID

実行中のThreadを一意に特定することが出来るIDを返します.
こんな感じのComputeShaderを定義したとします.

.c
#pragma kernel calc
RWStructuredBuffer<float> buffer;
[numthreads(128,1,1)]
void calc(uint3 id : SV_DispatchThreadID)
{
	uint idx = id.x;
	buffer[idx] = (float)idx;
}
.cs
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になっていることが確認できます.
bandicam 2020-04-15 12-53-40-663.jpg

SV_GroupID

現在実行されているkernelが所属するThreadGroupのID(3次元)です

.c
#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;
}
.cs
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成分のみ変化してます.
bandicam 2020-04-15 13-05-05-803.jpg
なぜこのような結果となるかというと,今回128*8のbufferがあり,Group内にあるThreadは128なので8Group存在するわけです.なので上のような結果になるわけです.

SV_GroupThreadID

Group内でのThreadIDを返します.

.c
#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で繰り返し表現されます.
bandicam 2020-04-15 13-12-27-139.jpg

SV_GroupIndex

Group内でindexであるSV_GroupThreadIDを1D 化したindexが返ってきます.

.c
#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となります.
bandicam 2020-04-15 13-17-54-675.jpg

ここからが本題

GroupMemoryBarrierWithGroupSyncgroupsharedについて説明を行っていきます.
GroupMemoryBarrierWithGroupSync : 同一Group内の他スレッドの処理がこのメソッドが呼ばれる部分までの処理が終わるまでの処理を待って同期をとることを可能とします.
groupshared : 変数に対する記憶域修飾子でこれが付いた変数はGroupThread内で共有することが出来るようになります.さっきの例でいうと(1.0, 0.0, 0.0)とか(7.0, 0.0, 0.0)など同一のSV_GroupIDを持っているThreadでは共有できるってことですね.

どんな時に有効?

c#側でbufferの中身を定義する際に同一GroupThread内のBufferの値をすべて足し合わせるとThreadGroupIDになるようにしてみました.

.cs
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ループの処理を加えました.

.c
#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;
}

この結果を見ると..
おかしい..
bandicam 2020-04-15 14-44-50-290.jpg

これを解決するため,コメントアウトされているGroupMemoryBarrierWithGroupSync()を使用すると...
うまくいってますね
bandicam 2020-04-15 14-45-56-321.jpg

GroupMemoryBarrierWithGroupSync()を使うことでちゃんとメソッドが呼ばれるまでの処理が同期がとれるまで一度ブロックされていることが分かりますね.
本題まで長くなってしまいましたが,ここまで読んでいただいてありがとうございます,なにか不明な点や,誤っている点がありましたら教えてください.

まとめ

つまりGroupMemoryBarrierWithGroupSync()使用する場面としては同一GroupThread内でThreadごとに処理の重さが違うけどその後にgroupshared変数に値を格納して演算結果を欲しいときって感じです.

補足と経験談

自分は研究分野として流体シミュレーションを扱っているのですが,その計算のなかである粒子とその近傍粒子についての関係を計算する場面があります.ベーシックな方法として総当たりしてその中から近傍(一定半径内)の粒子を選んで計算するっていう方法があります.粒子(Threadに相当)によってその近傍の粒子数って変わってくるのでThreadごとの計算量って変わってくるんです.なのでGroupMemoryBarrierWithGroupSync()を使ってあげないと正しい計算結果が得られなかったりします.まぁ実際目で見る分にはあまり齟齬がない場合も多いんですが,ちゃんとやったほうがいいですよね...頑張ります.

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?