Trong C ++, việc trả về một vectơ từ một hàm có phải là phương pháp không tốt không?


103

Phiên bản ngắn: Thông thường trả về các đối tượng lớn — chẳng hạn như vectơ / mảng — trong nhiều ngôn ngữ lập trình. Phong cách này bây giờ có được chấp nhận trong C ++ 0x không nếu lớp có một hàm tạo chuyển động, hay các lập trình viên C ++ coi nó là kỳ lạ / xấu xí / ghê tởm?

Phiên bản dài: Trong C ++ 0x đây vẫn được coi là hình thức xấu?

std::vector<std::string> BuildLargeVector();
...
std::vector<std::string> v = BuildLargeVector();

Phiên bản truyền thống sẽ trông như thế này:

void BuildLargeVector(std::vector<std::string>& result);
...
std::vector<std::string> v;
BuildLargeVector(v);

Trong phiên bản mới hơn, giá trị được trả về từ BuildLargeVectorlà một rvalue, vì vậy v sẽ được xây dựng bằng cách sử dụng hàm tạo di chuyển của std::vector, giả sử (N) RVO không diễn ra.

Ngay cả trước C ++ 0x, dạng đầu tiên thường "hiệu quả" vì (N) RVO. Tuy nhiên, (N) RVO là tùy theo quyết định của trình biên dịch. Bây giờ chúng ta đã có tham chiếu rvalue nên đảm bảo rằng không có bản sao sâu nào diễn ra.

Chỉnh sửa : Câu hỏi thực sự không phải về tối ưu hóa. Cả hai hình thức được hiển thị đều có hiệu suất gần giống nhau trong các chương trình thế giới thực. Trong khi đó, trong quá khứ, dạng đầu tiên có thể có hiệu suất kém hơn ở mức độ lớn hơn. Kết quả là hình thức đầu tiên là một mùi mã chính trong lập trình C ++ trong một thời gian dài. Không còn nữa, tôi hy vọng?


18
Ai đã từng nói rằng ban đầu là hình thức xấu?
Edward Strange

7
Đó chắc chắn là một mùi mật mã tồi tệ trong “ngày xưa”, đó là nơi tôi đến. :-)
Nate

1
Tôi chắc chắn hy vọng như vậy! Tôi muốn thấy giá trị vượt qua ngày càng phổ biến hơn. :)
sellibitze

Câu trả lời:


73

Dave Abrahams đã có một phân tích khá toàn diện về tốc độ truyền / trả về giá trị .

Câu trả lời ngắn gọn, nếu bạn cần trả về một giá trị thì hãy trả về một giá trị. Không sử dụng tham chiếu đầu ra vì trình biên dịch vẫn làm việc đó. Tất nhiên có những lưu ý, vì vậy bạn nên đọc bài báo đó.


24
"compiler does it anyway": trình biên dịch không cần thiết để làm điều đó == không chắc chắn == ý tưởng tồi (cần chắc chắn 100%). "phân tích toàn diện" Có một vấn đề lớn với phân tích đó - nó dựa vào các tính năng ngôn ngữ không có tài liệu / không chuẩn trong trình biên dịch không xác định ("Mặc dù tiêu chuẩn không bao giờ yêu cầu xử lý bản sao"). Vì vậy, ngay cả khi nó hoạt động, bạn không nên sử dụng nó - hoàn toàn không có bảo đảm rằng nó sẽ hoạt động như dự định, và không có bảo hành rằng mọi trình biên dịch sẽ luôn hoạt động theo cách này. Dựa vào tài liệu này là một phương pháp viết mã tồi, IMO. Ngay cả khi bạn sẽ giảm hiệu suất.
SigTerm

5
@SigTerm: Đó là một nhận xét xuất sắc !!! hầu hết các bài báo tham khảo là quá mơ hồ để thậm chí xem xét để sử dụng trong sản xuất. Mọi người nghĩ bất cứ điều gì một tác giả viết cuốn sách Red In-Depth đều là phúc âm và cần được tôn trọng mà không cần suy nghĩ hoặc phân tích thêm. ATM không có trình biên dịch nào trên thị trường cung cấp copy-elison đa dạng như các ví dụ mà Abrahams sử dụng trong bài báo.
Hippicoder

13
@SigTerm, có rất nhiều thứ mà trình biên dịch không bắt buộc phải làm, nhưng bạn cho rằng nó vẫn làm được. Trình biên dịch không được "yêu cầu" để thay đổi x / 2để x >> 1cho ints, nhưng bạn cho rằng nó sẽ. Tiêu chuẩn này cũng không nói gì về cách các trình biên dịch được yêu cầu để triển khai các tham chiếu, nhưng bạn giả sử rằng chúng được xử lý hiệu quả bằng cách sử dụng con trỏ. Tiêu chuẩn cũng không nói gì về v-table, vì vậy bạn cũng không thể chắc chắn rằng các lệnh gọi hàm ảo là hiệu quả. Về cơ bản, đôi khi bạn cần đặt niềm tin vào trình biên dịch.
Peter Alexander

16
@Sig: Thực sự rất ít được đảm bảo ngoại trừ đầu ra thực tế của chương trình của bạn. Nếu bạn muốn chắc chắn 100% về những gì sẽ xảy ra trong 100% thời gian, thì bạn nên chuyển hoàn toàn sang một ngôn ngữ khác.
Dennis Zickefoose

6
@SigTerm: Tôi làm việc trên "tình huống thực tế". Tôi kiểm tra những gì trình biên dịch làm và hoạt động với điều đó. Không có "có thể hoạt động chậm hơn". Nó chỉ đơn giản là không hoạt động chậm hơn bởi vì trình biên dịch KHÔNG thực hiện RVO, cho dù tiêu chuẩn có yêu cầu nó hay không. Không có ifs, buts, hoặc maybes, đó chỉ là sự thật đơn giản.
Peter Alexander

37

Ít nhất là IMO, đó thường là một ý tưởng tồi, nhưng không phải vì lý do hiệu quả. Đó là một ý tưởng tồi vì hàm được đề cập thường được viết dưới dạng một thuật toán chung tạo ra đầu ra của nó thông qua một trình lặp. Hầu như bất kỳ mã nào chấp nhận hoặc trả về một vùng chứa thay vì hoạt động trên trình vòng lặp sẽ được coi là đáng ngờ.

Đừng hiểu sai ý tôi: đôi khi việc truyền xung quanh các đối tượng giống như tập hợp (ví dụ: chuỗi) rất hợp lý nhưng đối với ví dụ được trích dẫn, tôi coi việc chuyển hoặc trả về vectơ là một ý tưởng tồi.


6
Vấn đề với cách tiếp cận trình lặp là nó yêu cầu bạn tạo các hàm và phương thức được tạo mẫu, ngay cả khi đã biết kiểu phần tử tập hợp. Điều này thật khó chịu và khi phương pháp được đề cập là ảo, không thể thực hiện được. Lưu ý, tôi không đồng ý với câu trả lời của bạn, nhưng trên thực tế, nó trở nên hơi rườm rà trong C ++.
jon-hanson

22
Tôi phải không đồng ý. Sử dụng trình vòng lặp cho đầu ra đôi khi là thích hợp, nhưng nếu bạn không viết một thuật toán chung, các giải pháp chung thường cung cấp chi phí không thể tránh khỏi mà khó có thể biện minh. Cả về độ phức tạp của mã và hiệu suất thực tế.
Dennis Zickefoose

1
@Dennis: Tôi phải nói rằng kinh nghiệm của tôi hoàn toàn ngược lại: Tôi viết rất nhiều thứ dưới dạng mẫu ngay cả khi tôi biết trước các loại liên quan, bởi vì làm như vậy đơn giản hơn và cải thiện hiệu suất.
Jerry Coffin

9
Cá nhân tôi trả lại một container. Ý định rõ ràng, mã dễ dàng hơn, tôi không quan tâm nhiều đến hiệu suất khi tôi viết nó (tôi chỉ tránh bi quan sớm). Tôi không chắc liệu việc sử dụng trình lặp đầu ra có làm cho ý định của tôi rõ ràng hơn hay không ... và tôi cần mã không phải mẫu càng nhiều càng tốt, bởi vì trong một dự án lớn, các phụ thuộc sẽ giết chết sự phát triển.
Matthieu M.

1
@Dennis: Tôi sẽ xác định điều đó về mặt khái niệm, bạn không bao giờ nên "xây dựng một vùng chứa hơn là ghi vào một phạm vi." Một thùng chứa chỉ là vậy - một thùng chứa. Mối quan tâm của bạn (và mối quan tâm về mã của bạn) phải là nội dung, không phải vùng chứa.
Jerry Coffin

18

Ý chính là:

Copy Elision và RVO có thể tránh được "những bản sao đáng sợ" (trình biên dịch không bắt buộc phải triển khai những tối ưu hóa này và trong một số trường hợp, nó không thể được áp dụng)

Tham chiếu C ++ 0x RValue cho phép triển khai chuỗi / vectơ đảm bảo điều đó.

Nếu bạn có thể từ bỏ các trình biên dịch / triển khai STL cũ hơn, hãy trả về các vectơ một cách tự do (và đảm bảo các đối tượng của riêng bạn cũng hỗ trợ nó). Nếu cơ sở mã của bạn cần hỗ trợ các trình biên dịch "thấp hơn", hãy giữ nguyên kiểu cũ.

Thật không may, điều đó có ảnh hưởng lớn đến giao diện của bạn. Nếu C ++ 0x không phải là một tùy chọn và bạn cần đảm bảo, thay vào đó bạn có thể sử dụng các đối tượng được tính là tham chiếu hoặc sao chép khi ghi trong một số trường hợp. Tuy nhiên, chúng có nhược điểm với đa luồng.

(Tôi ước chỉ một câu trả lời trong C ++ sẽ đơn giản và dễ hiểu và không có điều kiện).


11

Trên thực tế, kể từ khi C ++ 11, chi phí của việc sao chép các std::vectorbiến mất trong hầu hết các trường hợp.

Tuy nhiên, cần lưu ý rằng chi phí xây dựng vectơ mới (sau đó hủy nó) vẫn tồn tại và việc sử dụng các tham số đầu ra thay vì trả về theo giá trị vẫn hữu ích khi bạn muốn sử dụng lại dung lượng của vectơ. Điều này được ghi lại như một ngoại lệ trong F.20 của Nguyên tắc cốt lõi C ++.

Hãy so sánh:

std::vector<int> BuildLargeVector1(size_t vecSize) {
    return std::vector<int>(vecSize, 1);
}

với:

void BuildLargeVector2(/*out*/ std::vector<int>& v, size_t vecSize) {
    v.assign(vecSize, 1);
}

Bây giờ, giả sử chúng ta cần gọi các phương thức này numItertrong một vòng lặp chặt chẽ và thực hiện một số hành động. Ví dụ, hãy tính tổng của tất cả các phần tử.

Sử dụng BuildLargeVector1, bạn sẽ làm:

size_t sum1 = 0;
for (int i = 0; i < numIter; ++i) {
    std::vector<int> v = BuildLargeVector1(vecSize);
    sum1 = std::accumulate(v.begin(), v.end(), sum1);
}

Sử dụng BuildLargeVector2, bạn sẽ làm:

size_t sum2 = 0;
std::vector<int> v;
for (int i = 0; i < numIter; ++i) {
    BuildLargeVector2(/*out*/ v, vecSize);
    sum2 = std::accumulate(v.begin(), v.end(), sum2);
}

Trong ví dụ đầu tiên, có nhiều phân bổ động / phân bổ động không cần thiết xảy ra, chúng được ngăn chặn trong ví dụ thứ hai bằng cách sử dụng tham số đầu ra theo cách cũ, sử dụng lại bộ nhớ đã được cấp phát. Việc tối ưu hóa này có đáng thực hiện hay không phụ thuộc vào chi phí tương đối của việc phân bổ / phân bổ giao dịch so với chi phí tính toán / thay đổi các giá trị.

Điểm chuẩn

Hãy chơi với các giá trị của vecSizenumIter. Chúng tôi sẽ giữ vecSize * numIter không đổi để "trên lý thuyết", nó sẽ mất cùng một thời gian (= có cùng một số lần gán và bổ sung, với cùng giá trị) và chênh lệch thời gian chỉ có thể đến từ chi phí phân bổ, phân bổ giao dịch và sử dụng bộ nhớ đệm tốt hơn.

Cụ thể hơn, hãy sử dụng vecSize * numIter = 2 ^ 31 = 2147483648, vì tôi có 16GB RAM và con số này đảm bảo rằng không quá 8GB được phân bổ (sizeof (int) = 4), đảm bảo rằng tôi không hoán đổi sang đĩa ( tất cả các chương trình khác đã bị đóng, tôi có ~ 15GB khả dụng khi chạy thử nghiệm).

Đây là mã:

#include <chrono>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <vector>

class Timer {
    using clock = std::chrono::steady_clock;
    using seconds = std::chrono::duration<double>;
    clock::time_point t_;

public:
    void tic() { t_ = clock::now(); }
    double toc() const { return seconds(clock::now() - t_).count(); }
};

std::vector<int> BuildLargeVector1(size_t vecSize) {
    return std::vector<int>(vecSize, 1);
}

void BuildLargeVector2(/*out*/ std::vector<int>& v, size_t vecSize) {
    v.assign(vecSize, 1);
}

int main() {
    Timer t;

    size_t vecSize = size_t(1) << 31;
    size_t numIter = 1;

    std::cout << std::setw(10) << "vecSize" << ", "
              << std::setw(10) << "numIter" << ", "
              << std::setw(10) << "time1" << ", "
              << std::setw(10) << "time2" << ", "
              << std::setw(10) << "sum1" << ", "
              << std::setw(10) << "sum2" << "\n";

    while (vecSize > 0) {

        t.tic();
        size_t sum1 = 0;
        {
            for (int i = 0; i < numIter; ++i) {
                std::vector<int> v = BuildLargeVector1(vecSize);
                sum1 = std::accumulate(v.begin(), v.end(), sum1);
            }
        }
        double time1 = t.toc();

        t.tic();
        size_t sum2 = 0;
        {
            std::vector<int> v;
            for (int i = 0; i < numIter; ++i) {
                BuildLargeVector2(/*out*/ v, vecSize);
                sum2 = std::accumulate(v.begin(), v.end(), sum2);
            }
        } // deallocate v
        double time2 = t.toc();

        std::cout << std::setw(10) << vecSize << ", "
                  << std::setw(10) << numIter << ", "
                  << std::setw(10) << std::fixed << time1 << ", "
                  << std::setw(10) << std::fixed << time2 << ", "
                  << std::setw(10) << sum1 << ", "
                  << std::setw(10) << sum2 << "\n";

        vecSize /= 2;
        numIter *= 2;
    }

    return 0;
}

Và đây là kết quả:

$ g++ -std=c++11 -O3 main.cpp && ./a.out
   vecSize,    numIter,      time1,      time2,       sum1,       sum2
2147483648,          1,   2.360384,   2.356355, 2147483648, 2147483648
1073741824,          2,   2.365807,   1.732609, 2147483648, 2147483648
 536870912,          4,   2.373231,   1.420104, 2147483648, 2147483648
 268435456,          8,   2.383480,   1.261789, 2147483648, 2147483648
 134217728,         16,   2.395904,   1.179340, 2147483648, 2147483648
  67108864,         32,   2.408513,   1.131662, 2147483648, 2147483648
  33554432,         64,   2.416114,   1.097719, 2147483648, 2147483648
  16777216,        128,   2.431061,   1.060238, 2147483648, 2147483648
   8388608,        256,   2.448200,   0.998743, 2147483648, 2147483648
   4194304,        512,   0.884540,   0.875196, 2147483648, 2147483648
   2097152,       1024,   0.712911,   0.716124, 2147483648, 2147483648
   1048576,       2048,   0.552157,   0.603028, 2147483648, 2147483648
    524288,       4096,   0.549749,   0.602881, 2147483648, 2147483648
    262144,       8192,   0.547767,   0.604248, 2147483648, 2147483648
    131072,      16384,   0.537548,   0.603802, 2147483648, 2147483648
     65536,      32768,   0.524037,   0.600768, 2147483648, 2147483648
     32768,      65536,   0.526727,   0.598521, 2147483648, 2147483648
     16384,     131072,   0.515227,   0.599254, 2147483648, 2147483648
      8192,     262144,   0.540541,   0.600642, 2147483648, 2147483648
      4096,     524288,   0.495638,   0.603396, 2147483648, 2147483648
      2048,    1048576,   0.512905,   0.609594, 2147483648, 2147483648
      1024,    2097152,   0.548257,   0.622393, 2147483648, 2147483648
       512,    4194304,   0.616906,   0.647442, 2147483648, 2147483648
       256,    8388608,   0.571628,   0.629563, 2147483648, 2147483648
       128,   16777216,   0.846666,   0.657051, 2147483648, 2147483648
        64,   33554432,   0.853286,   0.724897, 2147483648, 2147483648
        32,   67108864,   1.232520,   0.851337, 2147483648, 2147483648
        16,  134217728,   1.982755,   1.079628, 2147483648, 2147483648
         8,  268435456,   3.483588,   1.673199, 2147483648, 2147483648
         4,  536870912,   5.724022,   2.150334, 2147483648, 2147483648
         2, 1073741824,  10.285453,   3.583777, 2147483648, 2147483648
         1, 2147483648,  20.552860,   6.214054, 2147483648, 2147483648

Kết quả điểm chuẩn

(Intel i7-7700K @ 4,20GHz; 16GB DDR4 2400Mhz; Kubuntu 18,04)

Ký hiệu: mem (v) = v.size () * sizeof (int) = v.size () * 4 trên nền tảng của tôi.

Không có gì đáng ngạc nhiên, khi numIter = 1(tức là, mem (v) = 8GB), thời gian hoàn toàn giống hệt nhau. Thật vậy, trong cả hai trường hợp, chúng tôi chỉ cấp phát một lần một vector khổng lồ 8GB trong bộ nhớ. Điều này cũng chứng minh rằng không có bản sao nào xảy ra khi sử dụng BuildLargeVector1 (): Tôi sẽ không có đủ RAM để sao chép!

Khi numIter = 2sử dụng lại công suất vectơ thay vì phân bổ lại vectơ thứ hai nhanh hơn 1,37 lần.

Khi numIter = 256sử dụng lại dung lượng vectơ (thay vì phân bổ / phân bổ một vectơ lặp đi lặp lại 256 lần ...) nhanh hơn 2,45 lần :)

Chúng ta có thể nhận thấy rằng time1 khá nhiều không đổi từ numIter = 1đến numIter = 256, có nghĩa là việc phân bổ một vectơ khổng lồ 8GB là khá tốn kém như phân bổ 256 vectơ 32MB. Tuy nhiên, việc phân bổ một vectơ khổng lồ 8GB rõ ràng là đắt hơn so với phân bổ một vectơ 32MB, vì vậy việc sử dụng lại dung lượng của vectơ sẽ mang lại hiệu suất tăng.

Từ numIter = 512(mem (v) = 16MB) đến numIter = 8M(mem (v) = 1kB) là điểm hấp dẫn: cả hai phương pháp đều nhanh như nhau và nhanh hơn tất cả các kết hợp khác của numIter và vecSize. Điều này có thể liên quan đến thực tế là kích thước bộ nhớ cache L3 của bộ xử lý của tôi là 8MB, do đó vector khá phù hợp hoàn toàn trong bộ nhớ cache. Tôi không thực sự giải thích tại sao sự nhảy vọt đột ngột time1là đối với mem (v) = 16MB, nó có vẻ hợp lý hơn nếu xảy ra ngay sau đó, khi mem (v) = 8MB. Lưu ý rằng đáng ngạc nhiên là ở điểm ngọt ngào này, việc không sử dụng lại dung lượng trên thực tế sẽ nhanh hơn một chút! Tôi không thực sự giải thích điều này.

Khi numIter > 8Mmọi thứ bắt đầu trở nên tồi tệ. Cả hai phương pháp đều chậm hơn nhưng trả về vectơ theo giá trị thậm chí còn chậm hơn. Trong trường hợp xấu nhất, với một vectơ chỉ chứa một đơn lẻ int, khả năng tái sử dụng thay vì trả về theo giá trị sẽ nhanh hơn 3,3 lần. Có lẽ, điều này là do chi phí cố định của malloc () bắt đầu chiếm ưu thế.

Lưu ý rằng đường cong cho time2 mượt mà hơn đường cong cho time1: không chỉ sử dụng lại công suất vectơ nói chung nhanh hơn, mà có lẽ quan trọng hơn, nó dễ dự đoán hơn .

Cũng lưu ý rằng ở điểm đáng chú ý, chúng tôi có thể thực hiện 2 tỷ phép cộng số nguyên 64bit trong ~ 0,5 giây, khá tối ưu trên bộ xử lý 64bit 4,2Ghz. Chúng tôi có thể làm tốt hơn bằng cách tính toán song song để sử dụng tất cả 8 lõi (thử nghiệm ở trên chỉ sử dụng một lõi tại một thời điểm, mà tôi đã xác minh bằng cách chạy lại thử nghiệm trong khi theo dõi việc sử dụng CPU). Hiệu suất tốt nhất đạt được khi mem (v) = 16kB, là thứ tự độ lớn của bộ đệm L1 (bộ đệm dữ liệu L1 cho i7-7700K là 4x32kB).

Tất nhiên, sự khác biệt ngày càng ít liên quan hơn khi bạn thực sự phải tính toán nhiều hơn trên dữ liệu. Dưới đây là kết quả nếu chúng tôi thay thế sum = std::accumulate(v.begin(), v.end(), sum);bằng for (int k : v) sum += std::sqrt(2.0*k);:

Điểm chuẩn 2

Kết luận

  1. Sử dụng các tham số đầu ra thay vì trả về theo giá trị có thể mang lại hiệu suất tăng bằng cách sử dụng lại công suất.
  2. Trên máy tính để bàn hiện đại, điều này dường như chỉ áp dụng cho các vectơ lớn (> 16MB) và vectơ nhỏ (<1kB).
  3. Tránh phân bổ hàng triệu / tỷ vectơ nhỏ (<1kB). Nếu có thể, hãy tái sử dụng công suất, hoặc tốt hơn, hãy thiết kế kiến ​​trúc của bạn theo cách khác.

Kết quả có thể khác nhau trên các nền tảng khác. Như thường lệ, nếu hiệu suất quan trọng, hãy viết điểm chuẩn cho trường hợp sử dụng cụ thể của bạn.


6

Tôi vẫn nghĩ đó là một thực tiễn không tốt nhưng điều đáng chú ý là nhóm của tôi sử dụng MSVC 2008 và GCC 4.1, vì vậy chúng tôi không sử dụng các trình biên dịch mới nhất.

Trước đây, rất nhiều điểm nóng được hiển thị trong vtune với MSVC 2008 bị sao chép chuỗi. Chúng tôi có mã như thế này:

String Something::id() const
{
    return valid() ? m_id: "";
}

... lưu ý rằng chúng tôi đã sử dụng loại Chuỗi của riêng mình (điều này là bắt buộc vì chúng tôi đang cung cấp một bộ công cụ phát triển phần mềm nơi người viết plugin có thể sử dụng các trình biên dịch khác nhau và do đó các triển khai khác nhau, không tương thích của std :: string / std :: wstring).

Tôi đã thực hiện một thay đổi đơn giản để đáp ứng với phiên lập hồ sơ lấy mẫu biểu đồ cuộc gọi hiển thị Chuỗi :: Chuỗi (const String &) đang chiếm một lượng thời gian đáng kể. Các phương thức như trong ví dụ trên là những phương thức đóng góp lớn nhất (thực sự phiên lập hồ sơ cho thấy cấp phát bộ nhớ và phân bổ giao dịch là một trong những điểm nóng lớn nhất, với hàm tạo bản sao chuỗi là người đóng góp chính cho việc phân bổ).

Thay đổi tôi đã thực hiện rất đơn giản:

static String null_string;
const String& Something::id() const
{
    return valid() ? m_id: null_string;
}

Tuy nhiên, điều này đã tạo ra một thế giới khác biệt! Điểm phát sóng đã biến mất trong các phiên lập hồ sơ tiếp theo và ngoài việc này, chúng tôi thực hiện rất nhiều thử nghiệm đơn vị kỹ lưỡng để theo dõi hiệu suất ứng dụng của chúng tôi. Tất cả các loại thời gian kiểm tra hiệu suất đều giảm đáng kể sau những thay đổi đơn giản này.

Kết luận: chúng tôi không sử dụng các trình biên dịch mới nhất tuyệt đối, nhưng chúng tôi dường như vẫn không thể phụ thuộc vào trình biên dịch tối ưu hóa việc sao chép để trả về theo giá trị một cách đáng tin cậy (ít nhất là không phải trong mọi trường hợp). Điều đó có thể không đúng với những người sử dụng trình biên dịch mới hơn như MSVC 2010. Tôi đang mong chờ khi nào chúng ta có thể sử dụng C ++ 0x và chỉ cần sử dụng các tham chiếu rvalue và không bao giờ phải lo lắng rằng chúng ta đang bi quan mã của mình bằng cách trả về phức tạp các lớp theo giá trị.

[Chỉnh sửa] Như Nate đã chỉ ra, RVO áp dụng để trả về các khoảng thời gian tạm thời được tạo bên trong một hàm. Trong trường hợp của tôi, không có khoảng thời gian tạm thời nào như vậy (ngoại trừ nhánh không hợp lệ nơi chúng tôi tạo một chuỗi rỗng) và do đó RVO sẽ không được áp dụng.


3
Đó là vấn đề: RVO phụ thuộc vào trình biên dịch, nhưng trình biên dịch C ++ 0x phải sử dụng ngữ nghĩa di chuyển nếu nó quyết định không sử dụng RVO (giả sử có một phương thức khởi tạo di chuyển). Sử dụng toán tử tam đồ đánh bại RVO. Xem cpp-next.com/archive/2009/09/move-it-with-rvalue-references mà Peter đã đề cập đến. Nhưng ví dụ của bạn dù sao cũng không đủ điều kiện cho ngữ nghĩa di chuyển vì bạn không trả lại tạm thời.
Nate

@ Stinky472: Trả lại thành viên theo giá trị luôn chậm hơn tham chiếu. Tham chiếu Rvalue sẽ vẫn chậm hơn so với việc trả về một tham chiếu cho thành viên ban đầu (nếu người gọi có thể lấy tham chiếu thay vì cần một bản sao). Ngoài ra, vẫn có nhiều lần bạn có thể lưu các tham chiếu quá giá trị do bạn có ngữ cảnh. Ví dụ, bạn có thể làm Chuỗi newstring; newstring.resize (string1.size () + string2.size () + ...); newstring + = string1; newstring + = string2; vv Đây vẫn là một khoản tiết kiệm đáng kể so với các giá trị.
Puppy

@DeadMG tiết kiệm đáng kể toán tử nhị phân + ngay cả với trình biên dịch C ++ 0x triển khai RVO? Nếu vậy thì thật đáng tiếc. Sau đó, một lần nữa ý nghĩa makse đó vì cuối cùng chúng ta vẫn phải tạo một tạm thời để tính toán chuỗi được nối trong khi + = có thể nối trực tiếp với chuỗi mới.
stinky472,

Làm thế nào về một trường hợp như: string newstr = str1 + str2; Trên một trình biên dịch thực hiện ngữ nghĩa di chuyển, có vẻ như nó sẽ nhanh bằng hoặc thậm chí nhanh hơn: string newstr; newstr + = str1; newstr + = str2; Không có dự trữ, có thể nói như vậy (tôi cho rằng bạn muốn đặt trước thay vì thay đổi kích thước).
stinky472

5
@Nate: Tôi nghĩ rằng bạn đang bối rối trigraphs như <::hoặc ??!với các nhà khai thác có điều kiện ?: (đôi khi được gọi là nhà điều hành ternary ).
fredoverflow

3

Chỉ cần chú ý một chút: việc trả về mảng từ các hàm không phổ biến trong nhiều ngôn ngữ lập trình. Trong hầu hết chúng, một tham chiếu đến mảng được trả về. Trong C ++, phép tương tự gần nhất sẽ trả vềboost::shared_array


4
@Billy: std :: vector là một kiểu giá trị có ngữ nghĩa sao chép. Tiêu chuẩn C ++ hiện tại không đưa ra đảm bảo rằng (N) RVO đã từng được áp dụng và trong thực tế, có nhiều trường hợp thực tế không như vậy.
Nemanja Trifunovic

3
@Billy: Một lần nữa, có một số tình huống rất thực tế mà ngay cả những trình biên dịch mới nhất không áp dụng NRVO: efnetcpp.org/wiki/Return_value_optimization#Named_RVO
Nemanja Trifunović

3
@Billy ONeal: 99% là không đủ, bạn cần 100%. Định luật Murphy - "nếu điều gì đó có thể xảy ra sai, nó sẽ xảy ra". Không chắc chắn là tốt nếu bạn đang xử lý một số loại logic mờ, nhưng nó không phải là một ý tưởng tốt để viết phần mềm truyền thống. Nếu thậm chí có 1% khả năng mã không hoạt động theo cách bạn nghĩ, thì bạn nên hy vọng mã này sẽ đưa ra một lỗi nghiêm trọng khiến bạn bị sa thải. Thêm vào đó, nó không phải là một tính năng tiêu chuẩn. Sử dụng các tính năng không có giấy tờ là một ý tưởng tồi - nếu trong một năm kể từ khi biết trình biên dịch sẽ giảm tính năng (nó không được yêu cầu theo tiêu chuẩn, phải không?), Bạn sẽ là người gặp rắc rối.
SigTerm

4
@SigTerm: Nếu chúng ta nói về tính đúng đắn của hành vi, tôi sẽ đồng ý với bạn. Tuy nhiên, chúng ta đang nói về tối ưu hóa hiệu suất. Những thứ như vậy là tốt với ít hơn 100% chắc chắn.
Billy ONeal

2
@Nemanja: Tôi không thấy điều gì đang được "dựa vào" ở đây. Ứng dụng của bạn chạy giống nhau cho dù RVO hay NRVO được sử dụng. Nếu chúng được sử dụng, nó sẽ chạy nhanh hơn. Nếu ứng dụng của bạn quá chậm trên một nền tảng cụ thể và bạn đã truy nguyên nó để trả về việc sao chép giá trị, thì bằng mọi cách hãy thay đổi nó, nhưng điều đó không thay đổi thực tế rằng phương pháp hay nhất vẫn là sử dụng giá trị trả về. Nếu bạn thực sự cần đảm bảo không xảy ra sao chép, hãy bọc vectơ trong một shared_ptrvà gọi nó là một ngày.
Billy ONeal

2

Nếu hiệu suất là một vấn đề thực sự, bạn nên nhận ra rằng ngữ nghĩa di chuyển không phải lúc nào cũng nhanh hơn sao chép. Ví dụ: nếu bạn có một chuỗi sử dụng tính năng tối ưu hóa chuỗi nhỏ thì đối với các chuỗi nhỏ, hàm tạo di chuyển phải thực hiện cùng một lượng công việc như một hàm tạo sao chép thông thường.


1
NRVO không biến mất chỉ vì các hàm tạo chuyển động đã được thêm vào.
Billy ONeal

1
@Billy, đúng nhưng không thích hợp, câu hỏi được đã C ++ 0x thay đổi thực tiễn tốt nhất và NRVO đã không thay đổi do C ++ 0x
Motti
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.