結論から言うと「L1データキャッシュが 2-way セットアソシアティブであり、スラッシングが発生するアクセスの仕方をするから」のようです。データシートを軽く見た限りでは、Athlon 64 以降の CPU は全て L1データキャッシュが 2-way のようなので、この問題に引っかかります。

一方、手元の Intel Core 2 Quad Q6600 だと 8-way なのでスラッシングが発生せず、遅くなりません。おそらく 4-way でもスラッシングが発生しないものと思われます。

以下経過と詳細。

そういや、友人が Athlon 64 マシンを持っていたなあ、と思って、遊んでいる数少ない時間(それなりに忙しいマシンぽい)にリモートでさわらせてもらえるようにお願いして調査開始。ULRG で RGB24 入力の場合と ULY2 で YUY2 入力の場合とで大体同じ処理をしているにもかかわらず、ULY2 では遅くならずに ULRG では遅くなるのが不思議だったので、処理が異なる raw フォーマットから planar フォーマットへの変換ルーチンで、あーでもないこーでもないと試行錯誤してました。

ULY2 での処理(抜粋)

	switch (m_icc->lpbiInput->biCompression)
	{
	case FCC('YUY2'):
	case FCC('YUYV'):
	case FCC('YUNV'):
		for (p = pSrcBegin; p < pSrcEnd; p += 4)
		{
			*y++ = *p;
			*u++ = *(p+1);
			*y++ = *(p+2);
			*v++ = *(p+3);
		}
		break;

ULRG での処理(抜粋)

	switch (m_icc->lpbiInput->biCompression)
	{
	case BI_RGB:
		switch (m_icc->lpbiInput->biBitCount)
		{
		case 24:
			for (pStrideBegin = pSrcEnd - m_dwRawGrossWidth; pStrideBegin >= pSrcBegin; pStrideBegin -= m_dwRawGrossWidth)
			{
				const BYTE *pStrideEnd = pStrideBegin + m_icc->lpbiInput->biWidth * 3;
				for (p = pStrideBegin; p < pStrideEnd; p += 3)
				{
					*g++ = *(p+1);
					*b++ = *(p+0) - *(p+1) + 0x80;
					*r++ = *(p+2) - *(p+1) + 0x80;
				}
			}
			break;

で、

  • 色差を取ってから 0x80 を加算する処理があるが、0x80 を加算するのを省略する → 変化なし
  • 色差を取る処理も省略する → やっぱり変化なし
  • RGB24 はボトムアップフォーマットでアクセスする順序が問題なのではないかと思い、その辺を無視してフレームデータの先頭から変換する → それでも変化なし

うーん。

で、どーしようもないなぁ、と思いつつ風呂に入ってたのですが、ダメ元で次のネタを思いつきました。

  • 赤成分を書き込まなくする → 65ms/f から 22ms/f まで一気に速くなった
  • 赤成分に0を書き込む(対照群) → 元のまま変化なし

ナニコレ!?
色成分のフレームバッファは VirtualAlloc で確保していて初期値は 0 なので、書き込むかどうかにかかわらず 0 のままです。なので、速度の差は planar 変換のところで起きているのは間違いありません。赤成分を書き込まないことでメモリアクセスの回数は 2/3 程度になるはずですが、それを遥かに上回って速くなっているので、何かそれ以外の現象がありそうです。

いい加減夜も遅い(というか未明)ので、今度は寝ながら考えてたら、もしかしてキャッシュが効いてないんじゃ?と思いついて、起きてから CPU のスペックシートを眺めてみたら 2-way セットアソシアティブ。ああ、これだ。上に示したルーチンで、g, b, r の各ポインタは「常に」同じエントリアドレスのキャッシュラインを参照する事になり、連想度よりアクセスする先の数の方が大きいのでアクセスするたびにキャッシュミスして猛烈に遅くなります(raw フォーマットの方のポインタである p は指す先の進み具合が g, b, r とは異なり、同じエントリアドレスになる可能性が低いので、あまり考えなくてよい)。ULY2 においては、u, v は常に同じエントリアドレスのキャッシュラインを参照しますが、y と p はそれぞれ u, v とは進み具合が異なるので、連想度とアクセスする先の数が同じのため、一応セーフです。

とりあえず理由は分かった、たぶん。
しかし、いくら L1 キャッシュは速度が重視される関係で連想度を高くできないとはいえ、2-way はさすがにないんじゃないの…

Trackback

no comment untill now

Add your comment now