投稿日:2022/03/02

並列化

   

おはようございます。
エンジニアの yamamoto-mo です。

ここの所大作ラッシュですね。
ゲーム好きな方はお忙しい毎日を過ごされているかと思います。
…積みゲーが増える…

さて今回のブログのネタなんですが、
代表の川本が以前投稿した「CPUとGPUの動作の流れ」で明記していた
コマンドの作成処理を並列化することでCPU側の最適化を図ること
に関して少し具体的に触れていこうかと思います。
便乗とも言いますね。

ブログ当番は投稿日の約二週間前に「次はあなたの番です」と指名を受けるのですが、
正直思いつかなかったんですよね…w
一週間ほど「何にしようかな…」と悩んだ挙句の便乗です。

具体的に触れるなら、と実際にプログラムを用意してみたのですが、
思ったより時間かかったので正直後悔してます…w

 

並列化

川本のブログでも触れていますが、
ここでも簡単に並列化について簡単にお話しようと思います。

仮に千羽鶴を作るとしましょう。
折り鶴を一つ折るのに 1 分かかるとすると、
千個折るには 1000 分、約 16 時間かかる事になります。

とても時間がかかるので、短時間で終わらせるにはどうすれば良いか考えた時、
手伝ってもらうのが一番早いですよね。
10 人で同時に作れば、単純計算で 100 分、約 1 時間 40 分で終わります。

「一人でやると時間がかかる事を複数人で同時に行う」
これが並列化です。

コンピュータの場合、
上記の「人」が(CPU あるいは GPU の)コアに置き換わります。
コアには論理コアや物理コアといった物があるのですが、
ここではひとまず「コア」と括って話を進めます。

川本の記事を引用すると、
CPU のコア一つで全部の描画コマンド作成処理をすると凄く時間がかかるので、
複数のコアで分散して並列に描画コマンドを作成しましょう。
という事になります。

じゃあどうやってコア毎に処理を分散するの?ですが、
この時にスレッドが用いられます。
スレッドは分散させたい処理の単位と考えて頂ければ大丈夫かと思います。
スレッドに行わせたい処理を設定し動作させる事で、
各コアに割り振られて並列に処理する事が出来るようになります。

千羽鶴の例で当てはめると
・10 スレッド用意する
・各スレッドには「折り鶴を 100 個作る」という処理を設定する
みたいな感じでしょうか。

 

サンプル

急造ではありますがこのブログの為にサンプルプログラムを用意しました。
ポリゴンを 24000 個描画するサンプルです。

単なるポリゴンを大量に描画する場合、
本来であればバッチングやインスタンシングが利用されるので、
こんな事をする機会は無いと思いますが、
並列化でCPUの処理時間が軽減できるかどうかを試す為に用意しました。

色々コードがありますが、見て頂くのは main.cpp の下記関数のみです。

//---------------------------------------------------------------------------------
/**
 * @brief	アプリケーションの更新処理を行う
 */
bool appUpdate() noexcept {
	if (window::Window::instance().isEnd()) {
		return false;
	}

	{
		TIME_CHECK_SCORP("更新時間");

		// 描画開始
		{
			commandListBegin.reset();
			frameBuffer.startRendering(commandListBegin);
			commandListBegin.get()->Close();
		}

		// 各メッシュ描画
		{
			concurrency::parallel_for(0, commandListNum, [&](auto index) {
				commandLists[index].reset();

				frameBuffer.setToRenderTarget(commandLists[index]);
				pso.setToCommandList(commandLists[index]);
				mesh.setToCommandList(commandLists[index]);

				auto start = index * (objectNum / commandListNum);
				auto end = start + (objectNum / commandListNum);
				for (auto i = start; i < end; ++i) {
					// 描画
					constantBuffer.setToCommandList(commandLists[index], i);
					commandLists[index].get()->DrawIndexedInstanced(6, 1, 0, 0, 0);
				}
				commandLists[index].get()->Close();
			});
		}

		// 描画終了
		{
			commandListEnd.reset();
			frameBuffer.finishRendering(commandListEnd);
			commandListEnd.get()->Close();
		}

		// コマンドリスト実行
		{
			std::array<ID3D12CommandList*, commandListNum + 2> lists;

			// 描画開始
			lists[0] = commandListBegin.get();
			// 各メッシュ描画
			for (auto i = 0; i < commandListNum; ++i) {
				lists[i + 1] = commandLists[i].get();
			}
			// 描画終了
			lists[commandListNum + 1] = commandListEnd.get();

			// 実行
			commandQueue.get()->ExecuteCommandLists(lists.size(), static_cast<ID3D12CommandList**>(lists.data()));
			SwapChain::instance().present();

			// フレームバッファのインデックスを更新する
			frameBuffer.updateBufferIndex(SwapChain::instance().currentBufferIndex());
		}
	}

	// 時間表示
	TIME_PRINT("");

	// フェンス設定
	{
		fence.get()->Signal(0);
		commandQueue.get()->Signal(fence.get(), 1);
	}

	// GPU処理が全て終了するまでCPUを待たせる
	{
		auto event = CreateEvent(nullptr, false, false, "WAIT_GPU");
		fence.get()->SetEventOnCompletion(1, event);
		WaitForSingleObject(event, INFINITE);
		CloseHandle(event);
	}

	return true;
}

20 行目にある// 各メッシュ描画と書かれたコメントのスコープ内のみ、
描画コマンド作成の並列化を狙っています。

24000 個のポリゴンオブジェクトをコマンドリストの数で分割して、
分割した個数分単位で並列に描画する様にしています。

仮にコマンドリストの数が 4 つであれば、
1 つにつき 6000 個ごとに描画するといった形です。

コマンドリストの数はcommandListNumで決まっています。
設定している数を1~4くらいで変化させてみると、
大抵の場合、数が大きい方が更新時間が少なくなっているはずです。

※試す際の環境によってかなり違いがあると思いますのでご留意ください。

 

並列化の危険性

並列化は強力な武器になるのですが、
代わりに物凄く注意しながら利用する必要があります。

というのも、簡単にプログラムを壊せるようになり、
且つ壊れている原因を特定しにくくなっていく
からです。
さらに壊さない様に対処を入れていくと、
並列化しても大して処理が早くならないといった事にもなり得ます。

この辺りはまた別の機会にお話しようかなと思います。

 

免責事項

掲載された内容によって生じた損害等に関して、
弊社は一切の責任を負いかねますので、予めご了承ください。