SIMD 対応による描画のパフォーマンスの改善

この記事は Qt Blog の "Improving the rendering performance with more SIMD" を翻訳したものです。
執筆: Benjamjn 2010年8月24日

最近の2つのバージョンの Qt (訳注:この記事は Qt 4.7 がリリースされる前に書かれたものです)では、パフォーマンスの改善を行ってきました。Qt 4.5 で プラグイン形式のグラフィックスシステム を導入し、数多くの描画の最適化も行いました。Qt 4.6 では全ての範囲を対象に最適化を行いました。組み込み機器でのパフォーマンスはパッチリリース毎に順調に改善されています。

常にスピードを向上させてきた事により、さらに改善するための手法は不足気味です。とはいえ、Qt 4.7 をさらに高速なものにするために、私たちは全く別の改善方法を探さなければなりません。

Single Instruction, Multiple Data

Qt 4.7 を高速化する上で用いた1つの方法はプロセッサをより効率よく利用する方法です。最近のプロセッサは複数のデータを一つの命令で処理する手段を持っています。これは SIMD と呼ばれています。最近の x86 プロセッサの SSE 拡張、ARM Cortex の場合は Neon がこれに当たります。

この原理は簡単です。複数のデータに対して1つの命令で処理をした実際の例を見てみましょう。

quint32 a[256];
quint32 b[256];
quint32 c[256];
// [...]

for (int i = 0; i < 256; ++i) {
c[i] = a[i] + b[i];
}

SIMD をサポートするプロセッサではこのコードは複数のデータへの命令を使用することで改善できます。例えば、SSE2 では以下のコードで 4 つのデータを一度にロードし、加算処理を行い、c に値を保存します。

quint32 a[256];
quint32 b[256];
quint32 c[256];
// [...]

for (int i = 0; i < 256; i += 4) {
__m128i vectorA = _mm_loadu_si128((__m128i*)&a[i]);
__m128i vectorB = _mm_loadu_si128((__m128i*)&b[i]);
__m128i vectorC = _mm_add_epi32(vectorA, vectorB);
_mm_storeu_si128((__m128i*)&c[i], vectorC);
}

このコードにはコンパイラが SSE2 命令に置換するイントリンシック命令が含まれます。

このサンプルはとてもシンプルなので、正しいオプションを指定した場合にはコンパイラが自動で最適化を行うことも可能です。しかし、実際のケースではこれほど簡単ではないため、アルゴリズムをベクタで処理可能なように少し変更する必要があります。

Qt では MMX や 3DNow! など SIMD を長い間使用してきました。Qt 4.7 では x86 での SSE と ARM Cortex での Neon への対応をさらに進めました。SIMD をより多くの場所で使用することにより、あるケースでは2倍から4倍の速度が得られています。

ラスタの改善

Qt 4.7 では多くの描画プリミティブが SSE と Neon を使用するコードで再実装されてきました。これにより、ラスタグラフィックスシステムは非常に良い方向に向かっています。

SIMD で書き直された関数は一般的に2倍から4倍高速になります。狭い範囲でのベンチマークでは誤解を招く可能性があるため WebKit benchmark suite を使用した、現実的なユースケースでの効果を見てみましょう。

"scrolling" のテストでは、最もビジターの多い 50 のウェブページをロードし、上下にスクロールします。このテストの結果が以下のグラフになります。Qt 4.6 (SIMD 無し)の結果を 1 としたときの各バージョンでのスクロール速度が縦軸となり、値が大きいほど速い結果が得られています。

Performance improvement of Qt 4.7

このテストは WebKit のエンジン自体に対して行われてきた改善の影響を受けないように、同じバージョンの QtWebKit(trunk版) で動作させています。

SIMD とコンパイル

これらの Qt の最適化の恩恵を受けるために特別にしなければならないことはありません。Qt のビルド時に configure スクリプトがコンパイラでサポートされている機能を検知します。どの拡張機能がサポートされているかはコマンドラインに出力される結果で確認することができます。

CPU 拡張をコンパイル時に有効にすることと、これが使用されることはまた別です。アプリケーションの起動時に Qt は使用可能な拡張機能を検査し、実行時のプロセッサに応じた最適な関数を設定します。

より SSE を活用するために、アライメントを揃えることを意識したコードも増えてきています。残念ながらいくつかのコンパイラにはベクタのアライメントに関するバグがあります。クラッシュを防ぎ、ベストなパフォーマンスを得るためには、最新のコンパイラを使用するというのも良い案です。

そして

この改善はまだ終わりではありません。使用頻度の高い関数は最適化されてきましたが、改善の余地はまだまだあります。先月私は毎週のように「これでほぼ完璧だ」と思っていましたが、Andreas が面白いケースのつっこみをしてきました。これらの改善は 4.7 のブランチに対して行われ、4.7.1 に含まれる予定です。これで 4.7.0 より少し早くなるでしょう。


Blog Topics:

Comments