忍者ブログ

Memeplexes

プログラミング、3DCGとその他いろいろについて

[コンピュートシェーダー] groupsharedとGroupMemoryBarrierWithGroupSync()

スレッドグループ

GPGPUに限った話ではありませんが、
何か並列計算していると、
別のスレッドが計算を終わってから次の計算をしたい、
ということが良くあります。

次の計算が別のスレッドの計算結果に依存するような場合です。

そんなときに役に立つのがHLSLの組み込み関数、
GroupMemoryBarrierWithGroupSync()関数です。

void GroupMemoryBarrierWithGroupSync();

これを使うと、コンピュートシェーダーの他のスレッドの計算が終わるまで待ちます。
別の言い方をすると、スレッドグループ内のすべてのスレッドが
これの呼び出しに到達するまで実行をブロックします。

ただしこの関数はどんな時にも使えるというわけではなくて、
コンピュートシェーダーでgroupsharedな変数にアクセスしている場合に限られます。

groupsharedは変数の記憶域修飾子で、
これがついた変数はスレッドグループ内で共有されます。

例えばこんなふうに使います。

groupshared int data[100];

こうなっていると、dataはスレッドグループ内で共有され、
これに対するアクセスは
GroupMemoryBarrierWithGroupSync()の保護の対象となります。

なお、groupsharedな変数はコンピュートシェーダーの
スレッドグループ内で共有されているため、
アクセスするときのインデックスには
SV_DispatchThreadIDよりもSV_GroupIndexを使うほうがいいでしょう。

SV_GroupIndexはグループ内ごとのスレッドのインデックスです。
(別グループのスレッドとは値が衝突することはありますが、
自分のグループの他のスレッドとは衝突しません)
詳しくはMSDNのページによい図があります。


サンプルコード

まずは、GroupMemoryBarrierWithGroupSync()を使った時と使わないとき
の違いをはっきりさせたいと思います。

C#側はこうです。
こちらはHLSLとは関係ありませんので固定です。
これはHLSLで計算した結果を表示する、というプログラムです。
C#側からHLSLに入力として何かデータを与えたりはしません。

Program.cs

using System.Collections.Generic;
using System.Linq;

using SlimDX;
using SlimDX.Direct3D11;
using SlimDX.D3DCompiler;

class Program
{
    static Buffer bufferSum;

    static void Main(string[] args)
    {
        Device device = new Device(DriverType.Hardware);

        bufferSum = createStructuredBuffer(
            device, 128 * sizeof(int), BindFlags.UnorderedAccess
            );

        initComputeShader(device);
        device.ImmediateContext.Dispatch(1, 1, 1);
        writeBuffer(device, bufferSum);
    }

    static void initComputeShader(Device device)
    {
        device.ImmediateContext.ComputeShader.SetUnorderedAccessView(
            new UnorderedAccessView(device, bufferSum), 0
            );

        ShaderBytecode shaderBytecode = ShaderBytecode.CompileFromFile(
            "MyShader.fx",
            "MyComputeShader",
            "cs_5_0",
            ShaderFlags.None,
            EffectFlags.None
            );
        device.ImmediateContext.ComputeShader.Set(
            new ComputeShader(device, shaderBytecode)
            );
    }

    static void writeBuffer(Device device, Buffer buffer)
    {
        Buffer cpuAccessibleBuffer = createCpuAccessibleBuffer(device, buffer.Description.SizeInBytes);
        device.ImmediateContext.CopyResource(buffer, cpuAccessibleBuffer);
        int[] readBack = readBackFromGpu(cpuAccessibleBuffer);

        foreach (var number in readBack)
        {
            System.Console.WriteLine(number);
        }
    }

    static Buffer createStructuredBuffer(Device device, int sizeInBytes, BindFlags bindFlags)
    {
        return new Buffer(
            device,
            new BufferDescription
            {
                SizeInBytes = sizeInBytes,
                BindFlags = bindFlags,
                OptionFlags = ResourceOptionFlags.StructuredBuffer,
                StructureByteStride = sizeof(int)
            }
            );
    }

    static Buffer createCpuAccessibleBuffer(Device device, int sizeInBytes)
    {
        return new Buffer(
            device,
            new BufferDescription
            {
                SizeInBytes = sizeInBytes,
                CpuAccessFlags = CpuAccessFlags.Read,
                Usage = ResourceUsage.Staging
            }
            );
    }

    static int[] readBackFromGpu(Buffer from)
    {
        DataBox dataBox = from.Device.ImmediateContext.MapSubresource(
            from,
            MapMode.Read,
            MapFlags.None
            );
        int elementCount = (int)(dataBox.Data.Length / sizeof(int));
        return dataBox.Data.ReadRange<int>(elementCount);
    }
}

HLSL側はこうです。
GroupMemoryBarrierWithGroupSyncがあるのと無いのとでは
結果がなるべく違ってくるように書いています。
(もちろん、環境によって違うでしょうから、適宜変数を変えてみたりしてください)

MyShader.fx
RWStructuredBuffer<int> MyBuffer : register(u0);

static const int ElementCount = 128;
groupshared int data[ElementCount];

int getSum()
{
int result = 0;

for(int i = 0; i < ElementCount; i++)
{
result += data[i];
}

return result;
}

[numthreads(ElementCount, 1, 1)]
void MyComputeShader(uint groupIndex : SV_GroupIndex )
{
int time = 0;

//スレッドによって処理時間が変わる
for(int i = 0; i < groupIndex; i++)
{
time++;
}

data[groupIndex] = time;
//GroupMemoryBarrierWithGroupSync();
MyBuffer[groupIndex] = getSum();
}

さて私の環境では実行結果はこうなりました:

1951777886
1951777886
1951777886
1951777886
1951777886
1951777886
1951777886
1951777886
1951777886
..(中略)
1951777886
8128
8128
8128
8128
8128
8128
8128
8128
..(中略)
8128

このプログラムはある配列の中身の合計を、
複数のスレッドで計算すします。
複数のスレッドが扱うのは同じ配列ですから、
どのスレッドでも同じ結果になるとおもうかもしれません。
が、結果は見ての通り、19517778868128の2種類の違う値になってしまっています。
 
その原因は(これはわざとそうなるよう仕組んでいるのですが)、
スレッドごとに計算を開始する時間がかなり異なっているからです。
あるスレッドは配列の初期化が終わる前に合計の計算を開始するために1951777886というような値を出します。
一方で、別のスレッドは配列の初期化が終わった後に計算を開始するため8128という、正しい値が出るのです。
 
GroupMemoryBarrierWithGroupSync()のコメントアウトをとってみてください。
こんどは、全て8128という数になるはずです。
GroupMemoryBarrierWithGroupSync()によって全てのスレッドが
配列の初期化が終わるまで待つようになるのです。


拍手[1回]

PR