Có thể dự đoán tĩnh khi nào sẽ giải phóng bộ nhớ --- chỉ từ mã nguồn không?


27

Bộ nhớ (và khóa tài nguyên) được trả về HĐH tại các điểm xác định trong quá trình thực thi chương trình. Luồng điều khiển của một chương trình tự nó đủ để biết nơi nào, chắc chắn, một tài nguyên nhất định có thể được giải quyết. Cũng giống như cách một lập trình viên con người biết viết ở đâu fclose(file)khi chương trình được thực hiện với nó.

Các GC giải quyết điều này bằng cách tìm ra nó trực tiếp trong thời gian chạy khi luồng điều khiển được thực thi. Nhưng nguồn thực sự của dòng điều khiển là nguồn. Vì vậy, về mặt lý thuyết, có thể xác định vị trí chèn các free()cuộc gọi trước khi biên dịch bằng cách phân tích nguồn (hoặc AST).

Đếm tham chiếu là một cách rõ ràng để thực hiện điều này, nhưng thật dễ dàng gặp phải tình huống trong đó con trỏ vẫn được tham chiếu (vẫn trong phạm vi) nhưng không còn cần thiết nữa. Điều này chỉ chuyển đổi trách nhiệm giải quyết thủ công các con trỏ thành trách nhiệm quản lý thủ công phạm vi / tham chiếu đến các con trỏ đó.

Có vẻ như có thể viết một chương trình có thể đọc nguồn của chương trình và:

  1. dự đoán tất cả các hoán vị của luồng điều khiển của chương trình --- với độ chính xác tương tự như khi xem chương trình thực thi trực tiếp của chương trình
  2. theo dõi tất cả các tài liệu tham khảo đến các tài nguyên được phân bổ
  3. đối với mỗi tham chiếu, đi qua toàn bộ luồng điều khiển tiếp theo để tìm điểm sớm nhất mà tham chiếu được đảm bảo không bao giờ bị hủy bỏ
  4. tại thời điểm đó, chèn một câu lệnh thỏa thuận vào dòng mã nguồn đó

Có bất cứ điều gì ngoài đó đã làm điều này? Tôi không nghĩ rằng con trỏ thông minh Rust hoặc C ++ / RAII là như nhau.


57
tìm kiếm vấn đề tạm dừng Đó là ông của lý do tại sao câu hỏi "Không thể trình biên dịch tìm hiểu xem chương trình có X không?" luôn được trả lời với "Không phải trong trường hợp chung."
ratchet freak

18
Bộ nhớ (và khóa tài nguyên) được trả về HĐH tại các điểm xác định trong quá trình thực thi chương trình. Số
Euphoric

9
@ratchetfreak Cảm ơn, không bao giờ biết những thứ như vấn đề tạm dừng này khiến tôi ước mình có bằng cấp về khoa học thay vì hóa học.
zelcon

15
@ zelcon5, bây giờ bạn đã biết về hóa học vấn đề tạm dừng ... :)
David Arno

7
@Euphoric trừ khi bạn cấu trúc chương trình của mình để ranh giới khi sử dụng tài nguyên rất rõ ràng như với RAII hoặc thử với tài nguyên
ratchet freak

Câu trả lời:


23

Lấy ví dụ này (giả định):

void* resource1;
void* resource2;

while(true){

    int input = getInputFromUser();

    switch(input){
        case 1: resource1 = malloc(500); break;
        case 2: resource2 = resource1; break;
        case 3: useResource(resource1); useResource(resource2); break;
    }
}

Khi nào nên gọi miễn phí? trước malloc và gán cho resource1chúng tôi không thể vì nó có thể được sao chép vào resource2, trước khi gán cho resource2chúng tôi không thể vì chúng tôi có thể đã nhận được 2 từ người dùng hai lần mà không cần can thiệp 1.

Cách duy nhất để chắc chắn là kiểm tra resource1 và resource2 để xem chúng có bằng nhau trong trường hợp 1 và 2 không và giải phóng giá trị cũ nếu không. Đây thực chất là tài liệu tham khảo, trong đó bạn biết chỉ có 2 tài liệu tham khảo có thể.


Thật ra đó không phải là cách duy nhất; cách khác là chỉ cho phép một bản sao tồn tại. Điều này, tất nhiên, đi kèm với các vấn đề riêng của nó.
Jack Aidley

27

RAII không tự động giống nhau, nhưng nó có tác dụng tương tự. Nó cung cấp một câu trả lời dễ dàng cho câu hỏi "làm thế nào để bạn biết khi nào không thể truy cập được nữa?" bằng cách sử dụng phạm vi để bao phủ khu vực khi một tài nguyên cụ thể đang được sử dụng.

Bạn có thể muốn xem xét vấn đề tương tự "làm thế nào tôi có thể biết chương trình của tôi sẽ không gặp lỗi loại khi chạy?". Giải pháp cho vấn đề này không phải là dự đoán tất cả các đường dẫn thực hiện thông qua chương trình mà bằng cách sử dụng một hệ thống chú thích và suy luận kiểu để chứng minh rằng không thể có lỗi như vậy. Rust là một nỗ lực để mở rộng thuộc tính bằng chứng này để phân bổ bộ nhớ.

Có thể viết bằng chứng về hành vi của chương trình mà không phải giải quyết vấn đề tạm dừng, nhưng chỉ khi bạn sử dụng chú thích của một số loại để hạn chế chương trình. Xem thêm bằng chứng bảo mật (sel4, v.v.)


Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
maple_shaft

13

Vâng, điều này tồn tại trong tự nhiên. ML Kit là một trình biên dịch chất lượng sản xuất có chiến lược được mô tả (ít nhiều) là một trong các tùy chọn quản lý bộ nhớ có sẵn của nó. Nó cũng cho phép sử dụng một GC thông thường hoặc kết hợp với đếm tham chiếu (bạn có thể sử dụng một trình lược tả heap để xem chiến lược nào sẽ thực sự tạo ra kết quả tốt nhất cho chương trình của bạn).

Một hồi tưởng về quản lý bộ nhớ dựa trên khu vực là một bài viết của các tác giả ban đầu của ML Kit đi sâu vào những thành công và thất bại của nó. Kết luận cuối cùng là chiến lược này rất thiết thực khi viết với sự hỗ trợ của một hồ sơ heap.

(Đây là một minh hoạ tốt về lý do tại sao bạn không nên thường tìm đến các vấn đề ngăn chặn cho một câu trả lời cho những câu hỏi kỹ thuật thực tế: chúng ta không muốn hoặc cần để giải quyết trường hợp chung cho hầu hết các chương trình thực tế.)


5
Tôi nghĩ rằng đây là một ví dụ tuyệt vời về việc áp dụng đúng vấn đề Ngừng. Vấn đề tạm dừng cho chúng ta biết rằng vấn đề không thể giải quyết được trong trường hợp chung, vì vậy bạn tìm kiếm các kịch bản giới hạn trong đó vấn đề có thể giải quyết được.
Taemyr

Lưu ý rằng vấn đề sẽ trở nên dễ giải quyết hơn khi chúng ta nói về các ngôn ngữ không có chức năng thuần túy hoặc gần như thuần túy như Standard ML và Haskell
cat

10

dự đoán tất cả các hoán vị của dòng điều khiển của chương trình

Đây là chỗ có vấn đề. Lượng hoán vị là rất lớn (trong thực tế là vô hạn) đối với bất kỳ chương trình không tầm thường nào, thời gian và bộ nhớ cần thiết sẽ khiến điều này hoàn toàn không thực tế.


điểm tốt. Tôi đoán bộ xử lý lượng tử là hy vọng duy nhất, nếu có chút nào
zelcon

4
@ zelcon5 Haha, không. Điện toán lượng tử làm cho điều này tồi tệ hơn , không tốt hơn. Nó thêm các biến bổ sung ("ẩn") vào chương trình và không chắc chắn hơn nhiều. Hầu hết các mã QC thực tế tôi từng thấy đều dựa vào "lượng tử để tính toán nhanh, cổ điển để xác nhận". Bản thân tôi đã trầy xước bề mặt trên máy tính lượng tử, nhưng dường như máy tính lượng tử có thể không hữu ích nếu không có máy tính cổ điển sao lưu và kiểm tra kết quả của chúng.
Luaan

8

Vấn đề tạm dừng chứng minh điều này là không thể trong mọi trường hợp. Tuy nhiên, nó vẫn có thể xảy ra trong rất nhiều trường hợp và trên thực tế, được thực hiện bởi gần như tất cả các trình biên dịch cho phần lớn các biến. Đây là cách trình biên dịch có thể bảo đảm an toàn khi chỉ phân bổ một biến trên ngăn xếp hoặc thậm chí là một thanh ghi, thay vì lưu trữ heap dài hạn.

Nếu bạn có các hàm thuần túy hoặc ngữ nghĩa sở hữu thực sự tốt, bạn có thể mở rộng phân tích tĩnh đó hơn nữa, mặc dù việc này càng tốn kém hơn để làm như vậy thì càng nhiều mã của bạn càng mất nhiều chi nhánh.


Vâng, trình biên dịch nghĩ rằng nó có thể giải phóng bộ nhớ; nhưng nó có thể không phải như vậy. Hãy nghĩ về lỗi phổ biến của người mới bắt đầu để trả về một con trỏ hoặc tham chiếu đến một biến cục bộ. Các trường hợp tầm thường được trình biên dịch bắt, đúng; những cái ít tầm thường hơn không.
Peter - Tái lập Monica

Lỗi đó được tạo ra bởi các lập trình viên trong các ngôn ngữ nơi các lập trình viên phải tự quản lý cấp phát bộ nhớ @Peter. Khi trình biên dịch quản lý cấp phát bộ nhớ, những lỗi đó không xảy ra.
Karl Bielefeldt

Chà, bạn đã đưa ra một tuyên bố rất chung chung bao gồm cụm từ "gần như tất cả các trình biên dịch" phải bao gồm các trình biên dịch C.
Peter - Tái lập Monica

2
Trình biên dịch C sử dụng nó để xác định những biến tạm thời nào có thể được phân bổ cho các thanh ghi.
Karl Bielefeldt

4

Nếu một lập trình viên hoặc một nhóm viết toàn bộ chương trình, điều hợp lý là các điểm thiết kế có thể được xác định nơi giải phóng bộ nhớ (và các tài nguyên khác). Vì vậy, có, phân tích tĩnh của thiết kế có thể là đủ trong bối cảnh hạn chế hơn.

Tuy nhiên, khi bạn tính đến các DLL, API, khung, và cả các luồng của bên thứ ba), điều đó có thể rất khó (trong mọi trường hợp), các lập trình viên sử dụng để lý giải chính xác về thực thể nào sở hữu bộ nhớ nào và khi sử dụng cuối cùng của nó là. Nghi ngờ thông thường của chúng tôi về ngôn ngữ không đủ tài liệu về việc chuyển quyền sở hữu bộ nhớ của các đối tượng và mảng, nông và sâu. Nếu một lập trình viên không thể giải thích được điều đó (tĩnh hoặc động!) Thì một trình biên dịch rất có thể không thể. Một lần nữa, điều này là do thực tế là việc chuyển quyền sở hữu bộ nhớ không được ghi lại trong các cuộc gọi phương thức hoặc bởi các giao diện, v.v., vì vậy, không thể dự đoán tĩnh khi nào hoặc ở đâu trong mã để giải phóng bộ nhớ.

Vì đây là một vấn đề nghiêm trọng, nhiều ngôn ngữ hiện đại chọn bộ sưu tập rác, tự động lấy lại bộ nhớ sau khi tham chiếu trực tiếp lần cuối. GC có chi phí hiệu năng đáng kể (đặc biệt là cho các ứng dụng thời gian thực), tuy nhiên, vì vậy không phải là một phương pháp chữa bệnh phổ quát. Hơn nữa, bạn vẫn có thể bị rò rỉ bộ nhớ bằng cách sử dụng GC (ví dụ: bộ sưu tập chỉ phát triển). Tuy nhiên, đây là một giải pháp tốt cho hầu hết các bài tập lập trình.

Có một số lựa chọn thay thế (một số mới nổi).

Ngôn ngữ Rust đưa RAII đến một thái cực. Nó cung cấp các cấu trúc ngôn ngữ xác định việc chuyển quyền sở hữu trong các phương thức của các lớp và giao diện chi tiết hơn, ví dụ như các đối tượng được chuyển sang so với mượn giữa người gọi và callee hoặc trong các đối tượng trọn đời dài hơn. Nó cung cấp một mức độ cao về an toàn thời gian biên dịch đối với việc quản lý bộ nhớ. Tuy nhiên, nó không phải là một ngôn ngữ tầm thường để chọn, và cũng không phải không có vấn đề gì (ví dụ: tôi không nghĩ rằng thiết kế hoàn toàn ổn định, một số thứ vẫn đang được thử nghiệm và do đó, thay đổi).

Swift và Objective-C đi một tuyến đường khác, chủ yếu là tính tham chiếu tự động. Việc đếm tham chiếu gặp vấn đề với các chu kỳ, và, có những thách thức đáng kể về lập trình viên, ví dụ, đặc biệt là với việc đóng cửa.


3
Chắc chắn, GC có chi phí, nhưng nó cũng có lợi ích hiệu suất. Ví dụ, trên .NET, việc phân bổ từ heap gần như miễn phí, bởi vì nó sử dụng mẫu "phân bổ ngăn xếp" - chỉ cần tăng một con trỏ và đó là nó. Tôi đã thấy các ứng dụng chạy nhanh hơn viết lại xung quanh .NET GC so với việc chúng đang sử dụng cấp phát bộ nhớ thủ công, nó thực sự không rõ ràng. Tương tự, việc đếm tham chiếu thực sự khá tốn kém (chỉ ở những nơi khác nhau từ một GC) và một số thứ bạn không muốn trả nếu bạn có thể tránh được. Nếu bạn muốn hiệu suất thời gian thực, phân bổ tĩnh thường vẫn là cách duy nhất.
Luaan

2

Nếu một chương trình không phụ thuộc vào bất kỳ đầu vào không xác định nào thì có, điều đó là có thể (với lời cảnh báo rằng đó có thể là một nhiệm vụ phức tạp và có thể mất nhiều thời gian; nhưng điều đó cũng đúng với chương trình). Các chương trình như vậy sẽ hoàn toàn có thể giải quyết được tại thời điểm biên dịch; theo thuật ngữ C ++, chúng có thể (gần như) hoàn toàn gồm constexprs. Các ví dụ đơn giản sẽ là tính 100 chữ số đầu tiên của pi hoặc sắp xếp một từ điển đã biết.


2

Giải phóng bộ nhớ, nói chung, tương đương với vấn đề tạm dừng - nếu bạn không thể biết chính xác liệu chương trình có tạm dừng (tĩnh) hay không, bạn cũng không thể biết liệu nó có giải phóng bộ nhớ (tĩnh) hay không.

function foo(int a) {
    void *p = malloc(1);
    ... do something which may, or may not, halt ...
    free(p);
}

https://en.wikipedia.org/wiki/Halting_propet

Điều đó nói rằng, Rust rất đẹp ... https://doc.rust-lang.org/book/ownership.html

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.