Trình diễn thu gom rác nhanh hơn quản lý bộ nhớ thủ công


23

Tôi đã đọc ở nhiều nơi (quái, tôi thậm chí đã tự viết) rằng bộ sưu tập rác có thể (về mặt lý thuyết) có thể nhanh hơn quản lý bộ nhớ thủ công.

Tuy nhiên, hiển thị là khó khăn hơn nhiều để nói hơn là nói.
Tôi chưa bao giờ thực sự thấy bất kỳ đoạn mã nào chứng minh hiệu ứng này trong hành động.

Có ai có (hoặc biết nơi tôi có thể tìm thấy) mã thể hiện lợi thế hiệu suất này không?


5
Vấn đề với GC là hầu hết các triển khai đều không mang tính quyết định nên 2 lần chạy có thể có kết quả rất khác nhau, chưa kể đến việc khó có thể tách các biến đúng để so sánh
ratchet freak

@ratchetfreak: Nếu bạn biết bất kỳ ví dụ nào chỉ nhanh hơn (giả sử) 70% thời gian, điều đó cũng tốt với tôi. Phải có một số cách để so sánh hai, về mặt thông lượng ít nhất (độ trễ có thể sẽ không hoạt động).
Mehrdad

1
Chà, điều này hơi khó vì bạn luôn có thể làm thủ công bất cứ điều gì mang lại cho GC một lợi thế so với những gì bạn đã làm thủ công. Có lẽ tốt hơn là hạn chế điều này với các công cụ quản lý bộ nhớ thủ công "tiêu chuẩn" (malloc () / free (), con trỏ thuộc sở hữu, con trỏ dùng chung với số đếm, con trỏ yếu, không có bộ cấp phát tùy chỉnh)? Hoặc, nếu bạn cho phép các bộ cấp phát tùy chỉnh (có thể thực tế hơn hoặc ít thực tế hơn, tùy thuộc vào loại lập trình viên mà bạn giả định), hãy đặt các hạn chế đối với nỗ lực đưa vào các bộ phân bổ đó. Mặt khác, chiến lược thủ công "sao chép những gì GC làm trong trường hợp này" luôn luôn ít nhất là nhanh như GC.

1
Bằng cách "sao chép những gì GC làm" Tôi không có nghĩa là "xây dựng GC của riêng bạn" (mặc dù lưu ý rằng điều này về mặt lý thuyết là có thể có trong C ++ 11 và hơn thế nữa, trong đó giới thiệu hỗ trợ tùy chọn cho một GC). Ý tôi là, như tôi đã nói trước đó trong cùng một bình luận, "hãy làm những gì mang lại cho GC một lợi thế so với những gì bạn đã làm thủ công". Ví dụ: nếu việc nén giống Cheney giúp ứng dụng này rất nhiều, bạn có thể thực hiện thủ công lược đồ phân bổ + nén tương tự, với các con trỏ thông minh tùy chỉnh để xử lý sửa lỗi con trỏ. Ngoài ra, với các kỹ thuật như ngăn xếp bóng, bạn có thể thực hiện tìm kiếm root trong C hoặc C ++, với chi phí làm thêm.

1
@Ike: Không sao đâu. Xem tại sao tôi hỏi câu hỏi mặc dù? Đó là toàn bộ điểm của câu hỏi của tôi - người đưa ra tất cả các loại giải thích rằng nên có ý nghĩa nhưng tất cả mọi người tình cờ khi bạn yêu cầu họ cung cấp một cuộc biểu tình để chứng minh những gì họ nói là đúng trong thực tế. Toàn bộ vấn đề của câu hỏi này là một lần và mãi mãi cho thấy điều này thực sự có thể xảy ra trong thực tế.
Mehrdad

Câu trả lời:


26

Xem http://bloss.msdn.com/b/ricom/archive/2005/05/10/416151.aspx và theo dõi tất cả các liên kết để xem Rico Mariani vs Raymond Chen (cả hai lập trình viên rất có năng lực tại Microsoft) đấu tay đôi với nó . Raymond sẽ cải thiện cái không được quản lý, Rico sẽ đáp ứng bằng cách tối ưu hóa điều tương tự trong những cái được quản lý.

Với nỗ lực tối ưu hóa về cơ bản bằng không, các phiên bản được quản lý bắt đầu nhanh hơn nhiều lần so với hướng dẫn sử dụng. Cuối cùng, hướng dẫn sử dụng đánh bại người quản lý, nhưng chỉ bằng cách tối ưu hóa đến mức mà hầu hết các lập trình viên sẽ không muốn đi đến. Trong tất cả các phiên bản, việc sử dụng bộ nhớ của hướng dẫn sử dụng tốt hơn đáng kể so với quản lý.


+1 để trích dẫn một ví dụ thực tế bằng mã :) mặc dù việc sử dụng đúng các cấu trúc C ++ (chẳng hạn như swap) không khó lắm và có thể sẽ đưa bạn đến đó một cách khá dễ dàng về hiệu năng ...
Mehrdad

5
Bạn có thể vượt qua Raymond Chen về hiệu suất. Tôi tự tin rằng tôi không thể trừ khi anh ấy ra khỏi bệnh vì tôi bị bệnh, tôi làm việc chăm chỉ hơn nhiều lần và tôi đã gặp may mắn. Tôi không biết tại sao anh ấy không chọn giải pháp mà bạn sẽ chọn. Tôi chắc rằng anh ta có lý do cho điều đó
btilly

Tôi đã sao chép mã của Raymond ở đây và để so sánh, tôi đã viết phiên bản của riêng mình ở đây . Tệp ZIP chứa tệp văn bản ở đây . Trên máy tính của tôi, của tôi chạy trong 14 ms và Raymond chạy trong 21 ms. Trừ khi tôi đã làm gì đó sai (điều này là có thể), mã 215 dòng của anh ta chậm hơn 50% so với triển khai 48 dòng của tôi, ngay cả khi không sử dụng các tệp ánh xạ bộ nhớ hoặc nhóm bộ nhớ tùy chỉnh (mà anh ta đã sử dụng). Của tôi dài bằng một nửa so với phiên bản C #. Tôi đã làm sai, hay bạn quan sát điều tương tự?
Mehrdad

1
@Mehrdad Rút ra một bản sao cũ của gcc trên máy tính xách tay này Tôi có thể báo cáo rằng cả mã của bạn và của anh ấy sẽ không được biên dịch, hãy để mọi thứ làm với nó. Thực tế là tôi không ở trên Windows có thể giải thích điều đó. Nhưng hãy giả sử rằng số và mã của bạn là chính xác. Họ có thực hiện tương tự trên một trình biên dịch và máy tính cũ hàng thập kỷ không? (Nhìn vào khi blog được viết.) Có thể, có thể không. Chúng ta hãy giả sử rằng họ (là một lập trình viên C) không biết sử dụng C ++ đúng cách, v.v. Chúng ta còn lại gì?
btilly

1
Chúng tôi chỉ còn lại một chương trình C ++ hợp lý có thể được dịch sang bộ nhớ được quản lý và tăng tốc. Nhưng nơi mà phiên bản C ++ có thể được tối ưu hóa và tăng tốc xa hơn. Đó là những gì tất cả chúng ta đang thỏa thuận là mô hình chung luôn xảy ra khi mã được quản lý nhanh hơn không được quản lý. Tuy nhiên, chúng ta vẫn có một ví dụ cụ thể về mã hợp lý từ một lập trình viên giỏi nhanh hơn trong phiên bản được quản lý.
btilly

5

Nguyên tắc nhỏ là không có bữa trưa miễn phí.

GC lấy đi sự đau đầu của việc quản lý bộ nhớ thủ công và giảm khả năng mắc lỗi. Có một số tình huống trong đó một số chiến lược GC cụ thể là giải pháp tối ưu cho vấn đề, trong trường hợp đó, bạn sẽ không phải trả tiền phạt khi sử dụng nó. Nhưng có những giải pháp khác sẽ nhanh hơn. Vì bạn luôn có thể mô phỏng trừu tượng cao hơn từ cấp thấp hơn nhưng không phải cách khác xung quanh bạn có thể chứng minh một cách hiệu quả rằng không có cách nào trừu tượng cao hơn có thể nhanh hơn so với mức thấp hơn trong trường hợp chung.

GC một trường hợp đặc biệt của quản lý bộ nhớ thủ công

Nó có thể là rất nhiều công việc hoặc nhiều lỗi dễ có hiệu suất tốt hơn bằng tay nhưng đó là một câu chuyện khác.


1
Điều đó vô nghĩ với tôi. Để cung cấp cho bạn một vài ví dụ cụ thể: 1) các cấp phát và viết các rào cản trong các GC sản xuất là trình biên dịch viết tay vì C quá kém hiệu quả, do đó bạn sẽ đánh bại điều đó từ C và 2) loại bỏ cuộc gọi đuôi là một ví dụ về tối ưu hóa được thực hiện bằng các ngôn ngữ cấp cao (chức năng) mà trình biên dịch C không thực hiện và do đó, không thể thực hiện được trong C. Stack walk là một ví dụ khác về những thứ được thực hiện dưới mức C của các ngôn ngữ cấp cao.
Jon Harrop

2
1) Tôi sẽ phải xem mã cụ thể để nhận xét nhưng nếu trình phân bổ / rào cản viết tay trong trình biên dịch nhanh hơn thì hãy sử dụng trình biên dịch viết tay. Không chắc chắn những gì phải làm với GC. 2) Hãy xem tại đây: stackoverflow.com/a/9814654/441099 vấn đề không phải là liệu một số ngôn ngữ không phải là GC có thể loại bỏ đệ quy đuôi cho bạn hay không. Vấn đề là bạn có thể chuyển đổi mã của mình nhanh hoặc nhanh hơn. Việc trình biên dịch của một số ngôn ngữ cụ thể có thể tự động làm điều này cho bạn hay không là vấn đề thuận tiện. Trong một mức độ trừu tượng đủ thấp, bạn luôn có thể tự làm điều này nếu bạn muốn.
Guy Sirton

1
Ví dụ cuộc gọi đuôi đó trong C chỉ hoạt động trong trường hợp đặc biệt của hàm gọi chính nó. C không thể xử lý trường hợp chung của các hàm gọi nhau. Rơi vào trình biên dịch chương trình và giả sử thời gian vô hạn để phát triển là một tarpit Turing.
Jon Harrop

3

Thật dễ dàng để xây dựng một tình huống nhân tạo trong đó GC hiệu quả hơn nhiều so với các phương thức thủ công - chỉ cần sắp xếp rằng chỉ có một "gốc" cho trình thu gom rác và mọi thứ đều là rác, vì vậy bước GC được hoàn thành ngay lập tức.

Nếu bạn nghĩ về nó, đó là mô hình được sử dụng khi rác thu thập bộ nhớ được phân bổ cho một quy trình. Quá trình chết, tất cả bộ nhớ là rác, chúng ta đã hoàn thành. Ngay cả trong điều kiện thực tế, một quá trình bắt đầu, chạy và chết không để lại dấu vết có thể hiệu quả hơn một quá trình bắt đầu và chạy mãi mãi.

Đối với các chương trình thực tế, được viết bằng ngôn ngữ với bộ sưu tập rác, lợi thế của bộ sưu tập rác không phải là tốc độ mà là tính chính xác và đơn giản.


Nếu thật dễ dàng để xây dựng một ví dụ nhân tạo, bạn có phiền hiển thị một ví dụ đơn giản không?
Mehrdad

1
@Mehrdad Anh ấy đã giải thích một điều đơn giản. Viết chương trình trong đó phiên bản GC không thực hiện chạy rác trước khi thoát. Phiên bản được quản lý bộ nhớ thủ công sẽ chậm hơn vì nó rõ ràng là theo dõi và giải phóng công cụ.
btilly

3
@btilly: "Viết chương trình trong đó phiên bản GC không thực hiện chạy rác trước khi thoát." ... thất bại trong việc thu gom rác ở nơi đầu tiên là rò rỉ bộ nhớ do thiếu một chức năng hoạt động, không phải là một cải tiến hiệu suất do sự hiện diện của một GC! Điều đó giống như gọi abort()trong C ++ trước khi chương trình thoát. Đó là một so sánh vô nghĩa; bạn thậm chí không thu gom rác, bạn chỉ để rò rỉ bộ nhớ. Bạn không thể nói việc thu gom rác nhanh hơn (hoặc chậm hơn) nếu bạn không thu gom rác bắt đầu bằng ...
Mehrdad

Để làm một ví dụ cực đoan, bạn phải xác định một hệ thống hoàn chỉnh với quản lý heap và heap của riêng bạn, đó sẽ là một dự án sinh viên tuyệt vời nhưng quá lớn để phù hợp với lề này. Bạn sẽ làm khá tốt bằng cách viết một chương trình phân bổ và phân bổ các mảng có kích thước ngẫu nhiên, theo cách được thiết kế để gây căng thẳng cho các phương pháp quản lý bộ nhớ không phải là gc.
ddyer

3
@Mehrdad Không phải vậy. Kịch bản là phiên bản GC không bao giờ đạt đến ngưỡng mà nó sẽ thực hiện một lần chạy, không phải là nó sẽ không thực hiện chính xác trên một tập dữ liệu khác. Đó là tầm thường sẽ rất tốt cho phiên bản GC, mặc dù không phải là một công cụ dự đoán tốt về hiệu suất cuối cùng.
btilly

2

Cần xem xét rằng GC không chỉ là một chiến lược quản lý bộ nhớ; nó cũng đưa ra yêu cầu đối với toàn bộ thiết kế của ngôn ngữ và môi trường thời gian chạy, áp đặt chi phí (và lợi ích). Ví dụ, một ngôn ngữ hỗ trợ GC phải được biên dịch thành một dạng mà con trỏ không thể bị ẩn khỏi bộ thu gom rác và nói chung là nơi chúng không thể được xây dựng trừ khi nguyên thủy hệ thống được quản lý cẩn thận. Một xem xét khác là khó khăn trong việc duy trì đảm bảo thời gian đáp ứng, vì GC áp đặt một số bước phải được phép chạy để hoàn thành.

Do đó, nếu bạn có ngôn ngữ là rác được thu thập và so sánh tốc độ với bộ nhớ được quản lý thủ công trong cùng hệ thống, bạn vẫn phải trả phí để hỗ trợ thu gom rác ngay cả khi bạn không sử dụng ngôn ngữ đó.


2

Nhanh hơn là đáng ngờ. Tuy nhiên, nó có thể cực nhanh, không thể nhận ra hoặc nhanh hơn nếu phần cứng được hỗ trợ. Có những thiết kế như thế cho máy LISP từ lâu. Người ta đã xây dựng GC vào hệ thống con bộ nhớ của phần cứng để CPU chính không biết nó ở đó. Giống như nhiều thiết kế sau này, GC chạy đồng thời với bộ xử lý chính mà ít hoặc không cần tạm dừng. Một thiết kế hiện đại hơn là các máy Vega 3 của Azul Systems chạy mã Java nhanh hơn JVM bằng cách sử dụng các bộ xử lý được xây dựng có mục đích và một GC tạm dừng. Google chúng nếu bạn muốn biết tốc độ của GC (hoặc Java) nhanh như thế nào.


2

Tôi đã thực hiện khá nhiều công việc về điều này và mô tả một số ở đây . Tôi đã điểm chuẩn cho Boehm GC trong C ++, phân bổ bằng cách sử dụng mallocnhưng không giải phóng, phân bổ và giải phóng bằng cách sử dụng freevà một khu vực đánh dấu được xây dựng tùy chỉnh được viết bằng C ++ so với GC chứng khoán của OCaml chạy bộ giải n-qu dựa trên danh sách. GC của OCaml đã nhanh hơn trong mọi trường hợp. Các chương trình C ++ và OCaml được viết có chủ ý để thực hiện các phân bổ giống nhau theo cùng một thứ tự.

Tất nhiên, bạn có thể viết lại các chương trình để giải quyết vấn đề chỉ bằng số nguyên 64 bit và không phân bổ. Mặc dù nhanh hơn sẽ đánh bại điểm của bài tập (đó là dự đoán hiệu năng của thuật toán GC mới mà tôi đang thực hiện bằng cách sử dụng một nguyên mẫu được xây dựng trong C ++).

Tôi đã dành nhiều năm trong ngành chuyển mã C ++ thực sang các ngôn ngữ được quản lý. Trong hầu hết mọi trường hợp, tôi đã quan sát thấy các cải tiến hiệu suất đáng kể, nhiều trong số đó có thể là do quản lý bộ nhớ thủ công của GC. Giới hạn thực tế không phải là những gì có thể đạt được trong một microbenchmark mà là những gì có thể đạt được trước thời hạn và các ngôn ngữ dựa trên GC cung cấp những cải tiến năng suất lớn đến mức tôi không bao giờ nhìn lại. Tôi vẫn sử dụng C và C ++ trên các thiết bị nhúng (vi điều khiển) nhưng ngay cả điều đó hiện đang thay đổi.


+1 cảm ơn. Chúng ta có thể thấy và chạy mã điểm chuẩn ở đâu?
Mehrdad

Các mã nằm rải rác về nơi này. Tôi đã đăng phiên bản vùng đánh dấu ở đây: Groups.google.com/d/msg/ Kẻ
Jon Harrop

1
Có kết quả cho cả chủ đề an toàn và không an toàn trong đó.
Jon Harrop

1
@Mehrdad: "Bạn đã loại bỏ các nguồn lỗi tiềm ẩn như vậy chưa?". Vâng. OCaml có một mô hình biên dịch rất đơn giản, không có sự tối ưu hóa như phân tích thoát. Đại diện của OCaml về việc đóng cửa thực sự chậm hơn đáng kể so với giải pháp C ++, vì vậy nó thực sự nên sử dụng một tùy chỉnh List.filternhư C ++. Nhưng, vâng, bạn chắc chắn hoàn toàn đúng khi một số hoạt động RC có thể bị loại bỏ. Tuy nhiên, vấn đề lớn nhất tôi thấy trong tự nhiên là mọi người không có thời gian để thực hiện các tối ưu hóa đó bằng tay trên các cơ sở mã công nghiệp lớn.
Jon Harrop

2
Phải, chắc chắn rồi. Không có nỗ lực bổ sung để viết nhưng viết mã không phải là nút cổ chai với C ++. Duy trì mã là. Duy trì mã với loại phức tạp ngẫu nhiên này là một cơn ác mộng. Hầu hết các cơ sở mã công nghiệp là hàng triệu dòng mã. Bạn không muốn phải đối phó với điều đó. Tôi đã thấy mọi người chuyển đổi mọi thứ thành shared_ptrchỉ để sửa lỗi đồng thời. Mã chậm hơn rất nhiều nhưng, hey, bây giờ nó hoạt động.
Jon Harrop

-1

Một ví dụ như vậy nhất thiết phải có một sơ đồ phân bổ bộ nhớ thủ công xấu.

Giả sử người thu gom rác tốt nhất GC. Bên trong nó có các phương thức để phân bổ bộ nhớ, xác định bộ nhớ nào có thể được giải phóng và các phương thức để cuối cùng giải phóng nó. Cùng nhau những điều này mất ít thời gian hơn tất cả GC; một số thời gian được dành cho các phương pháp khác của GC.

Bây giờ hãy xem xét một bộ cấp phát thủ công sử dụng cùng một cơ chế phân bổ như GCfree()cuộc gọi của chúng chỉ đặt bộ nhớ sang một bên để được giải phóng theo cùng một phương thức như GC. Nó không có giai đoạn quét, cũng không có bất kỳ phương pháp nào khác. Nó nhất thiết phải mất ít thời gian hơn.


2
Một trình thu gom rác thường có thể giải phóng nhiều đối tượng mà không cần phải đặt bộ nhớ vào trạng thái hữu ích sau mỗi đối tượng. Xem xét nhiệm vụ loại bỏ khỏi danh sách mảng tất cả các mục đáp ứng một tiêu chí nhất định. Xóa một mục duy nhất khỏi danh sách N mục là O (N); xóa các mục M khỏi danh sách N, mỗi lần một mục là O (M * N). Tuy nhiên, loại bỏ tất cả các mục đáp ứng một tiêu chí trong một danh sách thông qua danh sách là O (1).
supercat

@supercat: freecũng có thể thu thập các đợt. (Và tất nhiên loại bỏ tất cả các mục đáp ứng tiêu chí vẫn là O (N), nếu chỉ vì danh sách truyền tải chính nó)
MSalters

Loại bỏ tất cả các mục đáp ứng một tiêu chí ít nhất là O (N). Bạn đã đúng freecó thể hoạt động ở chế độ thu thập hàng loạt nếu mỗi mục bộ nhớ có cờ liên kết với nó, mặc dù vậy, GC vẫn có thể xuất hiện trước trong một số trường hợp. Nếu một người có M tham chiếu xác định L các mục riêng biệt trong một tập hợp N điều, thì thời gian để xóa mọi tham chiếu không có tham chiếu nào tồn tại và hợp nhất phần còn lại là O (M) thay vì O (N). Nếu ai có sẵn M không gian, hằng số tỷ lệ có thể khá nhỏ. Hơn nữa, việc nén trong hệ thống GC không quét đòi hỏi ...
supercat

@supercat: Chà, chắc chắn không phải là O (1) như câu cuối cùng của bạn trong trạng thái bình luận đầu tiên.
MSalters

1
@MSalters: "Và điều gì sẽ ngăn cản một kế hoạch xác định có một vườn ươm?". Không có gì. Công cụ thu gom rác của OCaml mang tính quyết định và sử dụng vườn ươm. Nhưng nó không phải là "thủ công" và tôi nghĩ bạn đang sử dụng sai từ "xác định".
Jon Harrop
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.