2013-03-03 20 views
10

Tôi đã thử nghiệm một số mã nơi có một thành viên dữ liệu std::vector bên trong một lớp học. Lớp vừa copiabledi chuyển, và operator= được thực hiện như mô tả here sử dụng sao chép và hoán đổi thành ngữ.Tính không hiệu quả của thành ngữ sao chép và hoán đổi?

Nếu có hai vector s, nói v1 với công suất lớn và v2 với công suất nhỏ, và v2 được sao chép vào v1 (v1 = v2), công suất lớn trong v1 được giữ sau khi chuyển nhượng; điều này có ý nghĩa, vì các cuộc gọi v1.push_back() tiếp theo không cần phải buộc các đường dẫn mới (nói cách khác: giải phóng bộ nhớ đã có sẵn, sau đó phân bổ lại để phát triển vectơ không có ý nghĩa nhiều).

Nhưng, nếu việc chuyển nhượng tương tự được thực hiện với lớp vector như thành viên dữ liệu, hành vi này là khác nhau, và sau khi chuyển nhượng công suất lớn hơn là không giữ.

Nếu thành ngữ sao chép và trao đổi là không sử dụng, và sao chép và di chuyển operator=operator= được thực hiện riêng, sau đó hành vi được như mong đợi (như đối với bình thường không thành viên vector s).

Tại sao lại như vậy? Chúng tôi có nên không theo thành ngữ sao chép và hoán đổi và thay vào đó triển khai operator=(const X& other) (sao chépop=) và operator=(X&& other) (di chuyểnop=) riêng biệt để có hiệu suất tối ưu?

Đây là sản phẩm của một thử nghiệm tái sản xuất với sao chép và hoán đổi thành ngữ (lưu ý như thế nào trong trường hợp này, sau khi x1 = x2, x1.GetV().capacity() là 1.000, không 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 

Đây là đầu ra mà không có thành phần sao chép và hoán đổi (lưu ý cách thức trong trường hợp này x1.GetV().capacity() = 1000000, như mong đợi):

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 

compilable mẫu mã sau (thử nghiệm với 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(); 
} 
+0

@juanchopanza: Giống hệt nhau. – Puppy

+0

@DeadMG Yep, tệ của tôi. – juanchopanza

+0

@juanchopanza: Nó không tạo ra bất kỳ sự khác biệt nào. Cũng lưu ý rằng [mô hình điển hình của thành ngữ sao chép và hoán đổi] (http://stackoverflow.com/a/3279550/1629821) là "miễn phí" 'hoán đổi()'. –

Trả lời

12

Sao chép và trao đổi với một std::vector thực sự có thể dẫn đến mất hiệu suất. Vấn đề chính ở đây là sao chép một std::vector liên quan đến hai giai đoạn riêng biệt:

  1. Phân bổ phần mới của bộ nhớ
  2. Sao chép nội dung trong.

Sao chép và hoán đổi có thể loại bỏ # 2 nhưng không # 1. Hãy xem xét những gì bạn sẽ quan sát trước khi cuộc gọi swap() nhưng sau khi được gán op. Bạn có ba vectơ - một vectơ sắp sửa bị ghi đè, cái mà là bản sao và đối số ban đầu. Điều này rõ ràng ngụ ý rằng nếu vectơ sắp sửa bị ghi đè có dung lượng đầy đủ hoặc dư thừa, có một sự lãng phí trong việc tạo ra các vector trung gian, và mất đi khả năng bổ sung của nguồn. Các thùng chứa khác cũng có thể hoạt động theo cách này.

Sao chép và trao đổi là một đường cơ sở tuyệt vời, đặc biệt là khi nói đến sự an toàn ngoại lệ, nhưng nó là không phải là giải pháp hiệu suất cao nhất trên toàn cầu. Nếu bạn đang ở trong một khu vực chật hẹp, thì các triển khai chuyên biệt khác có thể hiệu quả hơn - nhưng được cảnh báo, ngoại lệ an toàn trong lĩnh vực này là không tầm thường và đôi khi không thể nếu không sao chép và đổi chỗ.

+0

Tôi nghĩ rằng [@better urbanite đã giải quyết vấn đề] (http://stackoverflow.com/a/15188347/1629821) với _ "Nhiệm vụ bảo tồn bài tập của mình.' Swap hoán đổi công suất. "_ Tuyên bố. Bạn có một điểm tốt trên _exception-safety_: trên thực tế, tôi không thể tìm ra cách khác để có được sự an toàn ngoại lệ mạnh mẽ mà không cần sao chép và trao đổi. Vì vậy, tôi nên chọn giữa ngoại lệ mạnh mẽ-an toàn với sao chép và trao đổi hoặc thực hiện trên cùng độc quyền với việc thực hiện bản sao 'operator =' và di chuyển 'operator =' (nhưng từ bỏ ngoại lệ mạnh mẽ)? Không thể có cả hai? –

+0

@ Mr.C64: Bạn có thể có cả hai, nhưng nếu có nhiều hơn một thành viên có thể ném-ném trong lớp của bạn, về cơ bản là không thể có sự an toàn ngoại lệ mạnh cho 'operator =' mà không cần sao chép và hoán đổi. – Puppy

+0

Vì vậy, nếu tôi có nhiều thành viên dữ liệu 'vector' trong lớp, tôi phải chọn giữa khả năng bảo toàn thực thi cả bản sao' operator = 'và di chuyển' toán tử = 'riêng biệt, hoặc có ngoại lệ mạnh mẽ an toàn với bản sao và -swap (và không duy trì công suất)? –

5

Trong trường hợp X, bạn đang hoán đổi các véc tơ, không sử dụng vector::operator=(). Nhiệm vụ bảo quản nhiệm vụ. swap khả năng hoán đổi.

2

Nếu có hai vectơ, nói v1 với công suất lớn và v2 với nhỏ công suất , và v2 được sao chép vào v1 (v1 = v2), công suất lớn trong v1 được giữ sau khi chuyển nhượng; điều này có ý nghĩa,

Nó không phải với tôi.

Sau khi gán, tôi cho rằng vectơ được gán có cùng giá trị và trạng thái khi véc tơ được gán. Tại sao tôi phải chịu đựng và phải kéo xung quanh dung lượng dư thừa.

Từ việc quét nhanh tiêu chuẩn, tôi không chắc chắn rằng tiêu chuẩn đảm bảo rằng dung lượng được giữ liên tục trong quá trình gán từ một véc tơ nhỏ hơn. (Nó sẽ được giữ trên một yêu cầu của vector::assign(...), do đó có thể là ý định.)

Nếu tôi quan tâm về hiệu quả bộ nhớ, tôi phải gọi vector::shrink_to_fit() sau khi chuyển nhượng trong nhiều trường hợp, nếu nhiệm vụ không làm điều này cho tôi.

Sao chép và trao đổi có ngữ nghĩa thu nhỏ. Trên thực tế nó là thành ngữ C++ 98 bình thường cho co lại để phù hợp với các thùng chứa tiêu chuẩn.

từ v1.push_back tiếp theo() các cuộc gọi không cần phải buộc tái phân bổ mới (nói cách khác: giải phóng bộ nhớ đã có sẵn, sau đó phân bổ lại nó phát triển vector không có ý nghĩa nhiều).

Đúng, nhưng điều đó tùy thuộc vào mẫu sử dụng của bạn. Nếu bạn gán các vectơ và sau đó tiếp tục thêm vào chúng, giữ cho bất kỳ khả năng có sẵn nào có ý nghĩa. Nếu bạn chỉ định một vectơ sau khi bạn đã xây dựng nội dung của nó, bạn có thể không muốn giữ dung lượng dư thừa được phân bổ.

Nhưng, nếu việc chuyển nhượng tương tự được thực hiện với sự lớp có vector như thành viên dữ liệu, hành vi này là khác nhau, và sau khi chuyển nhượng công suất lớn hơn không được giữ lại.

Đúng, nếu bạn sao chép và hoán đổi trong lớp đó. Làm như vậy cũng sẽ sao chép và hoán đổi các vector chứa, và như đã đề cập ở trên, đó là một cách để thu nhỏ lại cho vừa.

Nếu thành ngữ sao chép và trao đổi không được sử dụng, và sao chép operator = và di chuyển operator = được thực hiện riêng rẽ, sau đó hành vi được như mong đợi (như đối với vectơ phi thành viên thông thường).

Như đã thảo luận ở trên: có thể gây tranh cãi cho dù hành vi đó như mong đợi. Nhưng nếu nó phù hợp với các mẫu sử dụng của bạn, tức là nếu bạn muốn tiếp tục phát triển một vector sau khi được gán từ một vector khác có thể nhỏ hơn giá trị trước đó, thì bạn có thể đạt được hiệu quả bằng cách sử dụng không làm giảm dung lượng dư thừa hiện tại (ví dụ: vector::assign).

Tại sao lại như vậy? Nếu chúng ta không tuân theo thành ngữ sao chép và hoán đổi và thay vì thực hiện toán tử = (const X & khác) (copy op =) và toán tử = (X & & khác) (di chuyển op =) riêng biệt để đạt hiệu suất tối ưu?

Như đã thảo luận, nếu nó phù hợp với mẫu sử dụng của bạn và hiệu suất của chuỗi được gán và nối tiếp là rất quan trọng, thì bạn thực sự có thể xem xét không sử dụng trao đổi và sao chép để gán. Mục đích chính của trao đổi và sao chép là thực hiện tối thiểu (tránh mã trùng lặp) và an toàn ngoại lệ mạnh.

Nếu bạn chọn triển khai khác để đạt hiệu suất tối đa, bạn sẽ phải tự lo an toàn ngoại lệ và bạn sẽ phải trả một mức giá phức tạp về mã.

+0

Tôi hơi không đồng ý vì bạn luôn có thể 'shrink_to_fit' sau, nhưng bạn không thể' unshrink_to_fit'. – Puppy

Các vấn đề liên quan