IDXGISwapChain::Present()を基準に動いている件

前々からID3D11DeviceContext::Flush()を呼んでもいまいちコマンドが即座に発射されている気がしなかったり、GPU PerfStudio 2やPIX先生がIDXGISwapChain::Present()を発射しないとログれなかったりと、いやな予感はしてたんだけど実際に計測プログラムをGUI化してみてはっきりした。

結論としては

  • IDXGISwapChain::Present()を呼ぶとそれまでに呼び出したコマンドはGPUへ転送される
  • IDXGISwapChain::Present()を呼ばなくてもそのうちコマンドはGPUへ転送されるがかなり遅れる
  • ID3D11DeviceContext::Draw()系でもある程度コマンド転送される(体感ではPresent()の方が効果的)

という感じだった。

以前Compute Shaderの結果をもらうのに2秒かかる*1とか、データのコピーが遅い*2とか書いたような気がするけど、原因はPresent()を呼び出していないせいで実際にコマンドやデータがGPUに転送されるまでに間が開いているせいだったようだ。
上記の現象はPresent()を呼び出すことでかなり解消されてしまった。(まだ一部謎は残るけど・・・)

データのコピー

計測結果はこんな感じになった。1回目は計測値が極端に小さいので2回目以降のデータを採用した。
1回目はkernelでpage faultとかしてるのかな。検証方法を調べるのも面倒なのでそのままスルーするけど。

項目 5870 7670M 内蔵グラ
read 6.1 0.7 5.9
write 4.9 0.4 8.4
RW 5.3 0.5 6.8
理論値 6.4
(片側)
1.25
(片側)
25.6
(両方)

Present()呼び出し版でそれっぽく動くってことは、実は前回からD3D11_USAGE_STAGINGはシステムメモリに割り当たっていたのかもしれない。

read速度測定

5870が一番それっぽい数値になった。理論値より微妙に低いのはどこかにオーバーヘッドがあるんだろうね。7670Mは元々が小さい数値なのでよくわからないね。ディスプレイに表示するデータもやり取りしてるから帯域をフルに使えないし。内蔵グラだけはまったく理論値に及ばないけど、ハードとかがどういう実装になっているのかわからないので何とも言えないなぁ。

write/RW速度測定

writeは5870/7670Mでreadよりさらに数値が悪化してしまった。RWは上り下り両方で帯域を使って「片側」表記のバスは性能2倍を期待していたんだけど、1倍未満の性能しか出なかった。これはきっと検証プログラム側の問題だろうね。正直きちんとした測定をするにはもう少し測定方法を練らないとダメな気はしてる。
たとえばwrite測定では、D3D11_MAP_WRITEしてからGPUへ転送しているけどこれで厳密なwrite時間を計測できている気はしない。

コンピューティングパワー

HSV変換処理についても測定し直してみた。Draw()やPresent()がDispatch()に与える影響を見るために

  1. Dispatch()とPresent()をセットで呼ぶorDispatch()/Draw()を一通り呼んでからPresent()を1回だけ呼ぶ
  2. PixelShaderではdiscardしてPixel負荷をかけないDraw()呼び出しを1フレームに1、2、3、5、10、20回呼ぶ
  3. Dispatch()を1フレームに1、2、3、5、10、20回呼ぶ

を組み合わせて計測してみた。

計測ソースコードはこんなイメージ(疑似ソース)

void process_frame(bool is_kick_present, int num_draw, int num_kick)
{
    for(int k=0;k<num_kick;k++){    // 1、2、3、5、10、20回
        Dispath();
        if(is_kick_present){        // Dispatch()とPresent()をセットで呼ぶ or 呼ばない
            Present();
        }
    }
    for(int k=0;k<num_draw;k++){    // 1、2、3、5、10、20回
        Draw();                     // Pixel出力なしでシェーダ負荷のみのダミー
    }
    CopyResource();                 // 処理結果を読み戻す(バッファはトリプルバッファにして待ちを隠匿)
    Present();                      // これの呼び出しをもって1フレームと考える。Dispatch()とセットの方はややこしいのでフレーム数にカウントしない
    Map();                          // 読み出し終了を待つ(トリプルバッファなので3つ前のコピー待ち)
    Unmap();
}

測定結果はこんな感じ。40フレームにかかった時間を計測した。

グラフは上記1〜3の条件で並べてある。
大きな塊ごとにDispatch()回数が増えている。塊内部で後半徐々に増えているのはDraw()回数の増加に伴っている。

大まかな傾向は

  • ComputeShaderの起動回数を増やすと処理時間がかかる
  • Draw()呼び出しを増やしても大勢に影響しないがComputeShaderの起動回数が多いと重くなる
  • ComputeShaderをたたいてPresent()すると処理時間がかかる

というような感じかな。

40フレームでおよそ1秒なので1フレームは25msecくらい。Dispatch()20回呼び出しで2秒ちょっとに増えるので1.2秒と考えて1.2÷(20×40)でDispatch()1個でおよそ1.5ms増える計算になる。
ということは、25msという数値の大半はCopyResource()の転送時間ということになると思う。Dispatch()もDraw()もバックバッファには何も書き込んでいないのでPresent()自体は一瞬だと思うので。あと、Draw(()回数を増やした場合にDispatch()回数によって全体の処理時間が変わるってことは、Dispatch()が非効率に動いてるわけじゃなくてシェーダを本当に使い切ってるってことなのかな。あるいは、逆にComputeShaderが動いてると他のシェーダにも影響するとか?たぶんそれはないと思うけど。
でも、Dispatch()を1回でも増やすと全体の処理時間が増えてしまうというのはいったんなぜだろう。CopyResource()待ち時間の間にがんがん発射可能だと思ってたんだけど、どこか間違えたかな。
さらによくわからないのが、区間ごとの処理時間を計測すると試験パラメータごとに全然違う場所で時間がかかって見える。難しすぎる…

プログラムがなぜかGPU PerfStudio 2やPIX先生でデバッグできなくなっていたのでどこかミスったかな。
コンパイル環境をclangに変更した時にどこか設定とか間違ってるのかも。まぁ元がバグってたけどbcc32がよきに計らっていただけかもしれないが。