2013-09-05 33 views
9

Sự hiểu biết của tôi về tối ưu hóa giá trị trả về là trình biên dịch bí mật chuyển địa chỉ của đối tượng trong đó giá trị trả về sẽ được lưu trữ và thay đổi đối tượng đó thay vì biến cục bộ.Làm cách nào để người gọi hàm biết liệu có sử dụng Tối ưu hóa giá trị trả lại không?

Ví dụ, mã

std::string s = f(); 

std::string f() 
{ 
    std::string x = "hi"; 
    return x; 
} 

trở thành tương tự như

std::string s; 
f(s); 

void f(std::string& x) 
{ 
    x = "hi"; 
} 

Khi RVO được sử dụng. Điều này có nghĩa là giao diện của hàm đã thay đổi, vì có một tham số ẩn phụ.

Bây giờ xem xét các trường hợp sau đây tôi lấy trộm từ Wikipedia

std::string f(bool cond) 
{ 
    std::string first("first"); 
    std::string second("second"); 
    // the function may return one of two named objects 
    // depending on its argument. RVO might not be applied 
    return cond ? first : second; 
} 

Giả sử rằng một trình biên dịch sẽ áp dụng RVO đến trường hợp đầu tiên, nhưng không đến trường hợp thứ hai này. Nhưng giao diện của chức năng có thay đổi không phụ thuộc vào việc RVO có được áp dụng không? Nếu cơ quan chức năng f không hiển thị với trình biên dịch, trình biên dịch biết liệu RVO có được áp dụng hay không và liệu người gọi có cần truyền tham số địa chỉ ẩn không?

+3

Mọi trình biên dịch đều có thể chọn để thực hiện theo bất kỳ cách nào họ muốn và nếu vấn đề của họ trong trường hợp bạn mô tả, họ có thể chọn luôn sử dụng RVO. Khi bạn đang intrested trong cách trình biên dịch khác nhau làm nó dưới mui xe, tôi đề nghị bạn đọc mã assembler tạo ra. Sử dụng trình khám phá gcc để dễ dàng truy cập vào lắp ráp được tạo bằng clang/gcc. – PlasmaHH

+0

@PlasmaHH Nhưng RVO không phải lúc nào cũng có thể. –

+1

Điều đó không có nghĩa là quy ước cuộc gọi sẽ thay đổi khi nó được hoặc không được sử dụng. Nhìn vào một số người lắp ráp cách họ làm điều đó. – PlasmaHH

Trả lời

7

Không có thay đổi trong giao diện. Trong mọi trường hợp, kết quả của hàm phải xuất hiện trong phạm vi của người gọi; thông thường, trình biên dịch sử dụng một con trỏ ẩn. Sự khác biệt duy nhất là khi sử dụng RVO, như trong trường hợp đầu tiên, trình biên dịch sẽ "hợp nhất" x và giá trị trả về này, xây dựng x tại địa chỉ được con trỏ đưa ra; khi nó không được sử dụng, trình biên dịch sẽ tạo ra một cuộc gọi đến hàm tạo bản sao trong câu lệnh trả về , để sao chép bất kỳ thứ gì vào giá trị trả lại này.

Tôi có thể thêm ví dụ thứ hai của bạn là không phải rất gần với những gì xảy ra. Tại địa điểm cuộc gọi, bạn nhận được hầu như luôn luôn một cái gì đó như:

<raw memory for string> s; 
f(&s); 

Và gọi là chức năng hoặc là sẽ xây dựng một biến địa phương hoặc tạm thời trực tiếp tại địa chỉ nó được thông qua, hoặc sao chép xây dựng một số giá trị Othe tại đây địa chỉ nhà. Vì vậy mà trong ví dụ cuối cùng của bạn, báo cáo kết quả lợi nhuận sẽ được nhiều hơn hoặc ít hơn tương đương với:

if (cont) { 
    std::string::string(s, first); 
} else { 
    std::string::string(s, second); 
} 

(. Hiển thị các ngầm this con trỏ truyền cho constructor sao chép ) Trong trường hợp đầu tiên, nếu RVO áp dụng , các mã đặc biệt sẽ là trong constructor của x:

std::string::string(s, "hi"); 

và sau đó thay thế x với *s khắp mọi nơi khác trong hàm (và không làm gì khi trả lại).

+0

Vì vậy, nó thêm một tham số ẩn và làm cho các chức năng void? (rõ ràng dưới mui xe) – xanatos

+2

@xanatos: không có chức năng "void" ở cấp độ lắp ráp, có các quy ước về nơi đặt giá trị trả về và nơi người gọi mong đợi chúng, "void" chỉ là trường hợp số giá trị trả về là 0. – PlasmaHH

+1

@PlasmaHH Và không có tham số nào ở cấp độ trình kết hợp ... Chỉ những thứ mà ai đó đã đẩy lên ngăn xếp hoặc đặt vào sổ đăng ký. Chúng tôi có thể chơi trò chơi này cả ngày. Giả sử nó không đẩy giá trị trả về trên stack hoặc đặt nó vào thanh ghi, vì vậy nó tương tự như một hàm void. – xanatos

2

Cho phép chơi với NRVO, RVO và sao chép elision!

Dưới đây là một loại:

#include <iostream> 
struct Verbose { 
    Verbose(Verbose const&){ std::cout << "copy ctor\n"; } 
    Verbose(Verbose &&){ std::cout << "move ctor\n"; } 
    Verbose& operator=(Verbose const&){ std::cout << "copy asgn\n"; } 
    Verbose& operator=(Verbose &&){ std::cout << "move asgn\n"; } 
}; 

đó là khá dài dòng.

Đây là một chức năng:

Verbose simple() { return {}; } 

rằng là khá đơn giản, và sử dụng xây dựng trực tiếp của giá trị trả về của nó. Nếu Verbose thiếu một hàm tạo bản sao hoặc di chuyển, hàm trên sẽ hoạt động!

Đây là một chức năng sử dụng RVO:

Verbose simple_RVO() { return Verbose(); } 

ở đây vô danh Verbose() đối tượng tạm thời đang được nói để sao chép chính nó vào giá trị trả về. RVO có nghĩa là trình biên dịch có thể bỏ qua bản sao đó và trực tiếp xây dựng Verbose() vào giá trị trả về, nếu và chỉ khi có một hàm tạo bản sao hoặc di chuyển. Các nhà xây dựng bản sao hoặc di chuyển không được gọi, mà là elided.

Đây là một chức năng sử dụng NRVO:

Verbose simple_NRVO() { 
    Verbose retval; 
    return retval; 
} 

Đối NRVO xảy ra, tất cả các con đường phải trả lại đối tượng chính xác giống nhau, và bạn không thể lén lút về nó (nếu bạn cast giá trị trở lại một tham chiếu, sau đó trả về tham chiếu đó, sẽ chặn NRVO). Trong trường hợp này, trình biên dịch làm gì là xây dựng đối tượng được đặt tên retval trực tiếp vào vị trí giá trị trả về. Tương tự như RVO, một nhà xây dựng bản sao hoặc di chuyển phải tồn tại, nhưng không được gọi.

Đây là một chức năng mà không sử dụng NRVO:

Verbose simple_no_NRVO(bool b) { 
    Verbose retval1; 
    Verbose retval2; 
    if (b) 
    return retval1; 
    else 
    return retval2; 
} 

như có hai tên đối tượng có thể nó có thể trở lại, nó không thể xây dựng cả trong số họ ở vị trí giá trị trả về, vì vậy nó phải làm một bản sao thực tế. Trong C++ 11, đối tượng được trả về sẽ được ẩn hoàn toàn move d thay vì sao chép, vì nó là một biến cục bộ được trả về từ một hàm trong câu lệnh trả về đơn giản. Vì vậy, có ít nhất đó.

Cuối cùng, có bản sự bỏ bớt ở đầu kia:

Verbose v = simple(); // or simple_RVO, or simple_NRVO, or... 

Khi bạn gọi một hàm, bạn cung cấp cho nó với các đối số của nó, và bạn thông báo cho nó, nơi nó nên đặt giá trị trả về của nó. Người gọi có trách nhiệm làm sạch giá trị trả lại và phân bổ bộ nhớ (trên ngăn xếp) cho nó.

Giao tiếp này được thực hiện theo cách nào đó thông qua quy ước gọi điện, thường ngầm (nghĩa là thông qua con trỏ ngăn xếp).

Dưới nhiều quy ước gọi điện, vị trí mà giá trị trả về có thể được lưu trữ có thể sẽ được sử dụng làm biến cục bộ.

Nói chung, nếu bạn có một biến có dạng:

Verbose v = Verbose(); 

bản sao ngụ ý có thể được elided - Verbose() được xây dựng trực tiếp trong v, chứ không phải là tạm thời được tạo ra sau đó sao chép vào v.Trong cùng một cách, giá trị trả về của simple (hoặc simple_NRVO hoặc bất kỳ thứ gì) có thể được ưu tiên nếu mô hình thời gian chạy của trình biên dịch hỗ trợ nó (và nó thường làm).

Về cơ bản, trang web gọi có thể yêu cầu simple_* đặt giá trị trả về ở một vị trí cụ thể và chỉ cần xử lý điểm đó làm biến cục bộ v.

Lưu ý rằng NRVO và RVO và di chuyển ngầm đều được thực hiện trong hàm và người gọi không cần biết gì về nó.

Tương tự như vậy, eliding tại trang web gọi là tất cả được thực hiện bên ngoài chức năng và nếu quy ước gọi hỗ trợ nó, bạn không cần bất kỳ sự hỗ trợ nào từ phần thân của hàm.

Điều này không nhất thiết phải đúng trong mọi quy ước gọi điện và mô hình thời gian chạy, vì vậy tiêu chuẩn C++ làm cho các tối ưu hóa này trở thành tùy chọn.

+0

nếu chúng ta thay đổi ví dụ về "simple_no_NRVO" để tạo các biến trong các trường hợp if-else khác, thì nrvo có áp dụng không? Verbose simple_no_NRVO (bool b) { \t \t if (b) \t \t { \t \t \t retval1 Verbose; \t \t \t return retval1; \t \t} \t \t khác \t \t { \t \t \t retval2 Verbose; \t \t \t return retval2; \t \t} } –

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