28

GCC (または任意のコンパイラ) に実行時サイズ チェックをmemcpy()ループの外側 (そのサイズはコンパイル時定数ではなく、そのループ内で定数) で除外させ、関連するサイズ範囲ごとにループを特殊化させる信頼できる方法はありますか?その中のサイズを繰り返しチェックするのではなく?

これは、大規模なデータ セットの効率的なインメモリ分析用に設計されたオープン ソース ライブラリについて、ここで報告されているパフォーマンスの低下から縮小されたテスト ケースです。(リグレッションは、たまたま私のコミットの 1 つが原因です...)

元のコードは Cython にありますが、次のように純粋な C プロキシに縮小しました。

void take(double * out, double * in,
          int stride_out_0, int stride_out_1,
          int stride_in_0, int stride_in_1,
          int * indexer, int n, int k)
{
    int i, idx, j, k_local;
    k_local = k; /* prevent aliasing */
    for(i = 0; i < n; ++i) {
        idx = indexer[i];
        for(j = 0; j < k_local; ++j)
            out[i * stride_out_0 + j * stride_out_1] =
            in[idx * stride_in_0 + j * stride_in_1];
    }
}

ストライドは可変です。一般に、配列が連続しているとは限りません (より大きな配列の非連続スライスである可能性があるため)。ただし、c 連続配列の特定のケースでは、上記を次のように最適化しました。

void take(double * out, double * in,
          int stride_out_0, int stride_out_1,
          int stride_in_0, int stride_in_1,
          int * indexer, int n, int k)
{
    int i, idx, k_local;
    assert(stride_out_0 == k);
    assert(stride_out_0 == stride_in_0);
    assert(stride_out_1 == 1);
    assert(stride_out_1 == stride_in_1);
    k_local = k; /* prevent aliasing */
    for(i = 0; i < n; ++i) {
        idx = indexer[i];
        memcpy(&out[i * k_local], &in[idx * k_local],
               k_local * sizeof(double));
    }
}

(アサートは元のコードには存在しません。代わりに、連続性をチェックし、可能な場合は最適化されたバージョンを呼び出し、そうでない場合は最適化されていないバージョンを呼び出します。)

このバージョンは、ほとんどの場合、非常にうまく最適化されます。これは、通常のユース ケースが小さい場合nと大きい場合であるためkです。ただし、逆のユース ケースも発生し (大小n) k、特定のケース(仮想ワークフローの重要な部分を代表するものとして除外できない) であることが判明し、バージョンは 3.6 倍になります。オリジナルより遅い。これは明らかに、この次のバージョンが元のバージョンと同様に (最適化設定に応じてほぼまたは正確に) 実行されるという事実によって証明されるように、コンパイル時に定数ではないという事実によるものです (または、より良い場合もあります)。の特定の場合:n == 10000k == 4memcpy()kk == 4

    if (k_local == 4) {
        /* this optimizes */
        for(i = 0; i < n; ++i) {
            idx = indexer[i];
            memcpy(&out[i * k_local], &in[idx * k_local],
                   k_local * sizeof(double));
        }
    } else {
        for(i = 0; i < n; ++i) {
            idx = indexer[i];
            memcpy(&out[i * k_local], &in[idx * k_local],
                   k_local * sizeof(double));
        }
    }

明らかに、 の特定の値ごとにループをハードコーディングするのは実用的ではないkため、代わりに次のことを試みました (機能する場合は後で一般化できる最初の試みとして)。

    if (k_local >= 0 && k_local <= 4) {
        /* this does not not optimize */
        for(i = 0; i < n; ++i) {
            idx = indexer[i];
            memcpy(&out[i * k_local], &in[idx * k_local],
                   k_local * sizeof(double));
        }
    } else {
        for(i = 0; i < n; ++i) {
            idx = indexer[i];
            memcpy(&out[i * k_local], &in[idx * k_local],
                   k_local * sizeof(double));
        }
    }

残念ながら、この最後のバージョンは元のmemcpy()バージョンよりも高速ではありません。これは、GCC の最適化機能に対する私の信頼をやや落胆させるものです。

ここで正しいことを行うのに役立つ追加の「ヒント」をGCCに(何らかの手段で)与える方法はありますか? (さらに良いことに、さまざまなコンパイラで確実に機能する「ヒント」はありますか? このライブラリは、さまざまなターゲット向けにコンパイルされています。)

引用された結果は、「-O2」フラグを使用した 32 ビット Ubuntu 上の GCC 4.6.3 のものですが、GCC 4.7.2 および「-O3」バージョンも同様の (ただし同一ではない) 結果でテストしました。テスト ハーネスをLiveWorkspaceに投稿しましたが、タイミングはコマンドを使用して自分のマシンからのものtime(1)です (LiveWorkspace のタイミングがどれほど信頼できるかはわかりません)。

編集:呼び出しの最小サイズに「マジック ナンバー」を設定することも検討しました。memcpy()テストを繰り返すことでそのような値を見つけることができましたが、異なるコンパイラ/プラットフォームで結果がどの程度一般化できるかはわかりません. ここで使用できる経験則はありますか?

さらなる編集:この場合、k_local変数は実際には役に立たないことに気づきました。エイリアシングが不可能であるためです。これは、可能な場合(kグローバルである場合)に実行したいくつかの実験から削減され、変更したことを忘れていました。その部分は無視してください。

EDIT TAG: Cythonの新しいバージョンでもC++を使用できることに気付いたので、C++から役立つものがある場合に備えてC++としてタグ付けします...

最終編集:(今のところ)特殊なアセンブリにドロップダウンする代わりmemcpy()に、次のことが私のローカルマシンに最適な経験的解決策のようです:

    int i, idx, j;
    double * subout, * subin;
    assert(stride_out_1 == 1);
    assert(stride_out_1 == stride_in_1);
    if (k < 32 /* i.e. 256 bytes: magic! */) {
        for(i = 0; i < n; ++i) {
            idx = indexer[i];
            subout = &out[i * stride_out_0];
            subin = &in[idx * stride_in_0];
            for(j = 0; j < k; ++j)
                subout[j] = subin[j];
        }
    } else {
        for(i = 0; i < n; ++i) {
            idx = indexer[i];
            subout = &out[i * stride_out_0];
            subin = &in[idx * stride_in_0];
            memcpy(subout, subin, k * sizeof(double));
        }
    }

これは「マジックナンバー」を使用して呼び出すかどうかを決定しますmemcpy()が、連続していることがわかっている小さな配列のケースを最適化します (元の配列ではそのような仮定がないため、ほとんどの場合、元の配列よりも高速です)。

4

3 に答える 3

7

最終的に、当面の問題は、オプティマイザーに複数の変数に基づいて実行時の動作についての仮定を行うように依頼することの1つです。キー変数で「const」および「register」宣言を使用してオプティマイザーにコンパイル時のヒントを提供することは可能ですが、最終的には、オプティマイザーに依存して多くの仮定を行います。さらに、memcpy()は本質的である可能性がありますが、本質的であるとは保証されておらず、そうである場合でも、実装はかなり大きく異なる可能性があります。

目標が最大のパフォーマンスを達成することである場合、それを理解するためにテクノロジーに頼る必要はなく、直接それを行う必要がある場合があります。この状況に対する最善のアドバイスは、インラインアセンブラを使用して問題に対処することです。そうすることで、コンパイラとオプティマイザのヒューリスティックを補完する「ブラックボックス」ソリューションの落とし穴をすべて回避し、意図を明確に述べることができます。インラインアセンブラを使用する主な利点は、メモリコピーの問題の解決策でプッシュ/ポップや無関係な「一般化」コードを回避できることと、問題を解決するプロセッサの機能を直接利用できることです。欠点はメンテナンスですが、市場の大部分をカバーするために実際にIntelとAMDに対応するだけでよいことを考えると、克服できないわけではありません。

また、このソリューションを使用すると、複数のコア/スレッドやGPUを利用できる場合は、並行してコピーを実行し、パフォーマンスを大幅に向上させることができます。レイテンシーは高くなる可能性がありますが、スループットもはるかに高くなる可能性があります。たとえば、GPUが存在する場合にそれを利用できる場合は、コピーごとに1つのカーネルを起動し、1回の操作で数千の要素をコピーできます。

これに代わる方法は、コンパイラー/オプティマイザーに依存して最適な推測を行い、「const」および「register」宣言を使用して、コンパイラーのヒントを提供し、マジックナンバーを使用して「最良のソリューション」に基づいて分岐することです。パス...ただし、これはコンパイラ/システムに非常に依存し、マイレージはプラットフォーム/環境ごとに大きく異なります。

于 2013-03-22T00:45:24.473 に答える
2

最良の方法は、実験して最適な「k」値を見つけ、元のアルゴリズム(ループあり)とmemcpyを使用した最適化されたアルゴリズムを切り替えることだと思います。最適な「k」はCPUによって異なりますが、大幅に異なることはありません。基本的には、memcpyを呼び出すオーバーヘッド、最適なアルゴリズム(サイズ、配置などに基づく)を選択する際のmemcpy自体のオーバーヘッドと、ループを使用する「ナイーブ」アルゴリズムのオーバーヘッドです。

memcpyはgccに固有のものですが、魔法ではありません。基本的には、サイズ引数がコンパイル時に既知であり、小さい場合(しきい値が何であるかはわかりません)、GCCはmemcpy関数の呼び出しをインラインコードに置き換えます。コンパイル時にsize引数がわからない場合は、ライブラリ関数memcpyが常に呼び出されます。

于 2013-03-22T13:49:07.210 に答える
2

SSE/AVX とアライメント

たとえば、最新の Intel プロセッサを使用している場合は、SSE または AVX 命令を使用できます。特にGCCについてではありませんが、これを参照してください。興味があり、キャッシュをフラッシュする場合は、IntelがWindowsだけでなくLinux用のコンパイラスイートのバージョンを作成していると思います。それには独自のライブラリスイートが付属していると思います。

この投稿もあります。

スレッド (eek)

私はかなり最近、まさにこの種の問題を抱えていました。 memcpy() に時間がかかりすぎました。私の例では、あなたがやっているような小さなものがたくさんあるのではなく、1 つの大きな memcpy() (1MByte 程度) でした。

スレッドが永続的で、独自の pmemcpy() 関数を呼び出すことでジョブのシェアを「タスク化」する独自のマルチスレッド memcpy() を作成することで、非常に良いマイレージを獲得しました。永続的なスレッドは、オーバーヘッドがかなり低いことを意味しました。4 コアで x4 の改善が得られました。

したがって、ループを妥当な数のスレッドに分割することができ (私は利用可能なコアごとに 1 つにしました)、マシンに予備のコアをいくつか用意する余裕があれば、同様の利点が得られるかもしれません。

リアルタイム クラウドの機能 - DMA

余談ですが、かなり風変わりな OpenVPX ハードウェアをいじってみる楽しみがあります。基本的には、ボード間に高速シリアル RapidIO 相互接続を備えた大きなボックス内のボードの束です。各ボードには、sRIO を介して別のボードのメモリにデータを転送する DMA エンジンがあります。

私が行ったベンダーは、CPU を最大限に活用する方法について非常に巧妙です。賢い点は、DMA エンジンが非常にスマートであることです。DMA エンジンは、オンザフライでの行列変換、ストリップ マイニング、やろうとしているようなことなどを実行するようにプログラムできます。また、それはハードウェアの別の部分であるため、CPUその間は拘束されていないので、他のことをするのに忙しいかもしれません。

たとえば、合成開口レーダー処理などを行っている場合、常に大きな行列変換を行うことになります。すばらしい点は、変換自体に CPU 時間がまったくかからないことです。データを別のボードに移動するだけで、既に変換された状態で到着します。

とにかく、そのような利点があると、Intel CPU (およびその他) に、メモリ周辺機器だけでなく、メモリメモリを操作できるオンボード DMA エンジンが搭載されていることを本当に望みます。そうすれば、あなたのようなタスクが本当に速くなります。

于 2013-03-21T22:06:29.463 に答える