「AMDで使うと遅いんだけど」 x86/x64最適化勉強会 #4 LT 梅澤威志 (UMEZAWA Takeshi) @umezawa_takeshi Q: dis ってんの? A: disasm なら少々… 自己紹介 • 映像可逆圧縮コーデック Ut Video Codec Suite の作者 ※ http://umezawa.dyndns.info/wordpress/?cat=28 • ある2ちゃんねらー曰く、 UtVideo唯一の欠点 作者がニコ厨 ※ http://pc11.2ch.net/test/read.cgi/avi/1205486331/178 – まったくツンデレなんだから… 前置き • 今回話すことは、何人かの人は過去の x86/x64最適化勉強会で雑談などで既に聞い ているはずです。 • blog を検索しても出てきます。 • 知ってる人は寝てていいです。 あるユーザの報告 • 「AMD で ULRG や ULRA を使うとエンコードが すごい遅いんだけど」 – ULRG は内部保持形式が RGB 8bpc のもの。 ULRA は同じく RGBA 8bpc のもの。 – ULY2 (YUV422 8bpc) や ULY0 (YUV420 8bpc) は遅くないらしい。 • デコードはエンコードほどではないが、やっぱ り遅いことは遅いらしい。 実測 • 確かに遅い。 • ULRG は 24bpp であり、16bpp である ULY2 と 比較して同じ画像サイズの時 1.5 倍ぐらい遅 いことが期待されるが、エンコードの場合は 期待されるより3倍ぐらい遅い。 明らかに何かおかしい エンコーダの実装 • 以下の順序で処理する。 – Packed → Planar 変換 – フレーム内予測 – ハフマン符号化 • フレーム内予測とハフマン符号化は種類によ らず全く同じ処理なので、Planar 変換に問題 がありそう。 – 本来は全体の 1 割ぐらいの時間なんだけど… Planar 変換 r = VirtualAlloc(NULL, width * height, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE); g = (ditto) b = (ditto) for (p = srcbegin; p < srcend; p += 3) { *(g++) = p[1]; *(b++) = p[0] - p[1] + 0x80; *(r++) = p[2] - p[1] + 0x80; } ちょっと変えてみる…速度変わらず r = VirtualAlloc(NULL, width * height, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE); g = (ditto) b = (ditto) for (p = srcbegin; p < srcend; p += 3) { *(g++) = p[1]; *(b++) = p[0] - p[1]; // + 0x80; *(r++) = p[2] - p[1]; // + 0x80; } さらに変えてみる…やっぱり遅い r = VirtualAlloc(NULL, width * height, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE); g = (ditto) b = (ditto) for (p = srcbegin; p < srcend; p += 3) { *(g++) = p[1]; *(b++) = p[0]; // - p[1] + 0x80; *(r++) = p[2]; // - p[1] + 0x80; } 遅くなくなった!? r = VirtualAlloc(NULL, width * height, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE); g = (ditto) b = (ditto) for (p = srcbegin; p < srcend; p += 3) { *(g++) = p[1]; *(b++) = p[0]; r++; } 対照群:遅いまま r = VirtualAlloc(NULL, width * height, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE); g = (ditto) b = (ditto) for (p = srcbegin; p < srcend; p += 3) { *(g++) = p[1]; *(b++) = p[0]; *(r++) = 0; } ULY2 の場合(遅くない) y = VirtualAlloc(NULL, width * height, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE); u = VirtualAlloc(NULL, width * height / 2, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE); v = (ditto) for (p = srcbegin; p < srcend; p += 4) { *(y++) = p[0]; *(u++) = p[1]; *(y++) = p[2]; *(v++) = p[3]; } Q: なぜこうなるのでしょう? A: store で毎回 L1 キャッシュミス するから VirtualAlloc() • 呼び出しプロセスのアドレス空間を予約ある いはコミットする。 – POSIX の mmap() に似ている。 • 予約あるいはコミットするアドレスは「割り当て 粒度 (allocation granularity)」に丸められる。 – ページサイズ (=4KiB) ではない。 – 少なくとも Windows XP~7 においては、Win32 で の割り当て粒度は 64KiB である。 AMD の L1 キャッシュ • 長らく 命令 64KiB + データ 64KiB の構成 • 長らく 2-way セットアソシアティブ • → 32KiB ごとに同じエントリアドレスになる。 両方合わせると… • VirtualAlloc() で割り当てられたバッファは 64KiB 境界に整列しているので、各バッファの 先頭アドレスは全て同じエントリアドレスを持 つ。 • ULRG では g, b, r のポインタが同じ速度で進 み「常に」同じエントリアドレスになるため、1 バイトアクセスするたびにキャッシュミスして 猛烈に遅くなる。 解決方法 • ポインタが同じ速度で進むのだから、最初か らずらしておけば今度は絶対に同じエントリア ドレスにはならない。 • p は 3 倍速で進むのでエントリアドレスが重な ることがあるが、その時でも同じエントリアドレ スを使っているのは 2 つだけなのでセーフ。 これで解決 r = VirtualAlloc(NULL, width * height, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE); g = VirtualAlloc(NULL, width * height + 256, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE) + 256; b = VirtualAlloc(NULL, width * height + 512, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE) + 512; … ※ 256 でいいかどうかは議論(というか計測)の余地がある。 当時(あまり)考えなかったこと • L1 キャッシュを共有する複数の物理スレッド – Intel HT とかのことだが、Intel 系だと 8-way なの で、2 スレッド走っても 1 スレッドあたり 4-way で 問題なし。 – AMD Bulldozer の場合、L1 は Bulldozer モジュー ルごとではなくコアごとに持ってるらしいから、半 分にはならない? まとめ? • キャッシュの連想度にも(たまには)気を付け ましょう。 • でも 2-way はひどいと思います。 – Intel は 8-way なのに。 Q: 結局 x86 関係あんの? A: さあ…?
© Copyright 2024 ExpyDoc