TypeZ性能評価(2)

で、肝心の外付けGPUなんだけど。まぁスペックレベルですでに死亡フラグが立ってるとかそういう話もありますが。
まず机上から検証してみる。

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

GPU-Zで見てみると480SPで600MHzなのでスペック上のコンピューティングパワーは480*0.6*2=576GFLOPS/sec(57.6G命令/sec相当)。
なんというか、効率の悪いシェーダプログラムを動かしたらCPUにうっかり負けそうな数値ですな。まぁふつうは数倍は早いはずだけど・・・。Fusion APUの足音がすぐ後ろに来てる感がたまらないね。
パワー的にはどうなんだろう。コンピュータグラフィックの分野だけならこれでも大丈夫なのかな?実際のアプリがどの程度なのかよくわからないよね。イメージ的にはテクスチャアクセスの方が重要って感じはするけど。

今回のシステムだとシステムメモリからVRAMまでの経路は

ってなことになりそう。(CPUとGPUは内部を省略して書いている。わからない部分も多いし。内部バスとか)
内臓グラフィックスのみで動く場合は図中の赤枠で囲った範囲だけで動くと思う。(ディスプレイ出力の経路は書いてない。よくわからんし)
あと、VRAMがDDR3ってのは予想。wikipedia には書いてあったけど、GPU-ZだとHyperMemoryとしか書いてなかった。Light Peakまわりもちょっと怪しい。

経路上のスペックレベルでの帯域幅

種別 帯域幅 方向
DDR3 1600MHz 64bit dual 25.6GB/sec 両方合算
Light Peak x1 1.25GB/sec 片側
PCIe Gen2 x4 1.6GB/sec 片側
DDR3 900MHz 128bit single 14.4GB/sec 両方合算

「両方合算」ってのは上り下り両方を合わせた数値で片方向だけで帯域を使いきることができるタイプ。上り下りで仲良く帯域を分け合った場合は半分の帯域になる。「片側」のタイプは上り下りで帯域が分かれていてシェアできないタイプ。両側をフルに使いきれば合計の帯域幅は倍になる。

で、表から数値上のボトルネックはLight Peakの1.25GB/secになる。上り下りを合算しても2.5GB/secで打ち止め。画面を外付けディスプレイじゃなくて本体に表示している場合は映像データを転送するために1920*1080*60*24bit=356MB/secくらい帯域がさらに消費されるので2GB/secほどになってしまう。

参考までにデスクトップはこんな感じ。

種別 帯域幅 方向
DDR3 1333MHz 64bit triple 32.0GB/sec 両方合算
QPI 2.4GHz 9.6GB/sec 片側
PCIe Gen2 x16 6.4GB/sec 片側
GDDR5 1.2GHz 256bit 153.6GB/sec 両方合算

ネックはPCIeの6.4GB/secだね。


これを確かめるためにシステムメモリとVRAMとでテクスチャデータをやり取りするプログラムを作ってみた。
まず、内臓グラフィックスと7670Mの2つつながって見えるのでIDXGIAdapterを列挙して。

std::vector	table_;
void initTable(void)
{
	IDXGIFactory	*factory;
	IDXGIAdapter	*adapter; 

	::CreateDXGIFactory(__uuidof(IDXGIFactory), reinterpret_cast(&factory));

	for(UINT i=0;factory->EnumAdapters(i, &adapter)!=DXGI_ERROR_NOT_FOUND;i++){
		table_.push_back(adapter);
	}

	factory->Release();
}

次にデバイスを作って。

ID3D11Device			device_;
ID3D11DeviceContext		context_;
void initDevice(IDXGIAdapter *adap)
{
	D3D_DRIVER_TYPE		type;

	const D3D_FEATURE_LEVEL		features[] = {D3D_FEATURE_LEVEL_11_0};

	type = (adap == NULL)? D3D_DRIVER_TYPE_HARDWARE : D3D_DRIVER_TYPE_UNKNOWN;
	::D3D11CreateDevice(adap, type, NULL, 0, features, countof(features), D3D11_SDK_VERSION, &device_, NULL, &context_);
}

後で動かしてわかったんだけど、IDXGIAdapterを指定する場合にはUNKNOWN指定じゃないとE_INVALIDARGを返してくるっぽい。D3D11CreateDevice()関数ヘルプの「Remarks」欄に書いてあった。なんでそんな仕様なの…
ちなみに、Power Media Dockをつないでいても7670Mしか列挙されず内蔵GPUは見えなかった。別に列挙しなくてよかったらしい。GPU-Zだと2つとも見えるんだけど逆にどういう仕組みなのか気になる。

ま、それはともかく。

バイスを作ったらテクスチャを作る。D3D11_USAGE_DEFAULT(VRAM割り当てを期待)とD3D11_USAGE_STAGING(システムメモリ割り当てを期待)とでID3D11Texture2Dを作っている。サイズは縦横1Kpixelのfloat4で16MBのテクスチャを512MB分(32個)ずつそれぞれ用意した。
テクスチャ1枚のサイズを抑え気味にしたのはサイズ制限に引っかかると面倒なのと実アプリでも使いそうなサイズで計測しようと思ったので。全体を512MBにしたのは1GBのVRAMにも収まるようにしたかったので。1GBぴったりとかにするとプライマリバッファとかの分で使ってるVRAM分だけあふれたりするし。
(以下で呼び出してるcreateTexture2d()関数は単にDESCに情報つめて呼ぶだけの自作ラッパ関数なので掲載省略)

std::vector	vram_;	// VRAM
std::vector	sram_;	// システムメモリ
struct Resolution
{
	typedef int		dot_type;

	dot_type		width_;
	dot_type		height_;

	Resolution(void):width_(0), height_(0) {}
	Resolution(dot_type w, dot_type h):width_(w), height_(h) {}
};
ID3D11Texture2D *createCpuTexture2d(ID3D11Device *device, const Renderer::CommonType::Resolution &r, DXGI_FORMAT format, const D3D11_SUBRESOURCE_DATA *data)
{
	return createTexture2d(device, r, format, D3D11_USAGE_STAGING, static_cast(0), static_cast(D3D11_CPU_ACCESS_READ | D3D11_CPU_ACCESS_WRITE), static_cast(0), data);
}
ID3D11Texture2D *createGpuTexture2d(ID3D11Device *device, const Renderer::CommonType::Resolution &r, DXGI_FORMAT format, D3D11_USAGE usage, UINT bind, const D3D11_SUBRESOURCE_DATA *data)
{
	return createTexture2d(device, r, format, usage, static_cast(bind), static_cast(0), static_cast(0), data);
}
void initTexture(int count, int width, int height, ID3D11Device *device)
{
	Resolution	res(width, height);

	for(int i=0;i

後は合計2GB分になるまでCopyResource()を繰り返して、かかった時間を計測するだけ。2GBという数値は特に根拠なしあまり小さいと計測誤差が無視できないかなとかそんな程度。

void getTexture(ID3D11DeviceContext *context, unsigned start, unsigned end)
{
	for(unsigned i=start;iCopyResource(sram_[i], vram_[i]);
	}

	context->Flush();
}
void putTexture(ID3D11DeviceContext *context, unsigned start, unsigned end)
{
	for(unsigned i=start;iCopyResource(vram_[i], sram_[i]);
	}

	context->Flush();
}

最後に転送レートに換算し直して終了。いろいろはしょってるけど、ざっくりとこんな感じで。

class StopWatch
{
	private:
		LARGE_INTEGER		start_;
		LARGE_INTEGER		end_;
		LARGE_INTEGER		unit_;
	public:
		StopWatch(void);
		void start(void);
		void stop(void);
		double getRate(unsigned rate) const;
};
StopWatch::StopWatch(void)
{
	::QueryPerformanceFrequency(&unit_);
}
void StopWatch::start(void)
{
	::QueryPerformanceCounter(&start_);
}
void StopWatch::stop(void)
{
	::QueryPerformanceCounter(&end_);
}
double StopWatch::getRate(unsigned rate) const
{
	return boost::numeric_cast(rate * unit_.QuadPart) / (end_.QuadPart - start_.QuadPart);
}

	StopWatch	watch;
	int				count;
	my_size_type	target_byte;

	// 「2GB÷512MB=4周」の計算
	target_byte = target_gb * unit_gb;
	count = target_byte / (texture_size * vram_.size());

	watch.start();	// 計測開始
	for(int i=0;i

後述するが、これだとダメだったので以下のように変更。(実際にはこれでもダメだったけどね・・・)

// Map()してまわるだけの関数
void mapForRead(ID3D11DeviceContext *context, unsigned start, unsigned end)
{
	D3D11_MAPPED_SUBRESOURCE	resource;

	for(unsigned i=start;iMap(sram_[i], 0, D3D11_MAP_READ, 0, &resource);
		context->Unmap(sram_[i], 0);
	}
}

	// 測定ループはこっちに変更
	for(int i=0;i

write側もMap()してからCopyResource()するように変更。



長くなってきたのでここでいったん切って次回。