2017-07-12 20 views
7

tôi cung cấp cho các ví dụ sau để minh họa cho câu hỏi của tôi:Làm thế nào để giảm mức tiêu thụ bộ nhớ của toán tử + cho lớp tự định nghĩa trong C++?

class BigClass 
{ 
public: 
    static int destruct_num; 
    friend BigClass operator + (const BigClass &obj1, const BigClass &obj2); 
    std::vector<int> abc; 

    BigClass() 
    { 

    } 

    ~BigClass() 
    { 
     destruct_num++; 
     std::cout << "BigClass destructor " <<destruct_num<< std::endl; 

    } 

    BigClass(BigClass&& bc) :abc(std::move(bc.abc)) 
    { 
     std::cout << "move operation is involed" << std::endl; 
    } 
}; 

int BigClass::destruct_num = 0; 

BigClass operator + (const BigClass &obj1, const BigClass &obj2) 
{ 
    BigClass temp; 
    temp.abc = obj1.abc; 
    temp.abc.insert(temp.abc.end(), obj2.abc.begin(), obj2.abc.end()); 

    return temp; 

} 

int main(void) 
{ 

    BigClass a; 
    a.abc = { 1,2,3 }; 
    BigClass b; 
    b.abc = { 5,6,7 }; 
    BigClass c = a + b; 

// for (auto& v : c.abc) 
//  std::cout << v << " "; 

    return 0; 


} 

Một vấn đề liên quan đến operator + với là chúng ta phải tạo ra một đối tượng tạm BigClass tạm thời và sau đó trả lại. Có cách nào để giảm bớt gánh nặng này?

+0

Có thể trùng lặp: https://stackoverflow.com/questions/4421706/what-are-the-basic-rules-and-idioms-for-operator-overloading – Rakete1111

+2

sao chép elision? http://en.cppreference.com/w/cpp/language/copy_elision – cppBeginner

+4

bạn có thể tránh tạm thời, nhưng cách duy nhất để tránh tạo ra một trường hợp mới không phải là để sử dụng 'operator ='. 'Operator + = 'không yêu cầu bạn phải tạo một đối tượng mới – user463035818

Trả lời

7

chung:

[...] các trình biên dịch được phép, nhưng không nhất thiết phải bỏ qua các bản sao [...]

Dù sao chức năng của bạn nên được tối ưu hóa bởi bất kỳ trình biên dịch hiện đại bởi vì copy elision.

Here một ví dụ:

Kết quả của operator+ là trong phần lắp ráp:

call operator+(BigClass const&, BigClass const&) 
addl $12, %esp 

Như bạn có thể thấy, không copy constructor được gọi để sao chép kết quả.

Thật vậy, nếu chúng ta vô hiệu hóa tối ưu hóa sao chép sự bỏ bớt trong GCC, các result thay đổi:

call operator+(BigClass const&, BigClass const&) 
addl $12, %esp 
subl $8, %esp 
leal -20(%ebp), %eax 
pushl %eax 
leal -56(%ebp), %eax 
pushl %eax 
call BigClass::BigClass(BigClass&&) 
addl $16, %esp 
subl $12, %esp 
leal -20(%ebp), %eax 
pushl %eax 
call BigClass::~BigClass() 
addl $16, %esp 

Sau tiếng gọi của operator+ bản sao (hoặc di chuyển trong trường hợp này) constructor được gọi, và sau khi destructor của đối tượng tạm thời.

Lưu ý rằng việc thu thập bản sao thu được ngay cả khi tắt tối ưu hóa (-O0).

Cùng một kết quả thu được với phiên bản cũ hơn: GCC 4.4.7.


Kể từ bản sao sự bỏ bớt không được bảo đảm cho tất cả các kiến ​​trúc, bạn có thể thực hiện một số giải pháp khác nhau.

Một giải pháp khả thi là tránh phân bổ một biến tạm thời bên trong hàm, yêu cầu người gọi đặt chỗ của không gian đó. Để làm điều đó, bạn nên sử dụng phương pháp "tùy chỉnh" và tránh quá tải operator+.

void sum_bigClasses(const BigClass& obj1, const BigClass& obj2, BigClass& output) { 
    // output.resize(obj1.size() + obj2.size()); 
    // std::copy(...); 
} 

Một giải pháp đó có thể là để thực hiện một hành không const cho tổng.Một ví dụ:

BigClass& operator+=(const BigClass& rhs) { 
    // std::copy(rhs.cbegin(), rsh.cend(), std::back_inserter(abc)); 
    return *this; 
} 

Bằng cách này, giao diện lớp cho phép các chiến lược khác nhau:

  • Tránh giao 3 đối tượng khác nhau nhưng chỉ có 2, nếu bạn không cần phải giữ gìn tất cả các tiểu bang khác nhau.
  • Cho phép phân bổ 3 đối tượng khác nhau và tránh xây dựng tạm thời bên trong toán tử.

Ở đây, hai ví dụ.

Điểm đầu tiên:

BigClass o1; 
BigClass o2; 
// Fill o1 and o2; 
o1 += o2; 
// only 2 object are alive 

Điểm thứ hai:

BigClass o1; 
BigClass o2; 
// Fill o1 and o2; 
BigClass o3 = o1; // Costructor from o1 
o3 += o2; 
// three different object 

EDIT: Kể từ khi chức năng đó là một NRVO (biểu thức quay trở lại không phải là một prvalue) không phải chuẩn C++ 17 w mới bị bệnh bảo vệ bản sao.

+0

Không C++ 17 đảm bảo sao chép elision cho RVO chỉ? Phần tử 'operator +' sẽ gọi NRVO. –

+1

@underscore_d Bạn hoàn toàn đúng! Tôi sẽ cập nhật câu trả lời của tôi, cảm ơn bạn! –

1

Nếu bạn chạy mã của bạn hơn bạn thấy rằng chỉ có 3 destructors được gọi. Điều đó có nghĩa là giá trị của đối tượng tmp được di chuyển, không được sao chép, vì RVO (Trả về giá trị tối ưu). Trình biên dịch không sao chép nó, bởi vì nó thấy đó là không cần thiết.

0

Việc sử dụng thời gian không chỉ lãng phí bộ nhớ mà còn thời gian xử lý (tính tổng của N BigClass trường hợp có thể có độ phức tạp bậc hai trong N). Không có giải pháp chung để tránh điều này bởi vì nó phụ thuộc vào cách các đối tượng của bạn được sử dụng. Trong kịch bản này:

BigClass c = a + b; 

trình biên dịch đã được miễn phí (hoặc yêu cầu, C++ 17) để sử dụng bản sao sự bỏ bớt, như được giải thích bởi banana36, và các đầu vào được lvalues, do đó họ không thể thay đổi mà không có khả năng gây ra rất lớn sự ngạc nhiên.

Một kịch bản khác nhau sẽ là:

BigClass f(); 
BigClass g(); 

BigClass h = f() + g(); 

Trong trường hợp này, f()g() là rvalues ​​và sao chép tất cả trong số họ là lãng phí. Việc lưu trữ ít nhất một trong số chúng có thể được sử dụng lại, ví dụ: người ta có thể viết thêm operator + quá tải để tối ưu hóa các trường hợp summand trái là một rvalue:

BigClass operator +(BigClass &&a, const BigClass &b) 
{ 
    a.abc.insert(a.abc.end(), b.abc.begin(), b.abc.end()); 
    return std::move(a); 
} 

này tái sử dụng lưu trữ a.abc 's và tránh sao chép nội dung của nó càng lâu càng khả năng là đủ. Một hiệu ứng phụ tốt đẹp là ví dụ tổng hợp N đối tượng có 10 phần tử sẽ có hiệu suất tuyến tính vì việc chèn một số nguyên tố không đổi ở cuối số std::vector có chi phí khấu hao không đổi. Nhưng nó chỉ hoạt động nếu quá tải phải của operator + được chọn, ví dụ: là không phải là trường hợp cho std::accumulate. Dưới đây là một tổng quan về lựa chọn chính của bạn:

  1. Supply operator +(const BigClass &, const BigClass &)operator +=, và giáo dục người dùng của bạn trên những tác động hiệu suất của việc sử dụng các cựu cẩu thả.
  2. Có thể thêm quá tải cho operator +(BigClass &&, const BigClass &) và có thể operator +(const BigClass &, BigClass &&)operator +(BigClass &&, BigClass &&).Lưu ý rằng nếu bạn có cả hai quá tải với một tài liệu tham khảo rvalue, bạn nên hoàn toàn cũng có thêm sự quá tải với hai tài liệu tham khảo rvalue, nếu không f() + g() sẽ là một cuộc gọi mơ hồ. Cũng lưu ý rằng sự quá tải trong đó tham số tay phải là tham chiếu rvalue phù hợp nhất để sử dụng với ví dụ: std::deque và không std::vector bởi vì nó có độ phức tạp thời gian ít hơn trên chèn phía trước, nhưng thay thế một vector với một deque chỉ rất hữu ích nếu trường hợp sử dụng này là phổ biến, vì deque là khác chậm hơn so với vector.
  3. Cung cấp chỉ hoạt động hiệu quả như operator += và đối phó với sự thất vọng của người sử dụng (cách khác, cung cấp cho các hoạt động kém hiệu quả tên miệt thị như copyAdd).
Các vấn đề liên quan