Nó có thể hiệu quả hơn cho các hệ thống nói chung để loại bỏ Stacks và chỉ sử dụng Heap để quản lý bộ nhớ?


14

Dường như với tôi rằng mọi thứ có thể được thực hiện với một ngăn xếp đều có thể được thực hiện với heap, nhưng không phải mọi thứ có thể được thực hiện với heap đều có thể được thực hiện với stack. Đúng không? Sau đó, vì đơn giản, và ngay cả khi chúng ta mất một ít hiệu suất với khối lượng công việc nhất định, không nên tốt hơn nếu chỉ đi với một tiêu chuẩn (nghĩa là heap)?

Hãy nghĩ về sự đánh đổi giữa mô-đun và hiệu suất. Tôi biết đó không phải là cách tốt nhất để mô tả kịch bản này, nhưng nói chung có vẻ như sự đơn giản của sự hiểu biết và thiết kế có thể là một lựa chọn tốt hơn ngay cả khi có tiềm năng cho hiệu suất tốt hơn.


1
Trong C và C ++, bạn cần phân bổ rõ ràng bộ nhớ được phân bổ trên heap. Điều đó không đơn giản.
dùng16764

Tôi đã sử dụng một triển khai C # đang định hình cho thấy các đối tượng ngăn xếp được phân bổ trong một khu vực giống như đống với bộ sưu tập rác khủng khiếp. Giải pháp của tôi? Di chuyển mọi thứ có thể (ví dụ: biến vòng lặp, biến tạm thời, v.v.) sang bộ nhớ heap liên tục. Làm cho chương trình ăn RAM gấp 10 lần và chạy với tốc độ gấp 10 lần.
imallett

@IanMallett: Tôi không hiểu lời giải thích của bạn về vấn đề và giải pháp. Bạn có một liên kết với nhiều thông tin hơn ở đâu đó? Thông thường tôi thấy phân bổ dựa trên ngăn xếp để được nhanh hơn.
Frank Hileman

@FrankHileman vấn đề cơ bản là đây: việc triển khai C # tôi đang sử dụng có tốc độ thu gom rác cực kỳ kém. "Giải pháp" là làm cho tất cả các biến liên tục để trong thời gian chạy không có hoạt động bộ nhớ nào xảy ra. Tôi đã viết một ý kiến cách đây một thời gian về sự phát triển của C # / XNA nói chung cũng thảo luận về một số bối cảnh.
imallett

@IanMallett: cảm ơn bạn. Là một nhà phát triển C / C ++ trước đây, người chủ yếu sử dụng C # ngày nay, trải nghiệm của tôi khá khác biệt. Tôi thấy rằng các thư viện là vấn đề lớn nhất. Có vẻ như nền tảng XBox360 đã được cung cấp một nửa cho các nhà phát triển .net. Thông thường khi tôi gặp vấn đề về GC, tôi chuyển sang dùng chung. Nó giúp.
Frank Hileman

Câu trả lời:


30

Heaps rất tệ trong việc phân bổ và giải quyết bộ nhớ nhanh. Nếu bạn muốn lấy nhiều bộ nhớ nhỏ trong một khoảng thời gian giới hạn, một đống không phải là lựa chọn tốt nhất của bạn. Một ngăn xếp, với thuật toán phân bổ / phân bổ siêu đơn giản, tự nhiên vượt trội về điều này (thậm chí nhiều hơn nếu nó được tích hợp vào phần cứng), đó là lý do tại sao mọi người sử dụng nó cho những việc như truyền đối số cho các hàm và lưu trữ các biến cục bộ - nhiều nhất Nhược điểm quan trọng là nó có không gian hạn chế, và vì vậy giữ các vật thể lớn trong đó, hoặc cố gắng sử dụng nó cho các vật thể tồn tại lâu dài, đều là những ý tưởng tồi.

Loại bỏ hoàn toàn ngăn xếp vì mục đích đơn giản hóa ngôn ngữ lập trình là cách IMO sai - cách tiếp cận tốt hơn là trừu tượng hóa sự khác biệt, hãy để trình biên dịch tìm ra loại lưu trữ nào sẽ sử dụng, trong khi lập trình viên kết hợp cao hơn- các cấu trúc mức gần với cách suy nghĩ của con người - và trên thực tế, các ngôn ngữ cấp cao như C #, Java, Python, v.v. thực hiện chính xác điều này. Chúng cung cấp cú pháp gần như giống hệt nhau cho các đối tượng được phân bổ heap và các nguyên hàm được phân bổ ngăn xếp ('kiểu tham chiếu' so với 'kiểu giá trị' trong biệt ngữ .NET), hoàn toàn minh bạch hoặc với một vài khác biệt về chức năng mà bạn phải hiểu để sử dụng ngôn ngữ một cách chính xác (nhưng bạn thực sự không cần phải biết làm thế nào một ngăn xếp và một đống hoạt động trong nội bộ).


2
WOW NÀY LÀ TỐT :) Thực sự súc tích và nhiều thông tin cho người mới bắt đầu!
Dark Templar

1
Trên nhiều CPU, ngăn xếp được xử lý trong phần cứng, đây là một vấn đề bên ngoài ngôn ngữ nhưng nó đóng một phần lớn trong thời gian chạy.
Patrick Hughes

@Patrick Hughes: Vâng, nhưng Heap cũng nằm trong phần cứng, phải không?
Dark Templar

@Dark Điều Patrick có thể muốn nói là các kiến ​​trúc như x86 có các thanh ghi đặc biệt để quản lý ngăn xếp và các hướng dẫn đặc biệt để đặt hoặc xóa thứ gì đó trên / khỏi ngăn xếp. Điều đó làm cho nó khá nhanh.
FUZxxl

3
@Donal Fellows: Tất cả đều đúng. Nhưng vấn đề là các ngăn xếp và đống đều có điểm mạnh và điểm yếu, và sử dụng chúng theo đó sẽ mang lại mã hiệu quả nhất.
tdammers

8

Nói một cách đơn giản, một ngăn xếp không phải là một chút hiệu suất. Nó nhanh hơn hàng trăm hoặc hàng ngàn lần so với đống. Ngoài ra, hầu hết các máy hiện đại đều có hỗ trợ phần cứng cho ngăn xếp (như x86) và chức năng phần cứng ví dụ như ngăn xếp cuộc gọi không thể bị xóa.


Ý bạn là gì khi bạn nói rằng các máy móc hiện đại có hỗ trợ phần cứng cho ngăn xếp? Bản thân ngăn xếp đã VÀO phần cứng, phải không?
Templar tối

1
x86 có các thanh ghi và hướng dẫn đặc biệt để xử lý ngăn xếp. x86 không hỗ trợ cho heaps - những thứ như vậy được tạo bởi HĐH.
Pubby

8

Không

Khu vực ngăn xếp trong C ++ là cực kỳ nhanh so với. Tôi mạo hiểm không có nhà phát triển C ++ có kinh nghiệm nào có thể mở để vô hiệu hóa chức năng đó.

Với C ++, bạn có quyền lựa chọn và bạn có quyền kiểm soát. Các nhà thiết kế không đặc biệt có xu hướng giới thiệu các tính năng có thêm thời gian thực hiện hoặc không gian.

Luyện tập sự lựa chọn đó

Nếu bạn muốn xây dựng một thư viện hoặc chương trình yêu cầu mọi đối tượng được phân bổ động, bạn có thể làm điều đó với C ++. Nó sẽ thực thi tương đối chậm, nhưng sau đó bạn có thể có "tính mô đun". Đối với phần còn lại của chúng tôi, tính mô-đun luôn là tùy chọn, giới thiệu nó khi cần thiết vì cả hai đều được yêu cầu để triển khai tốt / nhanh.

Lựa chọn thay thế

Có các ngôn ngữ khác yêu cầu lưu trữ cho từng đối tượng được tạo trên heap; nó khá chậm, đến nỗi nó làm ảnh hưởng đến các thiết kế (chương trình trong thế giới thực) theo cách tệ hơn là phải học cả hai (IMO).

Cả hai đều quan trọng và C ++ cung cấp cho bạn khả năng sử dụng hiệu quả cả hai cho từng kịch bản nhất định. Phải nói rằng, ngôn ngữ C ++ có thể không lý tưởng cho thiết kế của bạn, nếu các yếu tố này trong OP của bạn quan trọng đối với bạn (ví dụ: đọc các ngôn ngữ cấp cao hơn).


Heap thực sự có cùng tốc độ với stack, nhưng không có hỗ trợ phần cứng chuyên dụng để phân bổ. Mặt khác, có nhiều cách để tăng tốc rất nhiều (theo một số điều kiện khiến chúng trở thành kỹ thuật chỉ dành cho chuyên gia).
Donal Fellows

@DonalFellows: Việc hỗ trợ phần cứng cho ngăn xếp là không liên quan. Điều quan trọng là biết rằng bất cứ khi nào bất cứ thứ gì được phát hành, người ta có thể phát hành bất cứ thứ gì được phân bổ sau nó. Một số ngôn ngữ lập trình không có đống có thể độc lập với các đối tượng, nhưng thay vào đó chỉ có phương thức "miễn phí mọi thứ được phân bổ sau".
supercat

6

Sau đó, vì đơn giản, và ngay cả khi chúng ta mất một ít hiệu suất với khối lượng công việc nhất định, không nên tốt hơn nếu chỉ đi với một tiêu chuẩn (nghĩa là heap)?

Trên thực tế, hiệu suất hit có thể là đáng kể!

Như những người khác đã chỉ ra các ngăn xếp là một cấu trúc cực kỳ hiệu quả để quản lý dữ liệu tuân theo các quy tắc LIFO (cuối cùng trước hết). Cấp phát bộ nhớ / giải phóng trên ngăn xếp thường chỉ là một thay đổi đối với một thanh ghi trên CPU. Thay đổi một thanh ghi hầu như luôn là một trong những thao tác nhanh nhất mà bộ xử lý có thể thực hiện.

Heap thường là một cấu trúc dữ liệu khá phức tạp và việc cấp phát / giải phóng bộ nhớ sẽ cần nhiều hướng dẫn để thực hiện tất cả các sổ sách kế toán liên quan. Thậm chí tệ hơn, trong các triển khai chung, mọi lệnh gọi để làm việc với heap đều có khả năng dẫn đến một cuộc gọi đến hệ điều hành. Các cuộc gọi hệ điều hành rất tốn thời gian! Chương trình thường phải chuyển từ chế độ người dùng sang chế độ kernel và bất cứ khi nào điều này xảy ra, hệ điều hành có thể quyết định rằng các chương trình khác có nhu cầu cấp bách hơn và chương trình của bạn sẽ phải chờ.


5

Simula sử dụng đống cho tất cả mọi thứ. Đặt mọi thứ vào heap luôn gây ra thêm một mức độ gián tiếp cho các biến cục bộ và nó gây thêm áp lực cho Garbage Collector (bạn phải tính đến việc Garbage Collector thực sự bị hút ngược lại). Đó là một phần lý do tại sao Bjarne phát minh ra C ++.


Vậy về cơ bản C ++ chỉ sử dụng heap là tốt?
Dark Templar

2
@Dark: Cái gì? Không. Việc thiếu stack trong Simula là một nguồn cảm hứng để làm điều đó tốt hơn.
fredoverflow

Ah tôi hiểu ý của bạn lúc này! Cảm ơn +1 :)
Templar tối

3

Ngăn xếp cực kỳ hiệu quả đối với dữ liệu LIFO, chẳng hạn như dữ liệu meta được liên kết với các lệnh gọi hàm chẳng hạn. Ngăn xếp cũng tận dụng các tính năng thiết kế vốn có của CPU. Vì hiệu suất ở cấp độ này là nền tảng cho mọi thứ khác trong một quy trình, nên việc đạt được "nhỏ" đó ở cấp độ đó sẽ lan truyền rất rộng rãi. Ngoài ra, bộ nhớ heap có thể di chuyển được bởi HĐH, điều này sẽ gây chết người cho các ngăn xếp. Mặc dù một ngăn xếp có thể được thực hiện trong heap, nó đòi hỏi chi phí sẽ ảnh hưởng đến từng phần của một quy trình ở cấp độ chi tiết nhất.


2

"hiệu quả" về mặt bạn viết mã có thể, nhưng chắc chắn không phải là về hiệu quả phần mềm của bạn. Phân bổ ngăn xếp về cơ bản là miễn phí (chỉ cần một vài hướng dẫn máy để di chuyển con trỏ ngăn xếp và không gian dự trữ trên ngăn xếp cho các biến cục bộ).

Vì phân bổ ngăn xếp hầu như không mất thời gian, nên việc phân bổ ngay cả trên một đống rất hiệu quả sẽ chậm hơn 100 nghìn (nếu không phải là 1M +) lần.

Bây giờ hãy tưởng tượng có bao nhiêu biến cục bộ và các cấu trúc dữ liệu khác mà một ứng dụng điển hình sử dụng. Mỗi một chữ "i" nhỏ mà bạn sử dụng làm bộ đếm vòng lặp được phân bổ chậm hơn hàng triệu lần.

Chắc chắn nếu phần cứng đủ nhanh, bạn có thể viết một ứng dụng chỉ sử dụng heap. Nhưng bây giờ hình ảnh loại ứng dụng bạn có thể viết nếu bạn tận dụng heap và sử dụng cùng một phần cứng.


Khi bạn nói "hãy tưởng tượng có bao nhiêu biến cục bộ và các cấu trúc dữ liệu khác mà một ứng dụng thông thường sử dụng" bạn đang đề cập cụ thể đến cấu trúc dữ liệu nào khác?
Templar tối

1
Các giá trị "100k" và "1M +" bằng cách nào đó có khoa học không? Hay đó chỉ là một cách để nói "rất nhiều"?
Bruno Reis

@Bruno - IMHO các số 100K và 1M tôi đã sử dụng thực sự là ước tính bảo thủ để chứng minh một điểm. Nếu bạn đã quen thuộc với VS và C ++, hãy viết chương trình phân bổ 100 byte trên ngăn xếp và viết một chương trình phân bổ 100 byte trên heap. Sau đó chuyển sang chế độ xem tháo gỡ và chỉ cần đếm số lượng hướng dẫn lắp ráp mỗi lần phân bổ. Các hoạt động của heap thường là một số lệnh gọi hàm vào windows DLL, có các nhóm và các danh sách được liên kết và sau đó có sự kết hợp và các thuật toán khác. Với ngăn xếp, nó có thể rút gọn thành một hướng dẫn lắp ráp "thêm đặc biệt, 100" ...
DXM

2
"100k (nếu không 1M +) lần chậm hơn"? Đó là một chút phóng đại. Hãy để nó là hai bậc độ lớn chậm hơn, có thể là ba, nhưng đó là nó. Ít nhất, Linux của tôi có thể thực hiện phân bổ heap 100M (+ một số hướng dẫn xung quanh) trong chưa đầy 6 giây trên lõi i5, điều đó không thể nhiều hơn vài trăm hướng dẫn cho mỗi phân bổ - thực sự, nó gần như chắc chắn ít hơn. Nếu nó chậm hơn sáu bậc so với stack thì sẽ có điều gì đó không ổn với việc thực hiện heap của HĐH. Chắc chắn có rất nhiều sai lầm với Windows, nhưng điều đó ...
leftaroundabout

1
người điều hành có lẽ sắp giết toàn bộ chủ đề bình luận này. Vì vậy, đây là thỏa thuận, tôi thừa nhận rằng những con số thực tế đã được rút ra khỏi ...., nhưng chúng ta hãy đồng ý rằng yếu tố này thực sự rất lớn và không đưa ra nhiều bình luận :)
DXM

2

Bạn có thể có hứng thú với "Bộ sưu tập rác rất nhanh, nhưng một ngăn xếp thì nhanh hơn".

http://dspace.mit.edu/bitstream/handle/1721.1/6622/AIM-1462.ps.Z

Nếu tôi đọc chính xác, những kẻ này đã sửa đổi trình biên dịch C để phân bổ "khung ngăn xếp" trên heap, sau đó sử dụng bộ sưu tập rác để phân bổ lại các khung thay vì bật ngăn xếp.

"Khung ngăn xếp" được phân bổ ngăn xếp vượt trội hơn "khung ngăn xếp" được phân bổ theo heap một cách quyết định.


1

Làm thế nào là ngăn xếp cuộc gọi sẽ làm việc trên một đống? Về cơ bản, bạn sẽ phải phân bổ một ngăn xếp trên heap trong mọi chương trình, vậy tại sao không có phần cứng OS + làm điều đó cho bạn?

Nếu bạn muốn mọi thứ thực sự đơn giản và hiệu quả, chỉ cần cung cấp cho người dùng bộ nhớ của họ và để họ giải quyết. Tất nhiên, không ai muốn thực hiện mọi thứ của mình và đó là lý do tại sao chúng ta có một chồng và một đống.


Nói một cách chính xác thì "ngăn xếp cuộc gọi" không phải là một tính năng bắt buộc của môi trường thời gian chạy ngôn ngữ lập trình. ví dụ: Việc triển khai đơn giản ngôn ngữ chức năng được đánh giá một cách lười biếng bằng cách giảm biểu đồ (mà tôi đã mã hóa) không có ngăn xếp cuộc gọi. Nhưng ngăn xếp cuộc gọi là một kỹ thuật rất hữu ích và được sử dụng rộng rãi, đặc biệt là khi các bộ xử lý hiện đại cho rằng bạn sử dụng nó và được tối ưu hóa cho việc sử dụng nó.
Ben

@Ben - trong khi đó là sự thật (và là một điều tốt) đối với những thứ trừu tượng như phân bổ bộ nhớ ra khỏi ngôn ngữ, thì điều này không thay đổi kiến ​​trúc máy tính hiện đang thịnh hành. Do đó, mã giảm biểu đồ của bạn vẫn sẽ sử dụng ngăn xếp khi chạy - thích hay không.
Ingo

@Ingo Không thực sự theo bất kỳ ý nghĩa nào. Chắc chắn, HĐH sẽ khởi tạo một phần bộ nhớ theo truyền thống gọi là "ngăn xếp" và sẽ có một thanh ghi trỏ đến nó. Nhưng các chức năng trong ngôn ngữ nguồn không được biểu diễn dưới dạng các khung ngăn xếp theo thứ tự cuộc gọi. Thực thi chức năng được thể hiện hoàn toàn bằng cách thao tác các cấu trúc dữ liệu trong heap. Ngay cả khi không sử dụng tối ưu hóa cuộc gọi cuối cùng, không thể "tràn ngăn xếp". Đó là điều tôi muốn nói khi tôi nói không có gì cơ bản về "ngăn xếp cuộc gọi".
Ben

Tôi không nói về các chức năng của ngôn ngữ nguồn mà là các chức năng trong trình thông dịch (hoặc bất cứ điều gì) thực sự thực hiện việc giảm biểu đồ. Những người sẽ cần một chồng. Điều này là hiển nhiên, vì phần cứng hiện đại không làm giảm đồ thị. Do đó, thuật toán giảm biểu đồ của bạn cuối cùng được ánh xạ tới ode máy và tôi cá là có các cuộc gọi chương trình con trong số chúng. QED.
Ingo

1

Cả stack và heap đều được yêu cầu. Chúng được sử dụng trong các tình huống khác nhau, ví dụ:

  1. Phân bổ heap có giới hạn là sizeof (a [0]) == sizeof (a [1])
  2. Phân bổ ngăn xếp có một giới hạn là sizeof (a) là hằng số thời gian biên dịch
  3. Phân bổ heap có thể làm các vòng lặp, đồ thị vv cấu trúc dữ liệu phức tạp
  4. Phân bổ ngăn xếp có thể làm cây có kích thước thời gian biên dịch
  5. Heap yêu cầu theo dõi quyền sở hữu
  6. Phân bổ và phân bổ ngăn xếp là tự động
  7. Bộ nhớ heap có thể dễ dàng chuyển từ phạm vi này sang phạm vi khác thông qua con trỏ
  8. Bộ nhớ ngăn xếp là cục bộ của từng chức năng và các đối tượng cần được chuyển đến phạm vi trên để kéo dài tuổi thọ của chúng (hoặc được lưu trữ bên trong các đối tượng thay vì bên trong các chức năng thành viên)
  9. Heap là xấu cho hiệu suất
  10. Stack khá nhanh
  11. Các đối tượng heap được trả về từ các hàm thông qua các con trỏ có quyền sở hữu. Hoặc shared_ptrs.
  12. Các đối tượng ngăn xếp được trả về từ các hàm thông qua các tham chiếu không có quyền sở hữu.
  13. Heap yêu cầu khớp mọi thứ mới với loại xóa hoặc xóa chính xác []
  14. Các đối tượng ngăn xếp sử dụng danh sách khởi tạo RAII và constructor
  15. Các đối tượng heap có thể được khởi tạo bất kỳ điểm nào bên trong hàm và không thể sử dụng các tham số của hàm tạo
  16. Các đối tượng ngăn xếp sử dụng các tham số hàm tạo để khởi tạo
  17. Heap sử dụng mảng và kích thước mảng có thể thay đổi trong thời gian chạy
  18. Stack dành cho các đối tượng đơn lẻ và kích thước được cố định theo thời gian biên dịch

Về cơ bản các cơ chế không thể được so sánh ở tất cả vì rất nhiều chi tiết là khác nhau. Điều duy nhất phổ biến với họ là cả hai đều xử lý bộ nhớ bằng cách nào đó.


1

Các máy tính hiện đại có nhiều lớp bộ nhớ đệm ngoài hệ thống bộ nhớ chính lớn, nhưng chậm. Người ta có thể thực hiện hàng tá truy cập vào bộ nhớ cache nhanh nhất trong thời gian cần thiết để đọc hoặc ghi một byte từ hệ thống bộ nhớ chính. Do đó, truy cập một vị trí một nghìn lần nhanh hơn nhiều so với truy cập 1.000 (hoặc thậm chí 100) vị trí độc lập một lần. Bởi vì hầu hết các ứng dụng liên tục phân bổ và phân bổ một lượng nhỏ bộ nhớ gần đỉnh ngăn xếp, các vị trí trên đỉnh của ngăn xếp được sử dụng và sử dụng lại một lượng rất lớn, chiếm đa số (99% + trong một ứng dụng thông thường) truy cập ngăn xếp có thể được xử lý bằng bộ nhớ cache.

Ngược lại, nếu một ứng dụng liên tục tạo và từ bỏ các đối tượng heap để lưu trữ thông tin tiếp tục, mọi phiên bản của mọi đối tượng ngăn xếp đã được tạo sẽ phải được ghi vào bộ nhớ chính. Ngay cả khi phần lớn các đối tượng như vậy sẽ hoàn toàn vô dụng vào thời điểm CPU muốn tái chế các trang bộ nhớ cache mà chúng bắt đầu, CPU sẽ không có cách nào để biết điều đó. Do đó, CPU sẽ phải lãng phí rất nhiều thời gian để thực hiện ghi bộ nhớ chậm với thông tin vô dụng. Không chính xác một công thức cho tốc độ.

Một điều khác cần xem xét là trong nhiều trường hợp, thật hữu ích khi biết rằng một tham chiếu đối tượng được truyền cho một thường trình sẽ không được sử dụng một khi thói quen thoát ra. Nếu các tham số và biến cục bộ được truyền qua ngăn xếp và nếu kiểm tra mã của thường trình cho thấy rằng nó không tồn tại một bản sao của tham chiếu được truyền vào, thì mã gọi thường trình có thể chắc chắn rằng nếu không có tham chiếu bên ngoài đến đối tượng tồn tại trước cuộc gọi, không có gì sẽ tồn tại sau đó. Ngược lại, nếu các tham số được truyền qua các đối tượng heap, các khái niệm như "sau khi trả về thường trình" trở nên khó hiểu hơn, vì nếu mã giữ một bản sao của phần tiếp theo, thì thường xuyên có thể "trả về" nhiều lần cuộc gọi duy nhất.

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.