なぜ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関数よりも良いものを作ろうとするつもりはありませんが、それでも興味があります。
編集:
コンテキストが指定されていないため、コードがstd::swap()
または別のswap(a,b)
アルゴリズムを呼び出すかどうかは明確ではありません
_T tmp = a; a = b; b = tmp;
_
a
とb
がそれぞれ1000 int
sのベクトルの場合、すべてのベクトル要素が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]
_
n
とm
の最大公約数1で、これで完了です。それ以外の場合は、最初のm
連続要素(ここでは_n/m
_と見なされます)について、そのスキームを_n > m
_時間繰り返す必要があります。この少し複雑なアルゴリズムははるかに高速です。
双方向イテレータの場合、別の伝説的なO(3n)アルゴリズムを使用でき、「フリッピングハンド」と呼ばれます。 Jon Bentley氏の本 Programming Pearls は、初期のUNIXエディターでテキストを移動するために使用されていました。
両手を上下に並べて親指を上に向けます。今
コードで:
_reverse(first, middle);
reverse(middle, last);
reverse(first, last);
_
ランダムアクセスイテレータの場合、メモリの大きなチャンクはswap_ranges()
(またはPODタイプのmemmove()
オペレーションによって再配置できます) )。
アセンブラー操作を利用することによるマイクロ最適化は、少し余分な加速を与えることができます。これは、高速化されたアルゴリズムの上で実行できます。
メモリ内で「ホッピングアラウンド」するのではなく、連続する要素を使用するアルゴリズムでも、最新のコンピュータアーキテクチャではキャッシュミスの数が少なくなります。
コメンターがすでに述べたように、それはあなたの標準ライブラリの実装に依存します。ただし、投稿したコードはフォワードイテレータでも有効です。そのため、要件はほとんどありません(これらのイテレータをインクリメントおよび逆参照できることのみ)。
ステパノフの古典的な プログラミングの要素 は、章全体(10)をrotate
およびその他の再配置アルゴリズムに充てています。順方向反復子の場合、コード内の一連のスワップはO(3N)
割り当てを提供します。 双方向イテレータの場合、reverse
を3回連続して呼び出すと、別のO(3N)
アルゴリズムが生成されます。 ランダムアクセスイテレータの場合、std::rotate
は、インデックスの順列を定義することにより、O(N)
割り当てとして実装できます に関して開始イテレータfirst
に。
上記のすべてのアルゴリズムはインプレースです。メモリバッファーを使用すると、ランダムアクセスバージョンは、連続するメモリのブロック全体で、memcpy()
またはmemmove()
(基になる値のタイプがPODの場合)のキャッシュ局所性の恩恵を受けることができます。交換することができます。挿入ソートが配列またはstd::vector
で行われる場合、標準ライブラリがこの最適化を利用する可能性があります。
TL; DR:標準ライブラリを信頼し、ホイールを再発明しないでください!