Chi phí của con trỏ thông minh so với con trỏ bình thường trong C ++ là bao nhiêu?


101

Chi phí của con trỏ thông minh so với con trỏ bình thường trong C ++ 11 là bao nhiêu? Nói cách khác, mã của tôi sẽ chậm hơn nếu tôi sử dụng con trỏ thông minh, và nếu có, chậm hơn bao nhiêu?

Cụ thể, tôi đang hỏi về C ++ 11 std::shared_ptrstd::unique_ptr.

Rõ ràng, thứ được đẩy xuống ngăn xếp sẽ lớn hơn (ít nhất là tôi nghĩ vậy), bởi vì một con trỏ thông minh cũng cần lưu trữ trạng thái bên trong của nó (số lượng tham chiếu, v.v.), câu hỏi thực sự là, điều này sẽ là bao nhiêu ảnh hưởng đến hiệu suất của tôi, nếu có?

Ví dụ: tôi trả về một con trỏ thông minh từ một hàm thay vì một con trỏ bình thường:

std::shared_ptr<const Value> getValue();
// versus
const Value *getValue();

Hoặc, ví dụ: khi một trong các hàm của tôi chấp nhận một con trỏ thông minh làm tham số thay vì một con trỏ bình thường:

void setValue(std::shared_ptr<const Value> val);
// versus
void setValue(const Value *val);

8
Cách duy nhất để biết là chuẩn mã của bạn.
Basile Starynkevitch

Mà một trong những bạn có ý nghĩa? std::unique_ptrhoặc std::shared_ptr?
stefan

10
Câu trả lời là 42. (nói cách khác, ai biết được, bạn cần phải lập hồ sơ mã của mình và hiểu trên phần cứng của bạn cho tải công việc thông thường của bạn.)
Nim

Ứng dụng của bạn cần phải tận dụng tối đa các con trỏ thông minh để nó trở nên đáng kể.
user2672165 Ngày

Chi phí của việc sử dụng shared_ptr trong một hàm setter đơn giản là rất khủng khiếp và sẽ cộng thêm nhiều chi phí 100%.
Lothar

Câu trả lời:


176

std::unique_ptr chỉ có chi phí bộ nhớ nếu bạn cung cấp cho nó một số trình duyệt không tầm thường.

std::shared_ptr luôn có chi phí bộ nhớ cho bộ đếm tham chiếu, mặc dù nó rất nhỏ.

std::unique_ptr chỉ có chi phí thời gian trong khi hàm tạo (nếu nó phải sao chép bộ phân định đã cung cấp và / hoặc khởi tạo null con trỏ) và trong khi hủy (để hủy đối tượng sở hữu).

std::shared_ptrcó chi phí thời gian trong hàm khởi tạo (để tạo bộ đếm tham chiếu), trong bộ hủy (để giảm bộ đếm tham chiếu và có thể hủy đối tượng) và trong toán tử gán (để tăng bộ đếm tham chiếu). Do đảm bảo an toàn cho luồng std::shared_ptr, các bước tăng / giảm này là nguyên tử, do đó thêm một số chi phí cao hơn.

Lưu ý rằng không ai trong số họ có chi phí thời gian trong việc tham chiếu (trong việc nhận tham chiếu đến đối tượng được sở hữu), trong khi thao tác này dường như là phổ biến nhất đối với con trỏ.

Tóm lại, có một số chi phí, nhưng nó sẽ không làm cho mã chậm trừ khi bạn liên tục tạo và hủy các con trỏ thông minh.


11
unique_ptrkhông có chi phí trong trình hủy. Nó hoạt động giống hệt như bạn làm với một con trỏ thô.
R. Martinho Fernandes

6
@ R.MartinhoFernandes so với chính con trỏ thô, nó có phí thời gian trong hàm hủy, vì bộ hủy con trỏ thô không làm gì cả. So sánh với cách một con trỏ thô có thể sẽ được sử dụng, nó chắc chắn không có chi phí cao.
lisyarus

3
Đáng lưu ý rằng một phần của chi phí xây dựng / phá hủy / chuyển nhượng shared_ptr là do sự an toàn của luồng
Joe

1
Ngoài ra, những gì về hàm tạo mặc định của std::unique_ptr? Nếu bạn xây dựng một std::unique_ptr<int>, nội dung int*sẽ được khởi tạo cho nullptrdù bạn muốn hay không.
Martin Drozdik

1
@MartinDrozdik Trong hầu hết các tình huống, bạn cũng khởi tạo con trỏ thô để kiểm tra xem nó có vô hiệu sau này không, hoặc tương tự như vậy. Tuy nhiên, đã thêm điều này vào câu trả lời, cảm ơn bạn.
lisyarus,

26

Như với tất cả hiệu suất mã, phương tiện thực sự đáng tin cậy duy nhất để có được thông tin cứng là đo lường và / hoặc kiểm tra mã máy.

Điều đó nói rằng, lý luận đơn giản nói rằng

  • Bạn có thể mong đợi một số chi phí trong các bản dựng gỡ lỗi, vì ví dụ: operator->phải được thực thi như một lệnh gọi hàm để bạn có thể bước vào nó (điều này đến lượt nó do thiếu hỗ trợ chung cho việc đánh dấu các lớp và hàm là không gỡ lỗi).

  • shared_ptrbạn có thể mong đợi một số chi phí trong quá trình tạo ban đầu, vì điều đó liên quan đến phân bổ động của khối điều khiển và cấp phát động chậm hơn rất nhiều so với bất kỳ hoạt động cơ bản nào khác trong C ++ (sử dụng make_sharedkhi thực tế có thể, để giảm thiểu chi phí đó).

  • Ngoài ra, shared_ptrcó một số chi phí tối thiểu trong việc duy trì số lượng tham chiếu, ví dụ: khi chuyển một shared_ptrgiá trị bằng, nhưng không có chi phí nào như vậy cho unique_ptr.

Hãy ghi nhớ điểm đầu tiên ở trên, khi bạn đo lường, hãy làm điều đó cho cả bản dựng gỡ lỗi và bản phát hành.

Ủy ban tiêu chuẩn hóa C ++ quốc tế đã xuất bản một báo cáo kỹ thuật về hiệu suất , nhưng báo cáo này là vào năm 2006, trước đó unique_ptrshared_ptrđã được thêm vào thư viện tiêu chuẩn. Tuy nhiên, các con trỏ thông minh đã cũ ở thời điểm đó, vì vậy báo cáo cũng xem xét điều đó. Trích dẫn phần có liên quan:

“Nếu việc truy cập một giá trị thông qua một con trỏ thông minh tầm thường chậm hơn đáng kể so với việc truy cập nó thông qua một con trỏ thông thường, thì trình biên dịch đang xử lý trừu tượng không hiệu quả. Trong quá khứ, hầu hết các trình biên dịch đều có các hình phạt trừu tượng đáng kể và một số trình biên dịch hiện tại vẫn làm như vậy. Tuy nhiên, ít nhất hai trình biên dịch đã được báo cáo là có hình phạt trừu tượng dưới 1% và một hình phạt khác là 3%, vì vậy việc loại bỏ loại chi phí này là tốt trong quy trình hiện đại ”

Như một phỏng đoán đã được thông báo, tính đến đầu năm 2014 đã đạt được “sự tốt đẹp trong tình trạng hiện đại” với các trình biên dịch phổ biến nhất hiện nay.


Bạn có thể vui lòng bao gồm một số chi tiết trong câu trả lời của bạn về các trường hợp tôi đã thêm vào câu hỏi của mình không?
Venemo

Điều này có thể đúng từ 10 năm trước trở lên, nhưng ngày nay, việc kiểm tra mã máy không hữu ích như người trên gợi ý. Tùy thuộc vào cách hướng dẫn được pipelined, vectơ hóa, ... và cách trình biên dịch / bộ xử lý xử lý suy đoán cuối cùng là tốc độ của nó. Mã máy ít mã hơn không nhất thiết có nghĩa là mã nhanh hơn. Cách duy nhất để xác định hiệu suất là lập hồ sơ. Điều này có thể thay đổi trên cơ sở bộ xử lý và cũng trên mỗi trình biên dịch.
Byron

Một vấn đề mà tôi đã thấy là, một khi shared_ptrs được sử dụng trong một máy chủ, thì việc sử dụng shared_ptrs bắt đầu gia tăng và ngay sau đó shared_ptrs trở thành kỹ thuật quản lý bộ nhớ mặc định. Vì vậy, bây giờ bạn đã lặp lại các hình phạt trừu tượng 1-3% được thực hiện lặp đi lặp lại.
Nathan Doromal

Tôi nghĩ việc đo điểm chuẩn cho một bản dựng gỡ lỗi là một việc hoàn toàn lãng phí thời gian
Paul Childs

26

Câu trả lời của tôi khác với những người khác và tôi thực sự tự hỏi liệu họ đã từng viết mã hồ sơ chưa.

shared_ptr có chi phí đáng kể cho việc tạo vì nó phân bổ bộ nhớ cho khối điều khiển (giữ bộ đếm tham chiếu và danh sách con trỏ tới tất cả các tham chiếu yếu). Nó cũng có chi phí bộ nhớ lớn vì điều này và thực tế là std :: shared_ptr luôn là một bộ 2 con trỏ (một tới đối tượng, một tới khối điều khiển).

Nếu bạn truyền một shared_pointer cho một hàm dưới dạng tham số giá trị thì nó sẽ chậm hơn ít nhất 10 lần so với một cuộc gọi bình thường và tạo nhiều mã trong đoạn mã để giải nén ngăn xếp. Nếu bạn vượt qua nó bằng cách tham chiếu, bạn sẽ nhận được một hướng bổ sung mà cũng có thể khá tệ hơn về mặt hiệu suất.

Đó là lý do tại sao bạn không nên làm điều này trừ khi chức năng thực sự liên quan đến quản lý quyền sở hữu. Nếu không, hãy sử dụng "shared_ptr.get ()". Nó không được thiết kế để đảm bảo đối tượng của bạn không bị giết trong khi gọi hàm thông thường.

Nếu bạn phát điên và sử dụng shared_ptr trên các đối tượng nhỏ như cây cú pháp trừu tượng trong trình biên dịch hoặc trên các nút nhỏ trong bất kỳ cấu trúc đồ thị nào khác, bạn sẽ thấy hiệu suất giảm đáng kể và tăng bộ nhớ lớn. Tôi đã thấy một hệ thống phân tích cú pháp được viết lại ngay sau khi C ++ 14 tung ra thị trường và trước khi lập trình viên học cách sử dụng con trỏ thông minh một cách chính xác. Việc viết lại chậm hơn nhiều so với mã cũ.

Nó không phải là một viên đạn bạc và con trỏ thô cũng không tệ theo định nghĩa. Lập trình viên tồi tệ và thiết kế tồi tệ hại. Thiết kế cẩn thận, thiết kế với quyền sở hữu rõ ràng và cố gắng sử dụng shared_ptr chủ yếu trên ranh giới API hệ thống con.

Nếu bạn muốn tìm hiểu thêm, bạn có thể xem bài nói hay của Nicolai M. Josuttis về "Giá thực của con trỏ chia sẻ trong C ++" https://vimeo.com/131189627
Nó đi sâu vào chi tiết triển khai và kiến ​​trúc CPU cho các rào cản ghi, nguyên tử khóa, vv một khi nghe bạn sẽ không bao giờ nói về tính năng này là rẻ. Nếu bạn chỉ muốn bằng chứng về độ lớn chậm hơn, hãy bỏ qua 48 phút đầu tiên và xem anh ta chạy mã ví dụ chạy chậm hơn tới 180 lần (được biên dịch bằng -O3) khi sử dụng con trỏ chia sẻ ở mọi nơi.


Cảm ơn câu trả lời của bạn! Bạn đã lập hồ sơ trên nền tảng nào? Bạn có thể sao lưu các xác nhận quyền sở hữu của mình với một số dữ liệu không?
Venemo

Tôi không có số để hiển thị, nhưng bạn có thể tìm thấy một số trong Nico Josuttis talk vimeo.com/131189627
Lothar

6
Đã bao giờ nghe nói về std::make_shared()? Ngoài ra, tôi thấy các cuộc biểu tình của lạm dụng trắng trợn là xấu một chút nhàm chán ...
Deduplicator

2
Tất cả những gì "make_shared" có thể làm là giúp bạn an toàn khỏi một lần phân bổ bổ sung và cung cấp cho bạn nhiều vị trí bộ nhớ cache hơn nếu khối điều khiển được phân bổ phía trước đối tượng. Nó không thể giúp ích gì cả khi bạn vượt qua con trỏ xung quanh. Đây không phải là gốc rễ của các vấn đề.
Lothar

14

Nói cách khác, mã của tôi sẽ chậm hơn nếu tôi sử dụng con trỏ thông minh, và nếu có, chậm hơn bao nhiêu?

Chậm hơn? Nhiều khả năng là không, trừ khi bạn đang tạo một chỉ mục khổng lồ bằng shared_ptrs và bạn không có đủ bộ nhớ đến mức máy tính của bạn bắt đầu nhăn nheo, giống như một bà già bị một lực không thể chịu được từ xa rơi xuống đất.

Điều gì sẽ làm cho mã của bạn chậm hơn là tìm kiếm chậm chạp, xử lý vòng lặp không cần thiết, bản sao dữ liệu khổng lồ và nhiều thao tác ghi vào đĩa (như hàng trăm).

Những lợi thế của một con trỏ thông minh đều liên quan đến quản lý. Nhưng chi phí có cần thiết không? Điều này phụ thuộc vào cách thực hiện của bạn. Giả sử bạn đang lặp lại một mảng gồm 3 pha, mỗi pha có một mảng 1024 phần tử. Việc tạo một smart_ptrquá trình này có thể là quá mức cần thiết, vì khi quá trình lặp lại hoàn tất, bạn sẽ biết mình phải xóa nó. Vì vậy, bạn có thể có thêm bộ nhớ từ việc không sử dụng smart_ptr...

Nhưng bạn có thực sự muốn làm điều đó?

Một lần rò rỉ bộ nhớ có thể khiến sản phẩm của bạn gặp lỗi ngay (giả sử chương trình của bạn bị rò rỉ 4 megabyte mỗi giờ, sẽ mất hàng tháng để phá vỡ một máy tính, tuy nhiên, nó sẽ hỏng, bạn biết đấy vì rò rỉ ở đó) .

Giống như nói rằng "phần mềm của bạn được bảo hành trong 3 tháng, sau đó, hãy gọi cho tôi để được bảo dưỡng."

Vì vậy, cuối cùng nó thực sự là một vấn đề ... bạn có thể xử lý rủi ro này không? Việc sử dụng một con trỏ thô để xử lý việc lập chỉ mục của bạn trên hàng trăm đối tượng khác nhau có đáng để mất quyền kiểm soát bộ nhớ hay không.

Nếu câu trả lời là có, thì hãy sử dụng một con trỏ thô.

Nếu bạn thậm chí không muốn xem xét nó, a smart_ptrlà một giải pháp tốt, khả thi và tuyệt vời.


4
ok, nhưng valgrind là tốt trong việc kiểm tra rò rỉ bộ nhớ càng tốt, do đó, miễn là bạn sử dụng nó, bạn phải được an toàn ™
graywolf

@Paladin Vâng, nếu bạn có thể xử lý bộ nhớ của bạn, smart_ptrlà thực sự hữu ích cho các đội lớn
Claudiordgz

3
Tôi sử dụng unique_ptr, nó đơn giản hóa rất nhiều thứ, nhưng không giống như shared_ptr, việc đếm tham chiếu không hiệu quả lắm GC và nó cũng không hoàn hảo
greywolf

1
@Paladin Tôi cố gắng sử dụng con trỏ thô nếu tôi có thể đóng gói mọi thứ. Nếu nó là một cái gì đó mà tôi sẽ đi khắp nơi như một cuộc tranh cãi thì có lẽ tôi sẽ xem xét một smart_ptr. Hầu hết các unique_ptrs tôi được sử dụng trong việc thực hiện lớn, giống như một phương pháp chính hoặc chạy
Claudiordgz

@Lothar Tôi thấy bạn diễn giải một trong những điều tôi đã nói trong câu trả lời của bạn: Thats why you should not do this unless the function is really involved in ownership management... câu trả lời tuyệt vời, cảm ơn, bỏ phiếu tán
Claudiordgz

0

Chỉ để nhìn thoáng qua và chỉ cho người []vận hành, nó chậm hơn con trỏ thô ~ 5 lần như được minh họa trong đoạn mã sau, được biên dịch bằng cách sử dụng gcc -lstdc++ -std=c++14 -O0và xuất ra kết quả này:

malloc []:     414252610                                                 
unique []  is: 2062494135                                                
uq get []  is: 238801500                                                 
uq.get()[] is: 1505169542
new is:        241049490 

Tôi đang bắt đầu tìm hiểu c ++, tôi nghĩ đến điều này: bạn luôn cần biết mình đang làm gì và dành nhiều thời gian hơn để biết những gì người khác đã làm trong c ++ của bạn.

BIÊN TẬP

Theo ý kiến ​​của @Mohan Kumar, tôi đã cung cấp thêm thông tin chi tiết. Phiên bản gcc là 7.4.0 (Ubuntu 7.4.0-1ubuntu1~14.04~ppa1), Kết quả trên nhận được khi -O0sử dụng, tuy nhiên, khi tôi sử dụng cờ '-O2', tôi nhận được kết quả này:

malloc []:     223
unique []  is: 105586217
uq get []  is: 71129461
uq.get()[] is: 69246502
new is:        9683

Sau đó chuyển sang clang version 3.9.0, -O0là:

malloc []:     409765889
unique []  is: 1351714189
uq get []  is: 256090843
uq.get()[] is: 1026846852
new is:        255421307

-O2 là:

malloc []:     150
unique []  is: 124
uq get []  is: 83
uq.get()[] is: 83
new is:        54

Kết quả của tiếng kêu -O2là tuyệt vời.

#include <memory>
#include <iostream>
#include <chrono>
#include <thread>

uint32_t n = 100000000;
void t_m(void){
    auto a  = (char*) malloc(n*sizeof(char));
    for(uint32_t i=0; i<n; i++) a[i] = 'A';
}
void t_u(void){
    auto a = std::unique_ptr<char[]>(new char[n]);
    for(uint32_t i=0; i<n; i++) a[i] = 'A';
}

void t_u2(void){
    auto a = std::unique_ptr<char[]>(new char[n]);
    auto tmp = a.get();
    for(uint32_t i=0; i<n; i++) tmp[i] = 'A';
}
void t_u3(void){
    auto a = std::unique_ptr<char[]>(new char[n]);
    for(uint32_t i=0; i<n; i++) a.get()[i] = 'A';
}
void t_new(void){
    auto a = new char[n];
    for(uint32_t i=0; i<n; i++) a[i] = 'A';
}

int main(){
    auto start = std::chrono::high_resolution_clock::now();
    t_m();
    auto end1 = std::chrono::high_resolution_clock::now();
    t_u();
    auto end2 = std::chrono::high_resolution_clock::now();
    t_u2();
    auto end3 = std::chrono::high_resolution_clock::now();
    t_u3();
    auto end4 = std::chrono::high_resolution_clock::now();
    t_new();
    auto end5 = std::chrono::high_resolution_clock::now();
    std::cout << "malloc []:     " <<  (end1 - start).count() << std::endl;
    std::cout << "unique []  is: " << (end2 - end1).count() << std::endl;
    std::cout << "uq get []  is: " << (end3 - end2).count() << std::endl;
    std::cout << "uq.get()[] is: " << (end4 - end3).count() << std::endl;
    std::cout << "new is:        " << (end5 - end4).count() << std::endl;
}

Tôi đã kiểm tra mã ngay bây giờ, nó chỉ chậm 10% khi sử dụng con trỏ duy nhất.
Mohan Kumar

8
không bao giờ điểm chuẩn với -O0hoặc mã gỡ lỗi. Đầu ra sẽ cực kỳ kém hiệu quả . Luôn luôn sử dụng ít nhất -O2(hoặc -O3ngày nay vì một số vector hóa không được thực hiện trong -O2)
phuclv

1
Nếu bạn có thời gian và muốn nghỉ giải lao, hãy lấy -O4 để tối ưu hóa thời gian liên kết và tất cả các hàm trừu tượng nhỏ sẽ được nội tuyến và biến mất.
Lothar

Bạn nên bao gồm một freecuộc gọi trong kiểm tra malloc và delete[]cho new (hoặc tạo biến astatic), bởi vì các unique_ptrs đang gọi ẩn delete[], trong hàm hủy của chúng.
RnMss
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.