私はこのブログを読んだだけです http://lemire.me/blog/archives/2012/06/20/do-not-waste-time-with-stl-vectors/operator[]
割り当てとPush_back
のメモリは事前予約済みstd::vector
で、私は自分で試すことにしました。操作は簡単です:
// for vector
bigarray.reserve(N);
// START TIME TRACK
for(int k = 0; k < N; ++k)
// for operator[]:
// bigarray[k] = k;
// for Push_back
bigarray.Push_back(k);
// END TIME TRACK
// do some dummy operations to prevent compiler optimize
long sum = accumulate(begin(bigarray), end(array),0 0);
そしてここに結果があります:
~/t/benchmark> icc 1.cpp -O3 -std=c++11
~/t/benchmark> ./a.out
[ 1.cpp: 52] 0.789123s --> C++ new
[ 1.cpp: 52] 0.774049s --> C++ new
[ 1.cpp: 66] 0.351176s --> vector
[ 1.cpp: 80] 1.801294s --> reserve + Push_back
[ 1.cpp: 94] 1.753786s --> reserve + emplace_back
[ 1.cpp: 107] 2.815756s --> no reserve + Push_back
~/t/benchmark> clang++ 1.cpp -std=c++11 -O3
~/t/benchmark> ./a.out
[ 1.cpp: 52] 0.592318s --> C++ new
[ 1.cpp: 52] 0.566979s --> C++ new
[ 1.cpp: 66] 0.270363s --> vector
[ 1.cpp: 80] 1.763784s --> reserve + Push_back
[ 1.cpp: 94] 1.761879s --> reserve + emplace_back
[ 1.cpp: 107] 2.815596s --> no reserve + Push_back
~/t/benchmark> g++ 1.cpp -O3 -std=c++11
~/t/benchmark> ./a.out
[ 1.cpp: 52] 0.617995s --> C++ new
[ 1.cpp: 52] 0.601746s --> C++ new
[ 1.cpp: 66] 0.270533s --> vector
[ 1.cpp: 80] 1.766538s --> reserve + Push_back
[ 1.cpp: 94] 1.998792s --> reserve + emplace_back
[ 1.cpp: 107] 2.815617s --> no reserve + Push_back
すべてのコンパイラーで、vector
とoperator[]
の組み合わせは、operator[]
との未加工ポインターよりもはるかに高速です。これが最初の質問につながりました:なぜですか? 2番目の質問は、私はすでにメモリを「予約」しているのですが、なぜopeator[]
の方が速いのですか?
次の質問は、メモリがすでに割り当てられているので、なぜPush_back
がoperator[]
よりも遅いのですか?
テストコードは以下に添付されています:
#include <iostream>
#include <iomanip>
#include <vector>
#include <numeric>
#include <chrono>
#include <string>
#include <cstring>
#define PROFILE(BLOCK, ROUTNAME) ProfilerRun([&](){do {BLOCK;} while(0);}, \
ROUTNAME, __FILE__, __LINE__);
template <typename T>
void ProfilerRun (T&& func, const std::string& routine_name = "unknown",
const char* file = "unknown", unsigned line = 0)
{
using std::chrono::duration_cast;
using std::chrono::microseconds;
using std::chrono::steady_clock;
using std::cerr;
using std::endl;
steady_clock::time_point t_begin = steady_clock::now();
// Call the function
func();
steady_clock::time_point t_end = steady_clock::now();
cerr << "[" << std::setw (20)
<< (std::strrchr (file, '/') ?
std::strrchr (file, '/') + 1 : file)
<< ":" << std::setw (5) << line << "] "
<< std::setw (10) << std::setprecision (6) << std::fixed
<< static_cast<float> (duration_cast<microseconds>
(t_end - t_begin).count()) / 1e6
<< "s --> " << routine_name << endl;
cerr.unsetf (std::ios_base::floatfield);
}
using namespace std;
const int N = (1 << 29);
int routine1()
{
int sum;
int* bigarray = new int[N];
PROFILE (
{
for (unsigned int k = 0; k < N; ++k)
bigarray[k] = k;
}, "C++ new");
sum = std::accumulate (bigarray, bigarray + N, 0);
delete [] bigarray;
return sum;
}
int routine2()
{
int sum;
vector<int> bigarray (N);
PROFILE (
{
for (unsigned int k = 0; k < N; ++k)
bigarray[k] = k;
}, "vector");
sum = std::accumulate (begin (bigarray), end (bigarray), 0);
return sum;
}
int routine3()
{
int sum;
vector<int> bigarray;
bigarray.reserve (N);
PROFILE (
{
for (unsigned int k = 0; k < N; ++k)
bigarray.Push_back (k);
}, "reserve + Push_back");
sum = std::accumulate (begin (bigarray), end (bigarray), 0);
return sum;
}
int routine4()
{
int sum;
vector<int> bigarray;
bigarray.reserve (N);
PROFILE (
{
for (unsigned int k = 0; k < N; ++k)
bigarray.emplace_back(k);
}, "reserve + emplace_back");
sum = std::accumulate (begin (bigarray), end (bigarray), 0);
return sum;
}
int routine5()
{
int sum;
vector<int> bigarray;
PROFILE (
{
for (unsigned int k = 0; k < N; ++k)
bigarray.Push_back (k);
}, "no reserve + Push_back");
sum = std::accumulate (begin (bigarray), end (bigarray), 0);
return sum;
}
int main()
{
long s0 = routine1();
long s1 = routine1();
long s2 = routine2();
long s3 = routine3();
long s4 = routine4();
long s5 = routine5();
return int (s1 + s2);
}
Push_back
は境界チェックを行います。 operator[]
ではない。したがって、スペースを予約した場合でも、Push_back
はoperator[]
ありません。さらに、size
値を増加させます(予約はcapacity
を設定するだけです)。そのため、毎回それを更新します。
要するに、 Push_back
は何をしているのかoperator[]
が実行中-これが遅い(そしてより正確な)理由です。
Yakkと私が発見したように、Push_back
の明らかな遅延の原因となっている別の興味深い要因があるかもしれません。
最初の興味深い観察は、元のテストでnew
を使用して生の配列を操作することは、vector<int> bigarray(N);
とoperator[]
を使用するよりもslowerであり、因数2よりも多いことです。 。さらに興味深いのは、生の配列バリアントにadditionalmemset
を挿入することにより、両方で同じパフォーマンスが得られることです。
int routine1_modified()
{
int sum;
int* bigarray = new int[N];
memset(bigarray, 0, sizeof(int)*N);
PROFILE (
{
for (unsigned int k = 0; k < N; ++k)
bigarray[k] = k;
}, "C++ new");
sum = std::accumulate (bigarray, bigarray + N, 0);
delete [] bigarray;
return sum;
}
もちろん、結論は、PROFILE
は予想とは異なる何かを測定するということです。 Yakkと私は、メモリ管理と関係があると思います。 YakkのOPへのコメントから:
resize
は、メモリのブロック全体にアクセスします。reserve
は触れずに割り当てます。アクセスするまで物理メモリページをフェッチまたは割り当てないレイジーアロケーターがある場合、空のベクターのreserve
はほとんど解放されます(ページの物理メモリを見つける必要すらありません!)ページに書き込みます(その時点で、それらを見つける必要があります)。
私は似たようなものを考えたので、「strided memset」で特定のページをタッチして、この仮説の簡単なテストを試みました(プロファイリングツールはより信頼性の高い結果を得る可能性があります)。
int routine1_modified2()
{
int sum;
int* bigarray = new int[N];
for(int k = 0; k < N; k += PAGESIZE*2/sizeof(int))
bigarray[k] = 0;
PROFILE (
{
for (unsigned int k = 0; k < N; ++k)
bigarray[k] = k;
}, "C++ new");
sum = std::accumulate (bigarray, bigarray + N, 0);
delete [] bigarray;
return sum;
}
ストライドをすべてのページの半分から4ページごとに変更して完全に省略することにより、vector<int> bigarray(N);
ケースからタイミングへの素敵な移行が得られますmemset
が使用されていないnew int[N]
のケース。
私の意見では、これはメモリ管理が測定結果の主な原因であることを強く示唆しています。
別の問題は、Push_back
での分岐です。これがPush_back
を使用する場合と比較してoperator[]
がmuch遅い主な理由であると多くの回答で主張されています。実際、memsetなしのrawポインターをreserve
+ Push_back
を使用する場合と比較すると、前者の方が2倍高速です。
同様に、UBを少し追加した場合(ただし、結果を後で確認します):
int routine3_modified()
{
int sum;
vector<int> bigarray;
bigarray.reserve (N);
memset(bigarray.data(), 0, sizeof(int)*N); // technically, it's UB
PROFILE (
{
for (unsigned int k = 0; k < N; ++k)
bigarray.Push_back (k);
}, "reserve + Push_back");
sum = std::accumulate (begin (bigarray), end (bigarray), 0);
return sum;
}
この変更されたバージョンは、new
+フルmemset
を使用する場合よりも約2倍遅くなります。したがって、Push_back
の呼び出しが何をするようにも見え、要素を設定するだけの場合と比較すると、2
の要素が(vector
とraw配列の両方でoperator[]
を介して)スローダウンします。
しかし、それはPush_back
で必要な分岐ですか、それとも追加の操作ですか?
// pseudo-code
void Push_back(T const& p)
{
if(size() == capacity())
{
resize( size() < 10 ? 10 : size()*2 );
}
(*this)[size()] = p; // actually using the allocator
++m_end;
}
確かにそれは簡単です。 libstdc ++の実装 。
vector<int> bigarray(N);
+ operator[]
バリアントを使用してテストし、Push_back
の動作を模倣する関数呼び出しを挿入しました。
unsigned x = 0;
void silly_branch(int k)
{
if(k == x)
{
x = x < 10 ? 10 : x*2;
}
}
int routine2_modified()
{
int sum;
vector<int> bigarray (N);
PROFILE (
{
for (unsigned int k = 0; k < N; ++k)
{
silly_branch(k);
bigarray[k] = k;
}
}, "vector");
sum = std::accumulate (begin (bigarray), end (bigarray), 0);
return sum;
}
x
を揮発性として宣言した場合でも、これは測定に1%の影響しか与えません。もちろん、ブランチが実際にopcodeであることを確認する必要がありましたが、私のアセンブラーの知識では、(-O3
で)それを確認できません。
ここで興味深い点は、silly_branch
にインクリメントを追加するとどうなるかです。
unsigned x = 0;
void silly_branch(int k)
{
if(k == x)
{
x = x < 10 ? 10 : x*2;
}
++x;
}
現在、変更されたroutine2_modified
は、元のroutine2
の2倍の速度で実行され、メモリページをコミットするUBを含む上記のroutine3_modified
と同等です。ループ内のすべての書き込みに別の書き込みが追加されるため、これは特に驚くべきことではないので、2倍の作業と2倍の期間があります。
結論
メモリ管理の仮説を検証するために、アセンブリツールとプロファイリングツールを注意深く確認する必要があり、追加の書き込みは適切な仮説です(「正しい」)。しかし、ヒントは、Push_back
を遅くする単なるブランチよりも複雑なことが起こっていると主張するのに十分強力だと思います。
これが完全なテストコードです:
#include <iostream>
#include <iomanip>
#include <vector>
#include <numeric>
#include <chrono>
#include <string>
#include <cstring>
#define PROFILE(BLOCK, ROUTNAME) ProfilerRun([&](){do {BLOCK;} while(0);}, \
ROUTNAME, __FILE__, __LINE__);
//#define PROFILE(BLOCK, ROUTNAME) BLOCK
template <typename T>
void ProfilerRun (T&& func, const std::string& routine_name = "unknown",
const char* file = "unknown", unsigned line = 0)
{
using std::chrono::duration_cast;
using std::chrono::microseconds;
using std::chrono::steady_clock;
using std::cerr;
using std::endl;
steady_clock::time_point t_begin = steady_clock::now();
// Call the function
func();
steady_clock::time_point t_end = steady_clock::now();
cerr << "[" << std::setw (20)
<< (std::strrchr (file, '/') ?
std::strrchr (file, '/') + 1 : file)
<< ":" << std::setw (5) << line << "] "
<< std::setw (10) << std::setprecision (6) << std::fixed
<< static_cast<float> (duration_cast<microseconds>
(t_end - t_begin).count()) / 1e6
<< "s --> " << routine_name << endl;
cerr.unsetf (std::ios_base::floatfield);
}
using namespace std;
constexpr int N = (1 << 28);
constexpr int PAGESIZE = 4096;
uint64_t __attribute__((noinline)) routine1()
{
uint64_t sum;
int* bigarray = new int[N];
PROFILE (
{
for (int k = 0, *p = bigarray; p != bigarray+N; ++p, ++k)
*p = k;
}, "new (routine1)");
sum = std::accumulate (bigarray, bigarray + N, 0ULL);
delete [] bigarray;
return sum;
}
uint64_t __attribute__((noinline)) routine2()
{
uint64_t sum;
int* bigarray = new int[N];
memset(bigarray, 0, sizeof(int)*N);
PROFILE (
{
for (int k = 0, *p = bigarray; p != bigarray+N; ++p, ++k)
*p = k;
}, "new + full memset (routine2)");
sum = std::accumulate (bigarray, bigarray + N, 0ULL);
delete [] bigarray;
return sum;
}
uint64_t __attribute__((noinline)) routine3()
{
uint64_t sum;
int* bigarray = new int[N];
for(int k = 0; k < N; k += PAGESIZE/2/sizeof(int))
bigarray[k] = 0;
PROFILE (
{
for (int k = 0, *p = bigarray; p != bigarray+N; ++p, ++k)
*p = k;
}, "new + strided memset (every page half) (routine3)");
sum = std::accumulate (bigarray, bigarray + N, 0ULL);
delete [] bigarray;
return sum;
}
uint64_t __attribute__((noinline)) routine4()
{
uint64_t sum;
int* bigarray = new int[N];
for(int k = 0; k < N; k += PAGESIZE/1/sizeof(int))
bigarray[k] = 0;
PROFILE (
{
for (int k = 0, *p = bigarray; p != bigarray+N; ++p, ++k)
*p = k;
}, "new + strided memset (every page) (routine4)");
sum = std::accumulate (bigarray, bigarray + N, 0ULL);
delete [] bigarray;
return sum;
}
uint64_t __attribute__((noinline)) routine5()
{
uint64_t sum;
int* bigarray = new int[N];
for(int k = 0; k < N; k += PAGESIZE*2/sizeof(int))
bigarray[k] = 0;
PROFILE (
{
for (int k = 0, *p = bigarray; p != bigarray+N; ++p, ++k)
*p = k;
}, "new + strided memset (every other page) (routine5)");
sum = std::accumulate (bigarray, bigarray + N, 0ULL);
delete [] bigarray;
return sum;
}
uint64_t __attribute__((noinline)) routine6()
{
uint64_t sum;
int* bigarray = new int[N];
for(int k = 0; k < N; k += PAGESIZE*4/sizeof(int))
bigarray[k] = 0;
PROFILE (
{
for (int k = 0, *p = bigarray; p != bigarray+N; ++p, ++k)
*p = k;
}, "new + strided memset (every 4th page) (routine6)");
sum = std::accumulate (bigarray, bigarray + N, 0ULL);
delete [] bigarray;
return sum;
}
uint64_t __attribute__((noinline)) routine7()
{
uint64_t sum;
vector<int> bigarray (N);
PROFILE (
{
for (int k = 0; k < N; ++k)
bigarray[k] = k;
}, "vector, using ctor to initialize (routine7)");
sum = std::accumulate (begin (bigarray), end (bigarray), 0ULL);
return sum;
}
uint64_t __attribute__((noinline)) routine8()
{
uint64_t sum;
vector<int> bigarray;
PROFILE (
{
for (int k = 0; k < N; ++k)
bigarray.Push_back (k);
}, "vector (+ no reserve) + Push_back (routine8)");
sum = std::accumulate (begin (bigarray), end (bigarray), 0ULL);
return sum;
}
uint64_t __attribute__((noinline)) routine9()
{
uint64_t sum;
vector<int> bigarray;
bigarray.reserve (N);
PROFILE (
{
for (int k = 0; k < N; ++k)
bigarray.Push_back (k);
}, "vector + reserve + Push_back (routine9)");
sum = std::accumulate (begin (bigarray), end (bigarray), 0ULL);
return sum;
}
uint64_t __attribute__((noinline)) routine10()
{
uint64_t sum;
vector<int> bigarray;
bigarray.reserve (N);
memset(bigarray.data(), 0, sizeof(int)*N);
PROFILE (
{
for (int k = 0; k < N; ++k)
bigarray.Push_back (k);
}, "vector + reserve + memset (UB) + Push_back (routine10)");
sum = std::accumulate (begin (bigarray), end (bigarray), 0ULL);
return sum;
}
template<class T>
void __attribute__((noinline)) adjust_size(std::vector<T>& v, int k, double factor)
{
if(k >= v.size())
{
v.resize(v.size() < 10 ? 10 : k*factor);
}
}
uint64_t __attribute__((noinline)) routine11()
{
uint64_t sum;
vector<int> bigarray;
PROFILE (
{
for (int k = 0; k < N; ++k)
{
adjust_size(bigarray, k, 1.5);
bigarray[k] = k;
}
}, "vector + custom emplace_back @ factor 1.5 (routine11)");
sum = std::accumulate (begin (bigarray), end (bigarray), 0ULL);
return sum;
}
uint64_t __attribute__((noinline)) routine12()
{
uint64_t sum;
vector<int> bigarray;
PROFILE (
{
for (int k = 0; k < N; ++k)
{
adjust_size(bigarray, k, 2);
bigarray[k] = k;
}
}, "vector + custom emplace_back @ factor 2 (routine12)");
sum = std::accumulate (begin (bigarray), end (bigarray), 0ULL);
return sum;
}
uint64_t __attribute__((noinline)) routine13()
{
uint64_t sum;
vector<int> bigarray;
PROFILE (
{
for (int k = 0; k < N; ++k)
{
adjust_size(bigarray, k, 3);
bigarray[k] = k;
}
}, "vector + custom emplace_back @ factor 3 (routine13)");
sum = std::accumulate (begin (bigarray), end (bigarray), 0ULL);
return sum;
}
uint64_t __attribute__((noinline)) routine14()
{
uint64_t sum;
vector<int> bigarray;
PROFILE (
{
for (int k = 0; k < N; ++k)
bigarray.emplace_back (k);
}, "vector (+ no reserve) + emplace_back (routine14)");
sum = std::accumulate (begin (bigarray), end (bigarray), 0ULL);
return sum;
}
uint64_t __attribute__((noinline)) routine15()
{
uint64_t sum;
vector<int> bigarray;
bigarray.reserve (N);
PROFILE (
{
for (int k = 0; k < N; ++k)
bigarray.emplace_back (k);
}, "vector + reserve + emplace_back (routine15)");
sum = std::accumulate (begin (bigarray), end (bigarray), 0ULL);
return sum;
}
uint64_t __attribute__((noinline)) routine16()
{
uint64_t sum;
vector<int> bigarray;
bigarray.reserve (N);
memset(bigarray.data(), 0, sizeof(bigarray[0])*N);
PROFILE (
{
for (int k = 0; k < N; ++k)
bigarray.emplace_back (k);
}, "vector + reserve + memset (UB) + emplace_back (routine16)");
sum = std::accumulate (begin (bigarray), end (bigarray), 0ULL);
return sum;
}
unsigned x = 0;
template<class T>
void /*__attribute__((noinline))*/ silly_branch(std::vector<T>& v, int k)
{
if(k == x)
{
x = x < 10 ? 10 : x*2;
}
//++x;
}
uint64_t __attribute__((noinline)) routine17()
{
uint64_t sum;
vector<int> bigarray(N);
PROFILE (
{
for (int k = 0; k < N; ++k)
{
silly_branch(bigarray, k);
bigarray[k] = k;
}
}, "vector, using ctor to initialize + silly branch (routine17)");
sum = std::accumulate (begin (bigarray), end (bigarray), 0ULL);
return sum;
}
template<class T, int N>
constexpr int get_extent(T(&)[N])
{ return N; }
int main()
{
uint64_t results[] = {routine2(),
routine1(),
routine2(),
routine3(),
routine4(),
routine5(),
routine6(),
routine7(),
routine8(),
routine9(),
routine10(),
routine11(),
routine12(),
routine13(),
routine14(),
routine15(),
routine16(),
routine17()};
std::cout << std::boolalpha;
for(int i = 1; i < get_extent(results); ++i)
{
std::cout << i << ": " << (results[0] == results[i]) << "\n";
}
std::cout << x << "\n";
}
古い低速のコンピューターでのサンプル実行。注意:
N == 2<<28
ではなく2<<29
-std=c++11 -O3 -march=native
を使用してg ++ 4.9 20131022でコンパイル[temp.cpp:71] 0.654927s-> new + full memset(routine2) [temp.cpp:54] 1.042405s-> new(routine1) [temp.cpp:71] 0.605061s-> new + full memset(routine2) [temp.cpp:89] 0.597487s-> new + strided memset(every page half)(routine3) [temp.cpp:107] 0.601271s-> new + strided memset(every page)(routine4) [temp.cpp:125] 0.783610s-> new + strided memset(その他すべて)ページ)(routine5) [temp.cpp:143] 0.903038s-> new + strided memset(4ページごと)(routine6) [temp.cpp:157] 0.602401s- >ベクトル、初期化にctorを使用(routine7) [temp.cpp:170] 3.811291s->ベクトル(+予約なし)+ Push_back(routine8) [temp.cpp:184] 2.091391s->ベクトル+予約+ Push_back(routine9) [temp.cpp:199] 1.375837s- -> vector + reserve + memset(UB)+ Push_back(routine10) [temp.cpp:224] 8.738293s-> vector + custom emplace_back @ factor 1.5(routine11) [temp。 cpp:240] 5.513803s-> vector + custom emplace_back @ factor 2(routine12) [temp.cpp:256] 5.150388s-> vector + custom emplace_back @ factor 3(routine13) [temp.cpp:269] 3.789820s->ベクトル(+予約なし)+ emplace_back(routine14) [temp.cpp:283] 2.090259s->ベクトル+予約+ emplace_back(routine15) [temp.cpp:298] 1.288740s->ベクトル+予約+ memset(UB)+ emplace_back(routine16) [temp.cpp:325] 0.611168s->ベクトル、ctorを使用初期化+愚かなブランチ(ルーチン17) 1:true 2:true 3:true 4:true 5:true 6:真 7:真 8:真 9:真 10:真 11:真 12:真 13:真 14:真 15:真 16:真 17:真 335544320
コンストラクターで配列を割り当てると、コンパイラー/ライブラリーは基本的に元のフィルをmemset()
してから、個々の値を設定できます。 Push_back()
を使用すると、std::vector<T>
クラスは次のことを行う必要があります。
最後のステップは、メモリが一度に割り当てられるときに実行する必要がある唯一のことです。
2つ目の質問にお答えします。ベクトルは事前に割り当てられていますが、Push_backは、Push_backを呼び出すたびに使用可能なスペースを確認する必要があります。一方、operator []はチェックを実行せず、スペースが利用可能であると想定します。
これは回答ではなく拡張コメントであり、質問の改善に役立ちます。
ルーチン4は未定義の動作を呼び出します。配列のsize
の終わりを超えて書き込んでいます。リザーブをサイズ変更に置き換えて、それを排除します。
ルーチン3から5では、監視可能な出力がないため、最適化後は何もできません。
insert( vec.end(), src.begin(), src.end() )
ここで、src
はランダムアクセスジェネレーターの範囲です(boost
おそらく持っている)new
の場合、insert
バージョンをエミュレートできますスマートです。
routine1
の複製はおかしいようです-たぶん、これによりタイミングが変わりますか?