web-dev-qa-db-ja.com

なぜstd :: rotateはそんなに速いのですか?

なぜstd::rotateは、cplusplus.comが説明する同等の関数よりもはるかに高速なのですか?

cplusplus.comの実装:

template <class ForwardIterator>
  void rotate (ForwardIterator first, ForwardIterator middle, ForwardIterator last)
{
  ForwardIterator next= middle;

  while (first != next)
  {
    swap (*first++, *next++);

    if(next == last)
        next= middle;
    else if (first==middle)
        middle= next;
  }
}

私は2つの挿入ソートアルゴリズムを使用していますが、1つはstd::rotateを使用し、もう1つはcplusplus.comの同等の関数を使用しています。 1000個のint要素を持つ1000個のベクトルをソートするように設定しています。 std::rotateを使用するソートは0.376秒かかり、もう1つは8.181秒かかります。

どうしてこれなの? STL関数よりも良いものを作ろうとするつもりはありませんが、それでも興味があります。

27
bradshire

編集:

コンテキストが指定されていないため、コードがstd::swap()または別のswap(a,b)アルゴリズムを呼び出すかどうかは明確ではありません

_T tmp = a; a = b; b = tmp;
_

abがそれぞれ1000 intsのベクトルの場合、すべてのベクトル要素が3回コピーされます。 _std::vector<T>_のようなコンテナー向けのstd::swap()の特別なバージョンは、代わりにコンテナーのa.swap(b)メソッドを呼び出し、コンテナーの動的データポインターのみを交換します。

また、異なるイテレータタイプの場合、std::rotate()実装はいくつかの最適化を利用できます(以下の古い、おそらく誤解を招く答えを参照してください)。


警告:std::rotate()の実装は実装依存です。異なるイテレータカテゴリでは、異なるアルゴリズムを使用できます(たとえば、GNU g ++の___rotate(_ヘッダーで_bits/stl_algo.h_を探す)。

n要素をm=std::distance(first,middle)でシフトするには、1つの要素によるm回転のような単純な(素朴な)アルゴリズムがO(n * m)移動またはコピー操作。ただし、O(n)の移動のみが必要であり、各要素が直接正しい位置に配置されている場合、(おおよそ)m倍のアルゴリズム。

説明の例:文字列_s = "abcdefg"_を3つの要素で回転します。

_abcdefg : store 'a' in temporary place
dbcdefg : move s[3] to s[0] (where it belongs in the end, directly)
dbcgefg : move s[6] to s[3]
dbcgefc : move s[9%7] to s[6] (wrapping index modulo container size: 9%7 == 2)
dbfgefc : move s[5] to s[2]
dbfgebc : move s[1] to s[5] (another wrapping around)
defgebc : move s[4] to s[1]
defgabc : move 'a' from temporary place to s[4]
_

nmの最大公約数1で、これで完了です。それ以外の場合は、最初のm連続要素(ここでは_n/m_と見なされます)について、そのスキームを_n > m_時間繰り返す必要があります。この少し複雑なアルゴリズムははるかに高速です。

双方向イテレータの場合、別の伝説的なO(3n)アルゴリズムを使用でき、「フリッピングハンド」と呼ばれます。 Jon Bentley氏の本 Programming Pearls は、初期のUNIXエディターでテキストを移動するために使用されていました。

両手を上下に並べて親指を上に向けます。今

  1. 片手を回します。
  2. 他を回しなさい。
  3. お互いに接続された両方を回します。

コードで:

_reverse(first, middle);
reverse(middle, last);
reverse(first, last);
_

ランダムアクセスイテレータの場合、メモリの大きなチャンクはswap_ranges()(またはPODタイプのmemmove()オペレーションによって再配置できます) )。

アセンブラー操作を利用することによるマイクロ最適化は、少し余分な加速を与えることができます。これは、高速化されたアルゴリズムの上で実行できます。

メモリ内で「ホッピングアラウンド」するのではなく、連続する要素を使用するアルゴリズムでも、最新のコンピュータアーキテクチャではキャッシュミスの数が少なくなります。

20
René Richter

コメンターがすでに述べたように、それはあなたの標準ライブラリの実装に依存します。ただし、投稿したコードはフォワードイテレータでも有効です。そのため、要件はほとんどありません(これらのイテレータをインクリメントおよび逆参照できることのみ)。

ステパノフの古典的な プログラミングの要素 は、章全体(10)をrotateおよびその他の再配置アルゴリズムに充てています。順方向反復子の場合、コード内の一連のスワップはO(3N)割り当てを提供します。 双方向イテレータの場合、reverseを3回連続して呼び出すと、別のO(3N)アルゴリズムが生成されます。 ランダムアクセスイテレータの場合、std::rotateは、インデックスの順列を定義することにより、O(N)割り当てとして実装できます に関して開始イテレータfirstに。

上記のすべてのアルゴリズムはインプレースです。メモリバッファーを使用すると、ランダムアクセスバージョンは、連続するメモリのブロック全体で、memcpy()またはmemmove()(基になる値のタイプがPODの場合)のキャッシュ局所性の恩恵を受けることができます。交換することができます。挿入ソートが配列またはstd::vectorで行われる場合、標準ライブラリがこの最適化を利用する可能性があります。

TL; DR:標準ライブラリを信頼し、ホイールを再発明しないでください!

28
TemplateRex