C ++ hiện đại có thể giúp bạn thực hiện miễn phí không?


205

Đôi khi người ta cho rằng C ++ 11/14 có thể giúp bạn tăng hiệu suất ngay cả khi chỉ biên dịch mã C ++ 98. Sự biện minh thường là dọc theo dòng ngữ nghĩa di chuyển, vì trong một số trường hợp, các hàm tạo giá trị được tạo tự động hoặc bây giờ là một phần của STL. Bây giờ tôi đang tự hỏi liệu những trường hợp này trước đây thực sự đã được xử lý bởi RVO hoặc tối ưu hóa trình biên dịch tương tự.

Câu hỏi của tôi là nếu bạn có thể cho tôi một ví dụ thực tế về một đoạn mã C ++ 98, mà không cần sửa đổi, sẽ chạy nhanh hơn bằng cách sử dụng trình biên dịch hỗ trợ các tính năng ngôn ngữ mới. Tôi hiểu rằng một trình biên dịch tuân thủ tiêu chuẩn không bắt buộc phải thực hiện việc sao chép và chỉ vì lý do đó, ngữ nghĩa di chuyển có thể mang lại tốc độ, nhưng tôi muốn xem một trường hợp ít bệnh lý hơn, nếu bạn muốn.

EDIT: Để rõ ràng, tôi không hỏi liệu trình biên dịch mới có nhanh hơn trình biên dịch cũ không, nhưng nếu có mã theo đó thêm -std = c ++ 14 vào cờ trình biên dịch của tôi thì nó sẽ chạy nhanh hơn (tránh các bản sao, nhưng nếu bạn có thể đến với bất cứ điều gì khác ngoài di chuyển ngữ nghĩa, tôi cũng sẽ quan tâm)


3
Hãy nhớ rằng tối ưu hóa sao chép và trả về giá trị được thực hiện khi xây dựng một đối tượng mới bằng cách sử dụng một hàm tạo sao chép. Tuy nhiên, trong một toán tử gán sao chép, không có cuộc bầu chọn sao chép (làm sao có thể, vì trình biên dịch không biết phải làm gì với một đối tượng đã được xây dựng không phải là tạm thời). Do đó, trong trường hợp đó, C ++ 11/14 thắng lớn, bằng cách cho bạn khả năng sử dụng toán tử gán chuyển. Mặc dù về câu hỏi của bạn, tôi không nghĩ mã C ++ 98 sẽ nhanh hơn nếu được biên dịch bởi trình biên dịch C ++ 11/14, có thể nó nhanh hơn vì trình biên dịch mới hơn.
vsoftco

27
Ngoài ra mã sử dụng thư viện chuẩn có khả năng nhanh hơn, ngay cả khi bạn làm cho nó tương thích hoàn toàn với C ++ 98, bởi vì trong C ++ 11/14, thư viện bên dưới sử dụng ngữ nghĩa di chuyển nội bộ khi có thể. Vì vậy, mã trông giống hệt trong C ++ 98 và C ++ 11/14 sẽ nhanh hơn (có thể) trong trường hợp sau, bất cứ khi nào bạn sử dụng các đối tượng thư viện chuẩn như vectơ, danh sách, v.v. và di chuyển ngữ nghĩa sẽ tạo ra sự khác biệt.
vsoftco

1
@vsoftco, đó là loại tình huống tôi đã ám chỉ, nhưng không thể đưa ra một ví dụ: Từ những gì tôi nhớ nếu tôi phải xác định hàm tạo sao chép, hàm tạo di chuyển sẽ không được tạo tự động, khiến chúng tôi phải Các lớp rất đơn giản, nơi RVO, tôi nghĩ, luôn luôn hoạt động. Một ngoại lệ có thể là một cái gì đó kết hợp với các thùng chứa STL, trong đó các hàm tạo giá trị được tạo bởi người triển khai thư viện (có nghĩa là tôi sẽ không phải thay đổi bất cứ điều gì trong mã để nó sử dụng di chuyển).
alarge

các lớp không cần đơn giản để không có hàm tạo sao chép. C ++ phát triển mạnh về ngữ nghĩa giá trị và hàm tạo sao chép, toán tử gán, hàm hủy, v.v. nên là ngoại lệ.
sp2danny

1
@Eric Cảm ơn bạn đã liên kết, thật thú vị. Tuy nhiên, khi đã nhanh chóng xem xét nó, những lợi thế về tốc độ trong nó dường như chủ yếu đến từ việc thêm std::movevà di chuyển các hàm tạo (sẽ yêu cầu sửa đổi mã hiện có). Điều duy nhất thực sự liên quan đến câu hỏi của tôi là câu "Bạn có được lợi thế tốc độ ngay lập tức chỉ bằng cách biên dịch lại", không được hỗ trợ bởi bất kỳ ví dụ nào (nó đề cập đến STL trên cùng một slide, như tôi đã làm trong câu hỏi của mình, nhưng không có gì cụ thể ). Tôi đã yêu cầu một số ví dụ. Nếu tôi đọc sai các slide, hãy cho tôi biết.
alarge

Câu trả lời:


221

Tôi biết 5 loại chung trong đó biên dịch lại trình biên dịch C ++ 03 vì C ++ 11 có thể gây ra sự gia tăng hiệu suất không giới hạn mà thực tế không liên quan đến chất lượng thực hiện. Đây là tất cả các biến thể của ngữ nghĩa di chuyển.

std::vector tái phân bổ

struct bar{
  std::vector<int> data;
};
std::vector<bar> foo(1);
foo.back().data.push_back(3);
foo.reserve(10); // two allocations and a delete occur in C++03

mỗi khi foobộ đệm được phân bổ lại trong C ++ 03, nó được sao chép mọi thứ vectortrong bar.

Trong C ++ 11, nó thay vào đó di chuyển bar::datas, về cơ bản là miễn phí.

Trong trường hợp này, điều này phụ thuộc vào tối ưu hóa bên trong stdcontainer vector. Trong mọi trường hợp dưới đây, việc sử dụng các stdthùng chứa chỉ là vì chúng là các đối tượng C ++ có movengữ nghĩa hiệu quả trong C ++ 11 "tự động" khi bạn nâng cấp trình biên dịch. Các đối tượng không chặn nó chứa stdcontainer cũng thừa hưởng các hàm tạo được cải tiến tự động move.

Thất bại NRVO

Khi NRVO (tối ưu hóa giá trị trả về được đặt tên) không thành công, trong C ++ 03, nó rơi trở lại vào bản sao, trên C ++ 11, nó rơi trở lại khi di chuyển. Thất bại của NRVO rất dễ dàng:

std::vector<int> foo(int count){
  std::vector<int> v; // oops
  if (count<=0) return std::vector<int>();
  v.reserve(count);
  for(int i=0;i<count;++i)
    v.push_back(i);
  return v;
}

hoặc thậm chí:

std::vector<int> foo(bool which) {
  std::vector<int> a, b;
  // do work, filling a and b, using the other for calculations
  if (which)
    return a;
  else
    return b;
}

Chúng ta có ba giá trị - giá trị trả về và hai giá trị khác nhau trong hàm. Elision cho phép các giá trị trong hàm được 'hợp nhất' với giá trị trả về, nhưng không cho nhau. Cả hai đều không thể được hợp nhất với giá trị trả về mà không hợp nhất với nhau.

Vấn đề cơ bản là cuộc bầu chọn NRVO rất mong manh và mã với các thay đổi không ở gần returntrang web có thể đột nhiên giảm hiệu suất lớn tại điểm đó mà không có chẩn đoán phát ra. Trong hầu hết các trường hợp thất bại NRVO, C ++ 11 kết thúc bằng a move, trong khi C ++ 03 kết thúc bằng một bản sao.

Trả về một đối số hàm

Elision cũng là không thể ở đây:

std::set<int> func(std::set<int> in){
  return in;
}

trong C ++ 11 này là rẻ: trong C ++ 03 không có cách nào để tránh việc sao chép. Các đối số cho các hàm không thể được tách biệt với giá trị trả về, vì thời gian tồn tại và vị trí của tham số và giá trị trả về được quản lý bởi mã gọi.

Tuy nhiên, C ++ 11 có thể chuyển từ cái này sang cái khác. (Trong một ví dụ đồ chơi ít hơn, một cái gì đó có thể được thực hiện cho set).

push_back hoặc là insert

Cuối cùng, việc bỏ trốn vào các thùng chứa không xảy ra: nhưng C ++ 11 quá tải các toán tử chèn di chuyển giá trị, giúp lưu các bản sao.

struct whatever {
  std::string data;
  int count;
  whatever( std::string d, int c ):data(d), count(c) {}
};
std::vector<whatever> v;
v.push_back( whatever("some long string goes here", 3) );

trong C ++ 03, tạm thời whateverđược tạo, sau đó nó được sao chép vào vector v. 2 std::stringbộ đệm được phân bổ, mỗi bộ đệm có dữ liệu giống hệt nhau và một bộ đệm bị loại bỏ.

Trong C ++ 11, tạm thời whateverđược tạo. Quá whatever&& push_backtải sau đó moves mà tạm thời vào vector v. Một std::stringbộ đệm được phân bổ, và di chuyển vào vector. Một sản phẩm nào std::stringđược loại bỏ.

Bài tập

Lấy cắp từ câu trả lời của @ Jarod42 bên dưới.

Elision không thể xảy ra với sự phân công, nhưng di chuyển từ có thể.

std::set<int> some_function();

std::set<int> some_value;

// code

some_value = some_function();

ở đây some_functiontrả về một ứng cử viên để tách biệt, nhưng vì nó không được sử dụng để xây dựng một đối tượng trực tiếp, nên nó không thể bị loại bỏ. Trong C ++ 03, các kết quả trên trong nội dung của tạm thời được sao chép vào some_value. Trong C ++ 11, nó được chuyển vào some_value, về cơ bản là miễn phí.


Để có hiệu ứng đầy đủ ở trên, bạn cần một trình biên dịch tổng hợp các hàm tạo di chuyển và gán cho bạn.

MSVC 2013 triển khai các hàm tạo di chuyển trong stdcác thùng chứa, nhưng không tổng hợp các hàm tạo di chuyển trên các loại của bạn.

Vì vậy, loại có chứa std::vectors và tương tự không có được những cải tiến như vậy trong MSVC2013, nhưng sẽ bắt đầu nhận được chúng trong MSVC2015.

clang và gcc từ lâu đã thực hiện các hàm tạo di chuyển ngầm. Trình biên dịch 2013 của Intel sẽ hỗ trợ tạo các hàm tạo di chuyển ngầm nếu bạn vượt qua -Qoption,cpp,--gen_move_operations(mặc định họ không làm điều đó trong nỗ lực tương thích chéo với MSVC2013).


1
@alange có. Nhưng để một constructor di chuyển hiệu quả hơn nhiều lần so với một constructor sao chép, nó thường phải di chuyển các tài nguyên thay vì sao chép chúng. Nếu không viết các hàm tạo di chuyển của riêng bạn (và chỉ biên dịch lại chương trình C ++ 03), các stdthùng chứa thư viện sẽ được cập nhật với các hàm movetạo "miễn phí" và (nếu bạn không chặn nó) các cấu trúc sử dụng các đối tượng đã nói ( và cho biết các đối tượng) sẽ bắt đầu nhận xây dựng di chuyển miễn phí trong một số tình huống. Nhiều tình huống trong số đó được bao phủ bởi cuộc bầu chọn trong C ++ 03: không phải tất cả.
Yakk - Adam Nevraumont

5
Sau đó, đó là một triển khai tối ưu hóa tồi, bởi vì các đối tượng có tên khác được trả lại không có thời gian chồng lấp, RVO về mặt lý thuyết vẫn có thể.
Ben Voigt

2
@alarge Có những nơi mà cuộc bầu chọn thất bại, như khi hai vật thể có tuổi thọ chồng chéo có thể bị tách ra thành một phần ba, nhưng không phải là nhau. Sau đó di chuyển được yêu cầu trong C ++ 11 và sao chép trong C ++ 03 (bỏ qua as-if). Elision thường dễ vỡ trong thực tế. Việc sử dụng các stdthùng chứa ở trên chủ yếu là vì chúng rẻ để di chuyển quá mức để sao chép loại bạn nhận được 'miễn phí' trong C ++ 11 khi biên dịch lại C ++ 03. Đây vector::resizelà một ngoại lệ: nó sử dụng movetrong C ++ 11.
Yakk - Adam Nevraumont

27
Tôi chỉ thấy 1 danh mục chung là di chuyển ngữ nghĩa và 5 trường hợp đặc biệt đó.
Julian Schaub - litb

3
@sebro Tôi hiểu, bạn không coi "khiến các chương trình không phân bổ nhiều 1000 phân bổ nhiều kilobyte, và thay vào đó di chuyển con trỏ xung quanh" là đủ. Bạn muốn kết quả đúng thời gian. Microbenchmark không phải là bằng chứng cải thiện hiệu suất hơn bằng chứng về cơ bản bạn đang làm ít hơn. Thiếu một vài 100 ứng dụng trong thế giới thực trong nhiều ngành công nghiệp đang được mô tả với các nhiệm vụ trong thế giới thực, hồ sơ không thực sự là bằng chứng. Tôi đã đưa ra những tuyên bố mơ hồ về "hiệu suất miễn phí" và biến chúng thành sự thật cụ thể về sự khác biệt trong hành vi của chương trình theo C ++ 03 và C ++ 11.
Yakk - Adam Nevraumont

46

nếu bạn có một cái gì đó như:

std::vector<int> foo(); // function declaration.
std::vector<int> v;

// some code

v = foo();

Bạn đã có một bản sao trong C ++ 03, trong khi bạn có một bài tập di chuyển trong C ++ 11. Vì vậy, bạn có tối ưu hóa miễn phí trong trường hợp đó.


4
@Yakk: Làm thế nào cuộc bầu cử sao chép xảy ra trong bài tập?
Jarod42

2
@ Jarod42 Tôi cũng tin rằng việc sao chép không thể thực hiện được trong một nhiệm vụ, vì phía bên trái đã được xây dựng và không có cách nào hợp lý để trình biên dịch biết phải làm gì với dữ liệu "cũ" sau khi đánh cắp tài nguyên từ bên phải bên tay. Nhưng có lẽ tôi đã sai, tôi rất muốn tìm hiểu câu trả lời một lần và mãi mãi. Sao chép bản sao có ý nghĩa khi bạn sao chép cấu trúc, vì đối tượng là "mới" và không có vấn đề gì trong việc quyết định phải làm gì với dữ liệu cũ. Theo như tôi biết, ngoại lệ duy nhất là: "Việc chuyển nhượng chỉ có thể được thực hiện dựa trên quy tắc như nếu"
vsoftco

4
Mã C ++ 03 tốt đã thực hiện một động thái trong trường hợp này, thông quafoo().swap(v);
Ben Voigt

@BenVoigt chắc chắn, nhưng không phải tất cả các mã đều được tối ưu hóa, và không phải tất cả các điểm mà điều này xảy ra đều dễ dàng tiếp cận.
Yakk - Adam Nevraumont

Bản sao hình elip có thể hoạt động trong một bài tập, như @BenVoigt nói. Thuật ngữ tốt hơn là RVO (tối ưu hóa giá trị trả về) và chỉ hoạt động nếu foo () đã được triển khai như vậy.
Trống
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.