自動フィールドシフト 高速化 (振り返り編)

自動フィールドシフトはaji様によるとても素晴らしいAviutlプラグインである。なんならrigayaがAviutlを使い始めたのは自動フィールドシフトがあったからだったりする。

ただ残念ながら唯一の欠点として、処理速度が遅いという問題があったので、これをいろんな方法で高速化してきた。まあ試行錯誤とかでだいぶ時間も突っ込んできたし、自動フィールドシフト高速化は(おそらくあまり期待してる人はいないと思うけど)結構いろいろやってみているもののひとつ。

今回は、これを振り返ってみる。

自動フィールドシフトの処理の概要



前も書いた気がするけど、画像処理の高速化では、演算をSIMD化してしまうと、演算は十分速くなってしまい、演算そのものよりもその処理でどのくらいのメモリアクセスがあって、どういったアクセスパターンになっているかを把握することが重要になってくる。

自動フィールドシフトも重い処理はほぼSIMD化されているので、ここでは、主にどういう重い処理があって、その処理が読むデータと書き出すデータはどうなっているかを確認すると、まあおおまかにはこんな感じ。

afs_process_01.png

get_ycp_cahce
入力プラグインがデコードしたフレームを受け取り、フレームバッファにコピーする。基本的には、フレームのデコードとメモリコピーにかかる時間。

scan_frame
最も重い処理。スレッド並列化されていて、設定の「スレッド数」はここのスレッド数。フレームバッファの2枚のフレームを参照して縞や動きがあるかを解析し、解析データとして書き出す。

count_motion
解析データから、動きありとして判定された画素の数をフィールドごとに数える。解除パターン決定に使われる。

analyze_frame
2枚の解析データから、解析マップを作成する。

analyze_map_filter
解析マップにぼかしフィルタをかけ、細かなノイズによると思われる判定ノイズを取り除く。

count_stripe
解析マップから、縞ありとして判定された画素の数を数える。解除パターン決定に使われる。

blend
フレームバッファと解析マップから、インタレ解除されたフレームを再構成する。

というわけで、まあいろいろな処理をやるので、当然遅いのだ…。



計算時間の確認



計測環境

OSWin10 x64Win10 x64
CPUi7 4770K (4C/8T)i7 6700K (4C/8T)
CPU世代HaswellSkylake
Core4.0GHz4.3GHz
UnCore4.0GHz4.1GHz
キャッシュL3=8MBL3=8MB
メモリDDR3-1600, 2chDDR4-2933, 2ch
CL8-8-8-24-216-18-18-34-2


その他計測環境・計測条件
Aviutl 1.00
入力プラグイン: L-SMASH Works r917 (POP氏ビルド)
入力: MPEG2 1920x1080 29.97fps 10240フレーム
一発勝負

自動フィールドシフトのオリジナル( = r0 )と、これまでのいくつかの高速化版で、どのくらい速くなったのか測定した。

i7 4770K 測定結果
afs_speed_20161118_r19_4770k.png

i7 6700K 測定結果
afs_speed_20161118_r19_4770k.png

まあ、最初は劇的に、その後は少しずつ速くなっていっているのが分かる。オリジナル(=r0)からくらべると、まあ、だいたい3倍ぐらい速くなっているのが分かる。

それぞれどういう高速化をしたか振り返ると、



r0 (オリジナル) → r5
SSE化とSIMD化されていない箇所のSIMD化
・もともとのMMX命令(64bit幅)からSSE命令(128bit幅)に更新。
もともとはasmだったのだけど、ちょっとアセンブラを書く知識はなかったので、intrinsicで書いてみた。それなりに性能が出せた。

・count_motionなどSIMD化されていなかった処理をSIMD化。
count_motionのSIMD化は劇的な効果があった(15倍以上)。

まあ、これを

for(pos_y = top; pos_y < scan_h - bottom - ((scan_h - top - bottom) & 1); pos_y++){
sip = sp->map + pos_y * si_w + left;
if(is_latter_field(pos_y, sp->tb_order)){
for(pos_x = left; pos_x < scan_w - right; pos_x++){
lf_motion += ~*sip & 0x40;
sip++;
}
}else{
for(pos_x = left; pos_x < scan_w - right; pos_x++){
ff_motion += ~*sip & 0x40;
sip++;
}
}
}


こうする古典的なSIMD化である。

#if USE_POPCNT
#define popcnt32(x) _mm_popcnt_u32(x)
#else
#define popcnt32(x) popcnt32_c(x)
#endif

const __m128i xMotion = _mm_set1_epi8(0x40);
for (int pos_y = sp->clip.top; pos_y < y_fin; pos_y++) {
BYTE *sip = sp->map + pos_y * si_w + sp->clip.left;
const int is_latter_feild = is_latter_field(pos_y, sp->tb_order);
const int x_count = scan_w - sp->clip.right - sp->clip.left;
BYTE *sip_fin = sip + (x_count & ~31);
for ( ; sip < sip_fin; sip += 32) {
x0 = _mm_loadu_si128((__m128i*)(sip + 0));
x1 = _mm_loadu_si128((__m128i*)(sip + 16));
x0 = _mm_andnot_si128(x0, xMotion);
x1 = _mm_andnot_si128(x1, xMotion);
x0 = _mm_cmpeq_epi8(x0, xMotion);
x1 = _mm_cmpeq_epi8(x1, xMotion);
DWORD count0 = _mm_movemask_epi8(x0);
DWORD count1 = _mm_movemask_epi8(x1);
motion_count[is_latter_feild] += popcnt32(((count1 << 16) | count0));
}
if (x_count & 16) {
x0 = _mm_loadu_si128((__m128i*)sip);
x0 = _mm_andnot_si128(x0, xMotion);
x0 = _mm_cmpeq_epi8(x0, xMotion);
DWORD count0 = _mm_movemask_epi8(x0);
motion_count[is_latter_feild] += popcnt32(count0);
sip += 16;
}
sip_fin = sip + (x_count & 15);
for ( ; sip < sip_fin; sip++) {
motion_count[is_latter_feild] += ((~*sip & 0x40) >> 6);
}
}


いまとなっては、別にわざわざmovemask + popcntをしなくても途中まではSIMDレジスタで和を取っていってもいい気がするが…実はr16あたりでscan_frameに吸収されたので使われていないコードだったりする。



r5 → r8
メモリアクセス最適化
やはりメモリアクセスの最適化は重要で、かなり高速化した。
・scan_frameで分かれていた関数を統合してメモリアクセスを低減。
・scan_frameなどのフレームを縦にスキャンする部分で、メモリに対してとびとびにアクセスしたのを、なるべく連続アクセスができるように変更。

scan_frameでフレームを縦にスキャンするというのは、まあ図にするとこんな感じで

afs_scan_chunk_orig

上下のラインについて計算を行い、加えて一時変数がさらに下のラインの計算結果に影響するので、縦方向に依存関係が発生する。つまり最内ループが縦方向になっている。縦方向にデータを読むと、メモリ上では飛び飛びの位置を読むことになり、メモリアクセスがかなり遅くなる。オリジナル版では上の図のようにMMXレジスタ(64bit幅)を使って、なるべく一度に読み込もうとしていた。

r5では、SSEレジスタ(128bit幅)を使用したことで、一度に処理する量が広がった。

afs_scan_chunk_r5

とはいえ、まだわずか128bit( = 16byte)なので、まだまだ縦に飛び飛びのアクセスという感じ。

そこで、r8では、縦方向のループのさらに内側に、横方向にループを追加し、横方向に一度に処理する量を飛躍的に増やした(最大1536byte x4)。

afs_scan_chunk_r8

一時変数はレジスタには収まらないので、メモリに読み書きすることになるけど、1.5KB x4 = 6KBなので、L1-Dキャッシュ(32KB)に余裕を持って収まる量、になっている。

こうした最適化はループ構造が無駄に複雑になって、ちゃんと動くコードにするのが大変なのだけど、それなりに成果は出る。



r8 → r10
AVX2対応
・HaswellのAVX2に対応した。256bit整数演算がついに可能になり、さぞかし速くなるだろう…と思いきや、shuffle命令のスループットが半減したり、そもそも一部のshuffle命令がなんじゃそりゃという仕様だったり(vpalignrとか、vpshufbとか、unpack系とか)、期待したよりは効果がなかった。まあメモリ帯域で律速してしまっていると演算速度はあまり関係ないというのもある。

スレッド分割法の改善によるロードアンバランス(負荷不均衡)の抑制
・この時点では、scan_frameだけがスレッド並列化されて高速化されていた。ただ、基本的に並列化では、仕事を各スレッドに分配して計算させて並列に計算させて速くするのだけど、すべてのスレッドが計算を終了しないと、次の計算に進むことができない。

afs_thread_load_imbalance

r10まではスレッド間の負荷のバランスがとれていなくて、仕事の割り当ての多い1つのスレッドが終わるまで次に進めず、全体の速度が下がってしまっていたので、分割方法を見直して負荷の不均衡を改善したことで、それなりに高速化に繋がった。r8 → r10での速度向上は、AVX2の効果よりも、こちらの効果のほうが大きい。



r10 → r12
・ソフトウェアプリフェッチ再投入により高速化。
r8で連続アクセスが多くなったのでいらないかなと思ってやめていたprefetch命令を再投入した。Intelのマニュアルとかにはハードウェアプリフェッチがよしなにやってくれるからソフトウェアプリフェッチはいらない的なことを書いてるけど、afsの場合にはそんなことはないみたい。まあ、ハードウェアプリフェッチは4Kページ境界を越えられないので、次の行のアクセスなどは明示的にプリフェッチしておくと速くなるということかも。あるいは、D-TLBキャッシュミスも隠ぺいできるのかもしれない。

・フレームを最後に再構成する際にメモリアライメントを考慮して書き出すことで高速化。
アライメントが取れていないメモリアクセスは遅いということなので、とにかくアライメントを調整すれば速くなる。アライメント大事。



r12 → r16
・scan_frameにcount_motionを統合。
メモリアクセスをさらに減らして、微々たるものだけど高速化。速くするためなら何でもするという方針のもと、0.1ms短縮するために、えらい労力をかけた例。おかげで、count_motion分の0.1msを消すことができた。

・scan_frame以外もスレッド並列を導入(サブスレッド)して高速化。
ただし、対象の処理がほとんどメモリ律速なためか、増やすとすぐ遅くなってしまうので、2スレッドまで。まあ5960Xとかの4chだと、もう少しスレッドを増やしたほうが速くなったりする



r16 → r19
一部の計算にMMX命令を使ったり(MMXレジスタが使えるので…)、こまごまとした最適化をした。まあ、こんなことではあまり速くならない…。



と、執念深くいろいろやってきた。まあ、忘れたのも多いけどこれ以外に試したことはたくさんあって、いろんなネタが没になった…。コードを書いては捨て、書いては捨て…。

もっと確実に速くなるネタをぱっと思いつけるようになりたいけど、まあ、わたしの知識不足というのがあるのだと思う。


ともかく、ここまでいろいろやってしまうと、もう限界かな、と思うようになってきた。とにかく自動フィールドシフトは参照するデータ量が多く、メモリ帯域との戦いをやっている感じなので、辛い…。

で、もうこうなったら「同じ計算をより高速に」は一度おいておいて、「処理をケチって高速に」という方向性を考え始めた。

(続く>>)


スポンサーサイト

コメントの投稿

非公開コメント

プロフィール

rigaya

Author:rigaya
アニメとか見たり、エンコードしたり。
連絡先(@を半角にしてください!)
rigaya34589@live.jp
github

最新記事
最新コメント
カテゴリ
月別アーカイブ
カウンター
検索フォーム
いろいろ
公開中のAviutlプラグインとかのダウンロード

○Aviutlプラグイン
x264guiEx 2.xx (ミラー)
- x264を使用したH264出力
- x264guiExの導入>
- x264.exeはこちら>

x265guiEx (ミラー)
- x265を使用したH.265/HEVC出力
- x265.exeはこちら>

QSVEnc + QSVEncC (ミラー)
- QuickSyncVideoによるH264出力
- QSVEncCはコマンドライン版
- QSVEncC 導入/使用方法>
- QSVEncCオプション一覧>

NVEnc + NVEncC (ミラー)
- NVIDIAのNVEncによるH264出力
- NVEncCオプション一覧>

VCEEnc + VCEEncC (ミラー)
- AMDのVCEによるH.264出力

ffmpegOut (ミラー)
- ffmpeg/avconvを使用した出力

自動フィールドシフト (ミラー)
- SSE2~AVX2による高速化版
- オリジナル: aji様

エッジレベル調整MT (ミラー)
- エッジレベル調整の並列化/高速化
- SSE2~AVX対応
- オリジナル: まじぽか太郎様

バンディング低減MT (ミラー)
- SSE2~AVX2による高速化版
- オリジナル: まじぽか太郎様

PMD_MT (ミラー)
- SSE2~FMA3による高速化版
- オリジナル: スレ48≫989氏

透過性ロゴ (ミラー)
- SSE2~FMA3によるSIMD版
- オリジナル: MakKi氏

AviutlColor (ミラー)
- BT.2020nc向け色変換プラグイン
- BT.709/BT.601向けも同梱

○その他
x264afs (ミラー)
- x264のafs対応版

aui_indexer (ミラー使い方>)
- lsmashinput.aui/m2v.auiの
 インデックス事前・一括生成

auc_export (ミラー使い方>)
- Aviutl Controlの
 エクスポートプラグイン版
 エクスポートをコマンドから

aup_reseter (ミラー)
- aupプロジェクトファイルの
 終了フラグを一括リセット

CheckBitrate (ミラー, 使い方, ソース)
- ビットレート分布の分析(HEVC対応)

チャプター変換 (ミラー使い方>)
- nero/appleチャプター形式変換

エッジレベル調整 (avisynth)
- Avisynth用エッジレベル調整

メモリ・キャッシュ速度測定
- スレッド数を変えて測定

○ビルドしたものとか
L-SMASH (ミラー)
x264 (ミラー)
x265 (ミラー)

○その他
サンプル動画
その他

○読みもの (ミラー)
Aviutl/x264guiExの色変換
動画関連ダウンロードリンク集
簡易インストーラの概要

○更新停止・公開終了
改造版x264gui
x264guiEx 0.xx
RSSリンクの表示
リンク
QRコード
QR