Tại sao con trỏ thông minh đếm tham chiếu rất phổ biến?


52

Như tôi có thể thấy, con trỏ thông minh được sử dụng rộng rãi trong nhiều dự án C ++ trong thế giới thực.

Mặc dù một số loại con trỏ thông minh rõ ràng có lợi để hỗ trợ RAII và chuyển quyền sở hữu, nhưng cũng có một xu hướng sử dụng các con trỏ được chia sẻ theo mặc định , như một cách "thu gom rác" , để lập trình viên không phải suy nghĩ về việc phân bổ nhiều như vậy .

Tại sao các con trỏ chia sẻ phổ biến hơn việc tích hợp một trình thu gom rác thích hợp như Boehm GC ? (Hoặc bạn có đồng ý không, rằng chúng phổ biến hơn so với các GC thực tế?)

Tôi biết về hai lợi thế của các GC thông thường so với đếm tham chiếu:

  • Các thuật toán GC thông thường không có vấn đề với các chu trình tham chiếu .
  • Tổng số tham chiếu thường chậm hơn so với một GC thích hợp.

Các lý do để sử dụng con trỏ thông minh đếm tham chiếu là gì?


6
Tôi chỉ cần thêm một nhận xét rằng đây là một mặc định sai để sử dụng: trong hầu hết các trường hợp, std::unique_ptrlà đủ và như vậy có chi phí không vượt quá con trỏ thô về hiệu suất thời gian chạy. Bằng cách sử dụng std::shared_ptrở mọi nơi, bạn cũng sẽ che khuất ngữ nghĩa sở hữu, mất một trong những lợi ích chính của con trỏ thông minh ngoài quản lý tài nguyên tự động - hiểu rõ về ý định đằng sau mã.
Matt

2
Xin lỗi nhưng câu trả lời được chấp nhận ở đây là hoàn toàn sai. Đếm tham chiếu có tổng phí cao hơn (số đếm thay vì bit đánh dấu và hiệu suất thời gian chạy chậm hơn), thời gian tạm dừng không giới hạn khi giảm tuyết lở và không phức tạp hơn, như Cheney bán không gian.
Jon Harrop

Câu trả lời:


57

Một số lợi thế của việc tham chiếu đếm qua thu gom rác:

  1. Chi phí thấp. Công cụ thu gom rác có thể khá khó chịu (ví dụ: làm cho chương trình của bạn đóng băng vào những thời điểm không thể đoán trước trong khi quy trình thu gom rác xử lý) và khá tốn bộ nhớ (ví dụ: dấu chân bộ nhớ của quy trình của bạn không cần thiết tăng lên nhiều megabyte trước khi bộ sưu tập rác cuối cùng khởi động)

  2. Hành vi dễ dự đoán hơn. Với việc đếm tham chiếu, bạn được đảm bảo rằng đối tượng của bạn sẽ được giải phóng ngay khi tham chiếu cuối cùng đến nó biến mất. Mặt khác, với bộ sưu tập rác, đối tượng của bạn sẽ được giải phóng "đôi khi", khi hệ thống tiếp cận với nó. Đối với RAM, đây thường không phải là vấn đề lớn trên máy tính để bàn hoặc máy chủ được tải nhẹ, nhưng đối với các tài nguyên khác (ví dụ: xử lý tệp), bạn thường cần đóng chúng càng sớm càng tốt để tránh xung đột tiềm ẩn sau này.

  3. Đơn giản hơn. Việc đếm tham chiếu có thể được giải thích trong vài phút và được thực hiện trong một hoặc hai giờ. Người thu gom rác, đặc biệt là những người có hiệu suất tốt, cực kỳ phức tạp và không nhiều người hiểu chúng.

  4. Tiêu chuẩn. C ++ bao gồm đếm tham chiếu (thông qua shared_ptr) và bạn bè trong STL, điều đó có nghĩa là hầu hết các lập trình viên C ++ đều quen thuộc với nó và hầu hết mã C ++ sẽ hoạt động với nó. Tuy nhiên, không có bất kỳ trình thu gom rác C ++ tiêu chuẩn nào, điều đó có nghĩa là bạn phải chọn một và hy vọng nó hoạt động tốt cho trường hợp sử dụng của bạn - và nếu không, đó là vấn đề của bạn, không phải là ngôn ngữ.

Đối với những nhược điểm bị cáo buộc của việc đếm tham chiếu - không phát hiện chu kỳ là một vấn đề, nhưng một điều mà tôi chưa bao giờ cá nhân gặp phải trong mười năm qua khi sử dụng tính năng tham chiếu. Hầu hết các cấu trúc dữ liệu đều có tính chu kỳ tự nhiên và nếu bạn gặp phải tình huống bạn cần tham chiếu theo chu kỳ (ví dụ: con trỏ cha trong nút cây), bạn có thể chỉ cần sử dụng một con trỏ yếu hoặc con trỏ C thô cho "hướng ngược". Miễn là bạn nhận thức được vấn đề tiềm ẩn khi bạn thiết kế cấu trúc dữ liệu của mình, thì đó không phải là vấn đề.

Về hiệu suất, tôi chưa bao giờ gặp vấn đề với hiệu suất đếm tham chiếu. Tôi đã gặp vấn đề với hiệu suất của việc thu gom rác, đặc biệt là việc đóng băng ngẫu nhiên mà GC có thể phải chịu, mà giải pháp duy nhất ("không phân bổ đối tượng") cũng có thể được nhắc lại là "không sử dụng GC" .


16
Ngây thơ triển khai tham chiếu đếm thường được nhiều làm giảm thông lượng hơn các tổng công ty sản xuất (30-40%) tại các chi phí của độ trễ. Khoảng cách có thể được đóng lại bằng các tối ưu hóa, chẳng hạn như sử dụng ít bit hơn cho số tiền hoàn trả và tránh các đối tượng theo dõi cho đến khi chúng thoát khỏi C C ++ thực hiện điều này một cách tự nhiên nếu bạn chủ yếu make_sharedkhi quay lại. Tuy nhiên, độ trễ có xu hướng là vấn đề lớn hơn trong các ứng dụng thời gian thực, nhưng thông lượng thường quan trọng hơn, đó là lý do tại sao truy tìm các GC được sử dụng rộng rãi. Tôi sẽ không nhanh chóng nói xấu họ.
Jon Purdy

3
Tôi phân biệt 'đơn giản hơn': đơn giản hơn về tổng số lượng thực hiện cần có, nhưng không đơn giản hơn đối với mã sử dụng nó: so sánh việc nói với ai đó cách sử dụng RC ('làm điều này khi tạo đối tượng và điều này khi phá hủy chúng' ) làm thế nào để (ngây thơ, thường là đủ) sử dụng GC ('...').
AakashM

4
"Với việc đếm tham chiếu, bạn được đảm bảo rằng đối tượng của bạn sẽ được giải phóng ngay khi tham chiếu cuối cùng về nó biến mất". Đó là một quan niệm sai lầm phổ biến. Flyingfrogblog.blogspot.co.uk/2013/10/ từ
Jon Harrop

4
@JonHarrop: Bài đăng trên blog đó là sai lầm khủng khiếp. Bạn cũng nên đọc tất cả các ý kiến, đặc biệt là ý kiến ​​cuối cùng.
Ded repeatator

3
@JonHarrop: Vâng, có. Anh ta không hiểu rằng cuộc đời là phạm vi đầy đủ cho đến khi kết thúc. Và tối ưu hóa trong F # mà theo các nhận xét chỉ đôi khi hoạt động là kết thúc vòng đời sớm hơn, nếu biến không được sử dụng lại. Mà tự nhiên có hiểm họa riêng của nó.
Ded repeatator

26

Để có được hiệu năng tốt từ một GC, GC cần có khả năng di chuyển các đối tượng trong bộ nhớ. Trong một ngôn ngữ như C ++, nơi bạn có thể tương tác trực tiếp với các vị trí bộ nhớ, điều này là không thể. (Microsoft C ++ / CLR không được tính vì nó giới thiệu cú pháp mới cho các con trỏ được quản lý bởi GC và do đó thực sự là một ngôn ngữ khác.)

Boehm GC, trong khi một ý tưởng tiện lợi, thực sự là điều tồi tệ nhất của cả hai thế giới: bạn cần một malloc () chậm hơn một GC tốt, và do đó bạn mất đi hành vi phân bổ / phân bổ xác định mà không tăng hiệu suất tương ứng của một thế hệ GC . Thêm vào đó là do sự cần thiết phải bảo thủ, do đó, nó sẽ không nhất thiết phải thu thập tất cả rác của bạn.

Một GC tốt, điều chỉnh tốt có thể là một điều tuyệt vời. Nhưng trong một ngôn ngữ như C ++, mức tăng là tối thiểu và chi phí thường không đáng là bao.

Tuy nhiên, sẽ rất thú vị khi C ++ 11 trở nên phổ biến hơn, liệu lambdas và ngữ nghĩa nắm bắt bắt đầu dẫn dắt cộng đồng C ++ hướng tới các loại vấn đề phân bổ và đối tượng suốt đời khiến cộng đồng Lisp phát minh ra các GC trong lần đầu tiên địa điểm.

Xem thêm câu trả lời của tôi cho một câu hỏi liên quan trên StackOverflow .


6
Giới thiệu về Boehm GC, đôi khi tôi đã tự hỏi bản thân mình chịu trách nhiệm về sự ác cảm truyền thống đối với các lập trình viên C và C ++ đến mức nào chỉ bằng cách cung cấp một ấn tượng xấu đầu tiên về công nghệ nói chung.
Leushenko

@Leushenko Nói tốt. Một trường hợp điển hình là câu hỏi này, trong đó Boehm gc được gọi là gc "thích hợp", bỏ qua thực tế là nó chậm và thực tế được đảm bảo để rò rỉ. Tôi đã tìm thấy câu hỏi này trong khi nghiên cứu xem ai đó đã triển khai trình ngắt chu trình kiểu python cho shared_ptr, có vẻ như là một mục tiêu đáng giá để thực hiện c ++.
dùng4815162342

4

Như tôi có thể thấy, con trỏ thông minh được sử dụng rộng rãi trong nhiều dự án C ++ trong thế giới thực.

Đúng nhưng, khách quan, phần lớn mã hiện được viết bằng các ngôn ngữ hiện đại với các công cụ thu gom rác.

Mặc dù một số loại con trỏ thông minh rõ ràng có lợi để hỗ trợ RAII và chuyển quyền sở hữu, nhưng cũng có một xu hướng sử dụng các con trỏ được chia sẻ theo mặc định, như một cách "thu gom rác", để lập trình viên không phải suy nghĩ về việc phân bổ nhiều như vậy .

Đó là một ý tưởng tồi bởi vì bạn vẫn cần phải lo lắng về chu kỳ.

Tại sao các con trỏ chia sẻ phổ biến hơn việc tích hợp một trình thu gom rác thích hợp như Boehm GC? (Hoặc bạn có đồng ý không, rằng chúng phổ biến hơn so với các GC thực tế?)

Ôi chà, có quá nhiều điều sai với dòng suy nghĩ của bạn:

  1. GC của Boehm không phải là một GC "phù hợp" theo bất kỳ ý nghĩa nào của từ này. Nó thực sự khủng khiếp. Đó là bảo thủ vì vậy nó rò rỉ và không hiệu quả bởi thiết kế. Xem: http://fendingfrogblog.blogspot.co.uk/search/label/boehm

  2. Các con trỏ được chia sẻ, về mặt khách quan, không phổ biến như GC vì phần lớn các nhà phát triển đang sử dụng các ngôn ngữ của GC bây giờ và không cần dùng chung các con trỏ. Chỉ cần nhìn vào Java và Javascript trong thị trường việc làm so với C ++.

  3. Bạn dường như đang hạn chế xem xét đến C ++ bởi vì, tôi cho rằng, bạn nghĩ rằng GC là một vấn đề tiếp tuyến. Không phải ( cách duy nhất để có được một GC tốt là thiết kế ngôn ngữ và VM cho nó ngay từ đầu) để bạn giới thiệu lựa chọn thiên vị. Những người thực sự muốn thu gom rác thích hợp không gắn bó với C ++.

Các lý do để sử dụng con trỏ thông minh đếm tham chiếu là gì?

Bạn bị giới hạn ở C ++ nhưng muốn bạn có quản lý bộ nhớ tự động.


7
Ừm, đó là một câu hỏi được gắn thẻ c ++ nói về các tính năng của C ++. Rõ ràng, bất kỳ tuyên bố chung đang nói về trong C ++, không phải là toàn bộ chương trình. Vì vậy, bộ sưu tập rác "khách quan" có thể được sử dụng bên ngoài thế giới C ++, điều đó cuối cùng không liên quan đến câu hỏi trong tay.
Nicol Bolas

2
Dòng cuối cùng của bạn hoàn toàn sai: Bạn đang ở C ++ và rất vui vì bạn không bị buộc phải đối phó với GC và nó bị trì hoãn giải phóng tài nguyên. Có một lý do Apple không thích GC và nguyên tắc quan trọng nhất đối với các ngôn ngữ của GC là: Không tạo ra bất kỳ rác nào trừ khi bạn có những nguồn tài nguyên nhàn rỗi hoặc không thể giúp nó.
Ded repeatator

3
@JonHarrop: Vì vậy, hãy so sánh các chương trình nhỏ tương đương có và không có GC, vốn không được chọn rõ ràng để chơi với lợi thế của đội bóng. Cái nào bạn muốn cần thêm bộ nhớ?
Ded

1
@Ded repeatator: Tôi có thể dự tính các chương trình đưa ra kết quả. Việc đếm tham chiếu sẽ tốt hơn theo dõi GC khi chương trình được thiết kế để giữ bộ nhớ phân bổ heap cho đến khi nó tồn tại trong vườn ươm (ví dụ như một hàng danh sách) bởi vì đó là hiệu suất bệnh lý cho một thế hệ GC và sẽ tạo ra rác thải trôi nổi nhất. Truy tìm bộ sưu tập rác sẽ cần ít bộ nhớ hơn so với đếm tham chiếu dựa trên phạm vi khi có nhiều đối tượng nhỏ và thời gian sống ngắn nhưng không được biết rõ về mặt tĩnh nên một cái gì đó giống như một chương trình logic sử dụng cấu trúc dữ liệu chức năng thuần túy.
Jon Harrop

3
@JonHarrop: Ý tôi là với GC (theo dõi hoặc bất cứ điều gì) và RAII nếu bạn nói C ++. Bao gồm tính tham chiếu, nhưng chỉ khi nó hữu ích. Hoặc bạn có thể so sánh với một chương trình Swift.
Ded repeatator

3

Trong MacOS X và iOS và với các nhà phát triển sử dụng Objective-C hoặc Swift, việc đếm tham chiếu rất phổ biến vì nó được xử lý tự động và việc sử dụng thu gom rác đã giảm đáng kể do Apple không hỗ trợ nữa (Tôi nói rằng các ứng dụng sử dụng bộ sưu tập rác sẽ phá vỡ trong phiên bản MacOS X tiếp theo và bộ sưu tập rác chưa bao giờ được triển khai trong iOS). Tôi thực sự nghi ngờ rằng đã từng có nhiều phần mềm sử dụng bộ sưu tập rác khi nó có sẵn.

Lý do để loại bỏ bộ sưu tập rác: Nó không bao giờ hoạt động đáng tin cậy trong môi trường kiểu C nơi con trỏ có thể "thoát" đến các khu vực mà người thu gom rác không thể truy cập được. Apple tin tưởng mạnh mẽ và tin rằng việc tham chiếu nhanh hơn. (Bạn có thể đưa ra bất kỳ khiếu nại nào ở đây về tốc độ tương đối, nhưng không ai có thể thuyết phục Apple). Và cuối cùng, không ai sử dụng bộ sưu tập rác.

Điều đầu tiên mà bất kỳ nhà phát triển MacOS X hoặc iOS nào cũng học được là cách xử lý các chu trình tham chiếu, vì vậy đó không phải là vấn đề đối với một nhà phát triển thực sự.


Theo cách hiểu của tôi, đó không phải là một môi trường giống như C quyết định mọi thứ, nhưng đó là điều không xác định và cần nhiều bộ nhớ hơn để có hiệu năng chấp nhận được, và bên ngoài máy chủ / máy tính để bàn luôn khan hiếm.
Ded repeatator

Gỡ lỗi tại sao trình thu gom rác phá hủy một đối tượng mà tôi vẫn đang sử dụng (dẫn đến sự cố) đã quyết định nó cho tôi :-)
gnasher729

Ồ vâng, điều đó cũng sẽ làm điều đó. Cuối cùng bạn đã tìm ra lý do tại sao?
Ded repeatator

Vâng, đó là một trong nhiều hàm Unix nơi bạn chuyển một khoảng trống * dưới dạng "bối cảnh" sau đó được trả lại cho bạn trong hàm gọi lại; void * thực sự là một đối tượng Objective-C và trình thu gom rác không nhận ra đối tượng đã bị xóa trong cuộc gọi Unix. Gọi lại được gọi, bỏ khoảng trống * đến Object *, kaboom!
gnasher729

2

Nhược điểm lớn nhất của bộ sưu tập rác trong C ++ là không thể hiểu đúng:

  • Trong C ++, con trỏ không sống trong cộng đồng có tường bao quanh, chúng được trộn lẫn với các dữ liệu khác. Như vậy, bạn không thể phân biệt một con trỏ với dữ liệu khác chỉ có một mẫu bit có thể được hiểu là một con trỏ hợp lệ.

    Hậu quả: Bất kỳ trình thu gom rác C ++ nào cũng sẽ rò rỉ các đối tượng cần được thu thập.

  • Trong C ++, bạn có thể thực hiện số học con trỏ để lấy con trỏ. Như vậy, nếu bạn không tìm thấy một con trỏ bắt đầu một khối, điều đó không có nghĩa là khối đó không thể được tham chiếu.

    Hậu quả: Bất kỳ trình thu gom rác C ++ nào cũng phải tính đến các điều chỉnh này, xử lý bất kỳ chuỗi bit nào xảy ra ở bất kỳ đâu trong một khối, bao gồm ngay sau khi kết thúc khối đó, như một con trỏ hợp lệ tham chiếu khối.

    Lưu ý: Không có trình thu gom rác C ++ nào có thể xử lý mã bằng các thủ thuật như sau:

    int* array = new int[7];
    array--;    //undefined behavior, but people may be tempted anyway...
    for(int i = 1; i <= 7; i++) array[i] = i;
    

    Đúng, điều này gọi hành vi không xác định. Nhưng một số mã hiện có là thông minh hơn là tốt cho nó và nó có thể kích hoạt việc xử lý sơ bộ bởi một người thu gom rác.


2
" Chúng được trộn lẫn với dữ liệu khác. " Không nhiều đến mức chúng bị "trộn" với dữ liệu khác. Thật dễ dàng để sử dụng hệ thống loại C ++ để xem con trỏ là gì và không phải là gì. Vấn đề là con trỏ thường xuyên trở thành dữ liệu khác. Ẩn một con trỏ trong một số nguyên là một công cụ phổ biến không may cho nhiều API kiểu C.
Nicol Bolas

1
Bạn thậm chí không cần hành vi không xác định để làm hỏng trình thu gom rác trong c ++. Bạn có thể, ví dụ, tuần tự hóa một con trỏ tới một tệp và đọc nó sau. Trong khi đó, quy trình của bạn có thể không chứa con trỏ đó ở bất kỳ đâu trong không gian địa chỉ của nó, vì vậy trình thu gom rác có thể thu thập đối tượng đó và sau đó khi bạn giải tuần tự hóa con trỏ ... Rất tiếc.
Bwmat

@Bwmat "Thậm chí"? Viết con trỏ vào một tập tin như thế có vẻ hơi ... xa vời. Dù sao, cùng một vấn đề nghiêm trọng gây ra con trỏ để xếp chồng các đối tượng, chúng có thể biến mất khi bạn đọc con trỏ trở lại từ tệp ở nơi khác trong mã! Giải trừ giá trị con trỏ không hợp lệ hành vi không xác định, đừng làm điều đó.
hyde

Nếu tất nhiên, bạn sẽ cần phải cẩn thận nếu bạn đang làm một cái gì đó như thế. Nó có nghĩa là một ví dụ rằng, nói chung, một công cụ thu gom rác không thể hoạt động 'đúng cách' trong mọi trường hợp trong c ++ (mà không thay đổi ngôn ngữ)
Bwmat

1
@ gnasher729: Ơ, không? Con trỏ quá khứ là hoàn toàn tốt?
Ded repeatator
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.