[英]Is iterator range vector construction or vector erase faster?
考慮這兩個不同的函數實現,它從前面刪除x
元素:
template <typename T>
std::vector<T> drop(int size, const std::vector<T>& coll){
if (size<0) return std::vector<T>();
auto sized = size > coll.size() ? coll.size() : size;
typename std::vector<T>::const_iterator first = coll.begin()+sized;
typename std::vector<T>::const_iterator last = coll.end();
return std::vector<T>(first,last);
}
template <typename T>
std::vector<T> drop2(int size, std::vector<T> coll){
if (size<0) return std::vector<T>();
auto sized = size > coll.size() ? coll.size() : size;
coll.erase(coll.begin(),coll.begin()+sized);
return coll;
}
在這兩個版本中,都分配了一個新的std::vector
(在第二個版本中,它被復制為參數,而不是參考)。 在一個中,結果由erase()
創建,而在另一個中,結果是使用原始向量的迭代器創建的。
有沒有理由相信其中一個在性能上會有不同的影響?
另外,RVO是其中一種或兩種的保證嗎?
編輯:
這是我做的一個測試,它顯示第一個比第二個慢得多:
template<typename F>
void dropExample(F f){
std::cout<<"drop example"<<std::endl;
auto t1 = Clock::now();
for (auto x: range(100000)){
f(2, range(100));
}
auto t2 = Clock::now();
std::cout << "Delta t2-t1: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count()
<< " ms" << std::endl;
}
輸出:
dropExample(drop<int>);
dropExample(drop2<int>);
drop example
Delta t2-t1: 625 ms
drop example
Delta t2-t1: 346 ms
無論我在for
循環中添加了多少次迭代,數字大致都是這樣的,即使對於幾十秒的操作也是如此。
編輯2:
正如評論中所建議的那樣,我用左值增加了測試:
template<typename F, typename T>
void dropExample2(F f, T vec){
std::cout<<"drop example 2"<<std::endl;
auto t1 = Clock::now();
for (auto x: range(1000)){
f(2, vec);
}
auto t2 = Clock::now();
std::cout << "Delta t2-t1: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count()
<< " ms" << std::endl;
}
然后在主要:
int main(int argc, const char * argv[]) {
auto testrange=range(100000);
dropExample(drop<int>);
dropExample(drop2<int>);
dropExample2(drop<int>,testrange);
dropExample2(drop2<int>,testrange);
return 0;
}
輸出仍然表明第二個更快:
drop example
Delta t2-t1: 564 ms
drop example
Delta t2-t1: 375 ms
drop example 2
Delta t2-t1: 2318 ms
drop example 2
Delta t2-t1: 698 ms
以下是示例中使用的補充函數:
std::vector<int> range(int start, int end, int step);
std::vector<int> range(int start, int end){
if (end<start){
return range(start,end,-1);
}else if (start == end){
return std::vector<int> {start};
}else{
std::vector<int> nums(end-start);
std::iota(nums.begin(),nums.end(),start);
return nums;}
}
std::vector<int> range(int end){
return range(0,end);
}
std::vector<int> range(int start, int end, int step){
std::vector<int> nums{start};
auto next=start+step;
while ((next<end&&start<=end&&step>0)||
(next>end&&start>end&&step<0))
{
nums.push_back(next);
next+=step;
}
return nums;
}
第一個是幾乎可以肯定更快,除非你喂養drop
右值,在這種情況下,你自己去衡量。
假設你有N個元素開始,要刪除M個元素:
你的第二個例子將創建一大堆對象(在復制輸入參數時),以便稍后將其刪除(在調用erase時)。 因此的性能差異將取決於T
是什么,但我懷疑第一個會變慢。
此外,在第二個版本中使用的內存量將更大,因為擦除不會重新分配內存。
編輯
您當前的測試存在缺陷,因為您將向量作為臨時子集傳遞,允許編譯器移動構造drop2
的輸入參數,從而完全drop2
副本。 簡單地改變:
for (auto x: range(100000))
f(200, range(10000));
至
auto v = range(10000);
for (auto x: range(100000))
f(200, v);
大大改變了結果。 然而,第二種方法對我來說仍然更快,直到矢量更大。 值得注意的是,因為您使用的是int
,所以可以針對memcpy
和幾個指針操作優化不同的方法。
drop
可以簡單地成為(coll.size() - size) * sizeof(int)
字節的memcpy,而drop2
可以成為coll.size() * sizeof(int)
字節的memcpy。 這是因為int
的析構函數是no op,因此對erase的調用可以簡單地從向量的__last
指針中減去size
。
如果您感興趣的是這樣的原始類型那么沒關系,但是如果您還想為std::string
提供最佳實現,那么它的析構函數和復制構造函數將成為非常重要的因素。 我已經嘗試使用std::vector<int>
作為std::vector<int>
中的類型,雖然總體上較慢,但對於較小的大小,似乎drop2
仍然更快。 然而,在較低的閾值下, drop
變得更有效。 我非常懷疑這就是我們在這里看到的內容,因此我們最終運行的代碼處於某種中間狀態,即只是memcpy
和我們逐字寫入的內容。
我想最后我們正在測試編譯器優化不同函數的能力( std::uninitialized_copy
, std::move
(基於迭代器的函數),調用get_allocator().destroy(p)
在一個循環中瑣碎和非瑣碎的類型等...)。 我現在可以說的是,結果在優化的內容和代碼中看似微小的變化的大小方面會有很大差異。
然而,我仍然感到驚訝的是, drop2
運行速度比drop
速度快,即使只是在一定大小的范圍內。
真的,答案在於兩個版本的廣泛基准。
然而,進行評估時,我傾向於認為第一個版本會更快,因為在一般情況下你必須將較少的元素從初始向量復制到輸出向量,而不是在第二個版本中你復制它們。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.