19

1行のコードがあり、アプリケーションの実行時間の25%〜30%を消費します。これは、std :: setのコンパレータよりも小さいです(セットは赤黒木で実装されます)。28秒以内に約1億8000万回呼び出されます。

struct Entry {
  const float _cost;
  const long _id;

  // some other vars

    Entry(float cost, float id) : _cost(cost), _id(id) {
    } 
};



template<class T>
struct lt_entry: public binary_function <T, T, bool>
{
    bool operator()(const T &l, const T &r) const
    {
        // Most readable shape
        if(l._cost != r._cost) {
            return r._cost < l._cost;
        } else {
            return l._id < r._id;
        }
    }
};

エントリはコストで並べ替える必要があり、コストがIDで同じかどうかを確認します。最小値の抽出ごとに多くの挿入があります。フィボナッチヒープの使用を考えましたが、理論的には優れていると言われていますが、定数が高く、実装がかなり複雑です。また、挿入はO(log(n))にあるため、nが大きい場合、実行時の増加はほぼ一定です。だから、セットにこだわっても大丈夫だと思います。

パフォーマンスを向上させるために、さまざまな形で表現しようとしました。

return l._cost < r._cost || r._cost > l._cost || l._id < r._id;

return l._cost < r._cost || (l._cost == r._cost && l._id < r._id);

これでも:

typedef union {
    float _f;
    int _i;
} flint;

//...

flint diff;
diff._f = (l._cost - r._cost);
return (diff._i && diff._i >> 31) || l._id < r._id;

しかし、私はランタイムを改善することができなかったので、コンパイラーはすでに十分に賢いようです。

私もSSEについて考えましたが、この問題は実際にはSSEにはあまり当てはまりません...

アセンブリは次のようになります。

movss  (%rbx),%xmm1
mov    $0x1,%r8d
movss  0x20(%rdx),%xmm0
ucomiss %xmm1,%xmm0
ja     0x410600 <_ZNSt8_Rb_tree[..]+96>
ucomiss %xmm0,%xmm1
jp     0x4105fd <_ZNSt8_Rb_[..]_+93>
jne    0x4105fd <_ZNSt8_Rb_[..]_+93>
mov    0x28(%rdx),%rax
cmp    %rax,0x8(%rbx)
jb     0x410600 <_ZNSt8_Rb_[..]_+96>
xor    %r8d,%r8d

私はアセンブリ言語についてほんの少しの経験がありますが、それほど多くはありません。

パフォーマンスを絞り出すのが(唯一?)最良のポイントだと思いましたが、本当に努力する価値はありますか?いくつかのサイクルを節約できるショートカットを見つけることができますか?

コードが実行されるプラットフォームは、メニーコアIntelマシン上のgcc 4.6(-stl = c ++ 0x)を備えたubuntu12です。使用可能なライブラリは、boost、openmp、およびtbbのみです。30秒のベンチマークは、4年前のラップトップ(コア2デュオ)で実行されました。

私は本当にこれに固執しています、それはとても単純に見えますが、それだけの時間がかかります。どうすればこのラインを改善できるかを考えて何日も頭を悩ませてきました...

この部分を改善する方法を教えていただけますか、それともすでに最高の状態ですか?

編集1:ジェリーズの提案を使用した後、私は約4.5秒のスピードアップを達成しました。編集2:ブーストフィボナッチヒープを試した後、比較は未満関数への174Mio呼び出しに行きました。

4

5 に答える 5

11

私はそれを信じるのに苦労しています:

a) 比較関数は 30 秒間に 1 億 8000 万回実行されます

b) 比較関数は CPU 時間の 25% を使用します

両方とも真です。Core 2 Duo でさえ、1 億 8000 万回の比較を 1 秒未満で簡単に実行できるはずです (結局のところ、それが実際に意味があるとすれば、12,000 MIPS のような処理ができると主張されています)。したがって、プロファイリング ソフトウェアによる比較では、別のことがひとまとめにされていると考える傾向があります。(たとえば、新しい要素にメモリを割り当てます。)

ただし、少なくとも std::set が探しているデータ構造ではない可能性を考慮する必要があります。ソートされた値 (または最大値でさえ) が実際に必要になる前に何百万もの挿入を行う場合は、時間と空間の両方ではるかに安価なデータ構造であるベクトルに値を入れて、ソートする方がよい場合があります。オンデマンドで。

衝突が心配なために実際にセットが必要な場合は、代わりに unordered_set を検討してください。これは少し安価ですが、ベクトルほど安くはありません。(正確には、ベクトルは一意性を保証できないためです。)しかし、正直なところ、その構造定義を見ると、一意性があなたにとって重要であるとは信じがたいです。

"基準"

私の小さな Core i5 ラップトップでは、OP のマシンと同じレベルではないと思われますが、1,000 万のランダムな一意のエントリ (2 つの比較フィールドのみ) を std::set と std に挿入するいくつかのテストを実行しました。 :ベクター。この最後に、ベクトルをソートします。

これを 2 回行いました。1 つはおそらく一意のコストを生成するランダム ジェネレーターを使用し、もう 1 つは正確に 2 つの異なるコストを生成するジェネレーターを使用します (比較が遅くなるはずです)。1,000 万回の挿入により、OP によって報告されたよりもわずかに多くの比較が行われます。

              unique cost         discrete cost
           compares     time    compares     time
set       243002508    14.7s   241042920    15.6s   
vector    301036818     2.0s   302225452     2.3s

比較時間をさらに分離するために、std::sort と std::partial_sort の両方を使用してベクトル ベンチマークをやり直しました。 100万)。より大きな partial_sort の結果は私を驚かせました -- ベクトルの 10% をソートするのは、すべてをソートするよりも遅いと思っていたでしょうが、アルゴリズムのコストが比較のコストよりもはるかに重要であることを示しています。

                     unique cost         discrete cost
                  compares     time    compares     time
partial sort 10   10000598     0.6s    10000619     1.1s
partial sort 1M   77517081     2.3s    77567396     2.7s
full sort        301036818     2.0s   302225452     2.3s   

結論: 長い比較時間は目に見えますが、コンテナー操作が支配的です。1,000 万セットの挿入の総コストは、合計 52 秒の計算時間で確かに目に見えます。1,000 万のベクトル挿入の総コストは、それほど目立ちません。

それが価値があるものについての小さなメモ

アセンブリ コードの一部から得た 1 つのことは、コストをfloat. 実際には float に 8 バイトを割り当てているため、メモリを節約することはなく、CPU は単一の float 比較を単一の double 比較よりも高速に実行しません。ただ言っているだけです(つまり、時期尚早の最適化に注意してください)。

反対票を投じてください、説明しますか?

于 2012-12-12T04:08:49.803 に答える
11

簡単な解決策は、最も重要なコストと残りの id で構成されるソート識別子を事前計算することです。

例えば、

struct Entry
{
    double cost_;
    long id_;
    long long sortingId_;

  // some other vars

    Entry( double cost, float id )
        : cost_( cost ), id_( id ), sortingId_( 1e9*100*cost + id )
    {} 
};

sortingId_値の範囲について想定できることに基づいて値を調整します。

次に、並べ替えますsortingId_


または、同じ考えのバリエーションとして、データについて適切な仮定を立てることができない場合は、特にmemcmp.


より高いレベルのソリューションについては、ヒント引数std::set::insertを持つオーバーロードがあることに注意してください。データがすでにほぼソートされている場合、コンパレータ関数の呼び出し回数が大幅に減少する可能性があります。


そして、astd::unordered_setで十分かどうかを検討できますか?つまり、データをソート順にリストする必要があるかどうかです。std::setまたは、並べ替えが要素挿入の内部的なものにすぎない場合。


最後に、他の読者のために (OP は彼がこれを認識していることを明らかにしました)、MEASUREを覚えておいてください。

于 2012-12-12T04:10:00.340 に答える
10

ここで概説することは壊れやすく、完全に移植可能ではないという事実から始めましょう。 .

それが依存する点の 1 つは、IEEE 浮動小数点数が慎重に設計されているため、ビット パターンを整数として扱う場合でも、正しい順序に並べ替えられるという事実です (NaN などのいくつかのものを法として、実際には「正しい順序」はありません)。

それを利用するために、エントリをパックして、キーを構成する 2 つの部分の間にパディングがないようにします。次に、構造全体が 8 バイト境界に整列されていることを確認します。_idまた、64 ビットのシステム/コンパイラでも 32 ビットのままになるように toを変更しましint32_tた (ほぼ確実に、この比較に最適なコードが生成されます)。

次に、構造体のアドレスをキャストして、浮動小数点数と整数を 1 つの 64 ビット整数として表示できるようにします。リトルエンディアン プロセッサを使用しているため、それをサポートするには、重要でない部分 ( id) を最初に配置し、重要な部分 ( cost) を 2 番目に配置する必要があります。浮動小数点部分が最上位ビットになり、整数部分が下位ビットになります。

struct __attribute__ ((__packed__)) __attribute__((aligned(8)) Entry {
  // Do *not* reorder the following two fields or comparison will break.
  const int32_t _id;
  const float _cost;

  // some other vars

    Entry(long id, float cost) : _cost(cost), _id(id) {} 
};

次に、醜い小さな比較関数があります。

bool operator<(Entry const &a, Entry const &b) { 
   return *(int64_t const *)&a < *(int64_t const *)&b;
}

構造体を正しく定義すると、比較は非常に簡単になります。各構造体の最初の 64 ビットを取得し、それらを 64 ビット整数であるかのように比較するだけです。

最後に、いくつかの値に対して正しく動作することを少なくとも少しは保証するためのテスト コードを少し示します。

int main() { 
    Entry a(1236, 1.234f), b(1234, 1.235f), c(1235, 1.235f);

    std::cout << std::boolalpha;

    std::cout << (b<a) << "\n";
    std::cout << (a<b) << "\n";
    std::cout << (b<c) << "\n";
    std::cout << (c<b) << "\n";
    return 0;
}

少なくとも私にとっては、期待される結果が得られます。

false
true
true
false

考えられる問題のいくつか: 2 つのアイテムがそれらの間で再配置されたり、構造体の他の部分がそれらの前または間に配置されたりすると、比較は確実に壊れます。第 2 に、アイテムのサイズはそれぞれ 32 ビットのままであることに完全に依存しているため、それらを連結すると 64 ビットになります。第 3 に、誰か__packed__が構造体定義から属性を削除すると、_idと の間のパディングが発生する可能性があります。_cost、再び比較を壊します。同様に、誰かがaligned(8)を削除すると、8バイト境界に整列されていない8バイトの量をロードしようとするため、コードの速度が低下する可能性があります(別のプロセッサでは、これは完全に失敗する可能性があります)。[編集: おっと。@rici は、ここにリストするつもりだったことを思い出しましたが、忘れていました: これは、_idとの両方costが正の場合にのみ正しく機能します。が負の場合_cost、IEEE 浮動小数点が符号付き絶対値表現を使用しているため、比較が混乱します。an_idが負の場合、その符号ビットは数値の中間にある通常のビットと同じように扱われるため、負_idは正よりも大きく表示され_idます。]

要約すると、これは壊れやすいです。それについてはまったく疑問の余地はありません。それでも、かなり高速になるはずです。特に 64 ビット コンパイラを使用している場合は、比較が 2 回のロードと 1 回の比較になると予想されます。簡単に言うと、おそらく比較自体をこれ以上高速化することはできないという点に達しています。できることは、並列処理を増やしたり、メモリ使用パターンを最適化したりすることだけです。

于 2012-12-12T04:56:02.170 に答える
1

最小値の抽出ごとに多くの挿入があります。フィボナッチヒープの使用を考えましたが、理論的には優れていると言われましたが、定数が高く、実装がかなり複雑であると言われました。また、挿入は O(log(n)) にあるため、実行時の増加は n が大きくてもほぼ一定です。だから、セットに固執しても大丈夫だと思います。

これは典型的なプライオリティ キュー アプリケーションのように思えます。フィボナッチヒープの使用を検討したばかりだと言うので、そのような優先キューの実装で十分であると思います(要素をプッシュし、最小要素を1つずつ抽出します)。その比較関数から 1 つか 2 つのクロック サイクルを最適化することに執着する前に、市販のプライオリティ キューの実装をいくつか試してみることをお勧めします。std::priority_queueboost::d_ary_heap(またはboost::d_ary_heap_indirect変更可能な優先度キューの場合)、またはその他のブーストヒープ構造のように。

私は以前に同様の状況に遭遇しました。私はstd::setA* のようなアルゴリズムで優先度キューの代わりに a を使用していました (また、挿入のために sortedstd::vectorを試しました)、に切り替えるとパフォーマンスが大幅に向上し、後で切り替える余分なマイルを行きました。まだ試していない場合は、少なくとも試してみることをお勧めします。std::inplace_mergestd::priority_queueboost::d_ary_heap_indirect

于 2012-12-12T07:00:38.180 に答える
0

私はそれ自体に答えを持っていません-いくつかのアイデアです:

  1. GCC を使用している場合は、並列モードを有効にしていくつかのベンチマークを実行します。
  2. コスト コンポーネントの非正規化数を扱っていないことは確かですか?
于 2012-12-12T03:59:58.277 に答える