[英]Inefficiency of copy-and-swap idiom?
我正在測試一些代碼,其中一個類中有一個std::vector
數據成員。 類是可復制都和活動 ,並且operator=
如所描述的被實施在這里使用復制和交換成語 。
如果有兩個vector
S,說v1
大容量和v2
小容量,並且v2
被復制到v1
( v1 = v2
), 在大容量v1
保持在分配之后; 這是有道理的,因為下一次v1.push_back()
調用不必強制新的重新分配(換句話說:釋放已經可用的內存,然后重新分配它來增長向量沒有多大意義)。
但是,如果對具有vector
作為數據成員的類進行相同的賦值,則行為是不同的,並且在賦值之后不保留更大的容量。
如果復制和交換成語沒有被使用,並復制operator=
和移動operator=
被單獨實現,則該行為是按預期(對於普通的非成員vector
S)。
這是為什么? 我們是否應該遵循copy-and-swap慣用法,而是分別實現operator=(const X& other)
( copy op=
)和operator=(X&& other)
( move op=
)以獲得最佳性能?
這是具有復制和交換習慣用法的可重現測試的輸出(注意在這種情況下,在x1 = x2
, x1.GetV().capacity()
是1,000,而不是1,000,000):
C:\\TEMP\\CppTests>cl /EHsc /W4 /nologo /DTEST_COPY_AND_SWAP test.cpp test.cpp C:\\TEMP\\CppTests>test.exe v1.capacity() = 1000000 v2.capacity() = 1000 After copy v1 = v2: v1.capacity() = 1000000 v2.capacity() = 1000 [Copy-and-swap] x1.GetV().capacity() = 1000000 x2.GetV().capacity() = 1000 After x1 = x2: x1.GetV().capacity() = 1000 x2.GetV().capacity() = 1000
這是沒有復制和交換習慣用法的輸出(注意在這種情況下x1.GetV().capacity() = 1000000
,如預期的那樣):
C:\\TEMP\\CppTests>cl /EHsc /W4 /nologo test.cpp test.cpp C:\\TEMP\\CppTests>test.exe v1.capacity() = 1000000 v2.capacity() = 1000 After copy v1 = v2: v1.capacity() = 1000000 v2.capacity() = 1000 [Copy-op= and move-op=] x1.GetV().capacity() = 1000000 x2.GetV().capacity() = 1000 After x1 = x2: x1.GetV().capacity() = 1000000 x2.GetV().capacity() = 1000
隨后是可編譯的示例代碼(使用VS2010 SP1 / VC10進行測試):
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
class X
{
public:
X()
{
}
explicit X(const size_t initialCapacity)
{
m_v.reserve(initialCapacity);
}
X(const X& other)
: m_v(other.m_v)
{
}
X(X&& other)
: m_v(move(other.m_v))
{
}
void SetV(const vector<double>& v)
{
m_v = v;
}
const vector<double>& GetV() const
{
return m_v;
}
#ifdef TEST_COPY_AND_SWAP
//
// Implement a unified op= with copy-and-swap idiom.
//
X& operator=(X other)
{
swap(*this, other);
return *this;
}
friend void swap(X& lhs, X& rhs)
{
using std::swap;
swap(lhs.m_v, rhs.m_v);
}
#else
//
// Implement copy op= and move op= separately.
//
X& operator=(const X& other)
{
if (this != &other)
{
m_v = other.m_v;
}
return *this;
}
X& operator=(X&& other)
{
if (this != &other)
{
m_v = move(other.m_v);
}
return *this;
}
#endif
private:
vector<double> m_v;
};
// Test vector assignment from a small vector to a vector with big capacity.
void Test1()
{
vector<double> v1;
v1.reserve(1000*1000);
vector<double> v2(1000);
cout << "v1.capacity() = " << v1.capacity() << '\n';
cout << "v2.capacity() = " << v2.capacity() << '\n';
v1 = v2;
cout << "\nAfter copy v1 = v2:\n";
cout << "v1.capacity() = " << v1.capacity() << '\n';
cout << "v2.capacity() = " << v2.capacity() << '\n';
}
// Similar to Test1, but now vector is a data member inside a class.
void Test2()
{
#ifdef TEST_COPY_AND_SWAP
cout << "[Copy-and-swap]\n\n";
#else
cout << "[Copy-op= and move-op=]\n\n";
#endif
X x1(1000*1000);
vector<double> v2(1000);
X x2;
x2.SetV(v2);
cout << "x1.GetV().capacity() = " << x1.GetV().capacity() << '\n';
cout << "x2.GetV().capacity() = " << x2.GetV().capacity() << '\n';
x1 = x2;
cout << "\nAfter x1 = x2:\n";
cout << "x1.GetV().capacity() = " << x1.GetV().capacity() << '\n';
cout << "x2.GetV().capacity() = " << x2.GetV().capacity() << '\n';
}
int main()
{
Test1();
cout << '\n';
Test2();
}
使用std::vector
進行復制和交換確實會導致性能下降。 這里的主要問題是復制std::vector
涉及兩個不同的階段:
復制和交換可以消除#2但不會消除#1。 考慮在swap()調用之前您將觀察到的內容,但是在輸入賦值操作之后。 你有三個向量 - 即將要被覆蓋的向量,一個是副本的向量,以及原始參數。
這清楚地暗示,如果即將被覆蓋的矢量具有足夠或過剩的容量,則在創建中間矢量時會浪費,並且源的額外容量會損失。 其他容器也可以這樣做。
復制和交換是一個很好的基准,特別是在涉及異常安全時,但它並不是全球性能最高的解決方案。 如果你處於一個狹窄的區域,那么其他更專業的實現可以更有效 - 但要注意,這個領域的異常安全是非平凡的,如果不進行復制和交換有時是不可能的。
在X
情況下,您正在交換向量,而不是使用vector::operator=()
。 作業保留了容量。 swap
掉容量。
如果有兩個向量,比如v1具有大容量而v2具有小容量,並且v2被復制到v1(v1 = v2),則在賦值后保留v1中的大容量; 這是有道理的,
它不適合我。
在賦值之后,我希望賦值的向量具有與賦值的向量相同的值和狀態。 我為什么要招致並且不得不拖延多余的容量。
從標准的快速掃描中我不確定標准是否保證容量在較小的向量分配中保持不變。 (它將保留在vector::assign(...)
的調用中,因此這可能是意圖。)
如果我關心內存效率,在很多情況下我必須在賦值后調用vector::shrink_to_fit()
,如果賦值不對我這樣做的話。
復制和交換具有縮小到適合的語義。 實際上,它是通常的C ++ 98習慣用於縮小標准容器的配合。
因為下一次v1.push_back()調用不必強制新的重新分配(換句話說:釋放已經可用的內存,然后重新分配它以增長向量沒有多大意義)。
沒錯,但這取決於您的使用模式。 如果您指定向量然后繼續添加它們,則保留任何預先存在的容量是有意義的。 如果在構建其內容后分配向量,則可能不希望保留分配的多余容量。
但是,如果對具有向量作為數據成員的類進行相同的賦值,則行為是不同的,並且在賦值之后不保留更大的容量。
是的,如果你復制並交換該類。 這樣做也會復制和交換包含的向量,如上所述,這是一種實現縮小以適應的方法。
如果未使用復制和交換習慣用法,並且復制運算符=和移動運算符=分別實現,則行為與預期的一樣(對於普通的非成員向量)。
如上所述:這種行為是否符合預期是有爭議的。
但是,如果它符合您的使用模式,即如果您希望在從另一個可能小於先前值的分配后繼續增長向量,那么通過使用不會減少現有多余的東西,您確實可以獲得一些效率容量(例如vector::assign
)。
這是為什么? 我們是否應該遵循copy-and-swap慣用法,而是分別實現operator =(const X&other)(copy op =)和operator =(X && other)(move op =)以獲得最佳性能?
如上所述,如果它符合您的使用模式,並且該分配和追加序列的性能至關重要,那么您確實可以考慮不使用交換和復制進行分配。 交換和復制的主要目的是最小化實現(避免重復代碼)和強大的異常安全性。
如果您選擇不同的實現以獲得最佳性能,則必須自己處理異常安全問題,並且您需要為代碼復雜性付出代價。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.