Tại sao cần Heap trong khi Stack nghe đã đủ ngầu?

Nguyễn Bình Sơn
Feb 22 · 5 min read

Trên lớp, sau khi giải thích cho học viên về Stack và Heap, đôi khi tôi nhận được câu hỏi như sau:

  • Có điều gì khủng khiếp xảy ra nếu tôi chỉ sử dụng Stack cho hệ thống máy tính, đến nỗi mà phải có mô hình trên kia?
  • Tương tự, có điều gì xảy ra nếu tôi chỉ sử dụng Heap cho hệ thống máy tính?

Bài viết này làm rõ chúng thêm một chút, thật ra vẫn từ câu chuyện khái niệm mà ra thôi.

Fast-track về Stack và Heap

Stack (ngăn xếp) là vùng nhớ được dành cho cho một thread — cứ tưởng tượng nó như “giấy nháp”. Khi một hàm được gọi, một khối bộ nhớ gọi là frame (lát) được chuẩn bị lên trên cùng của stack (vì sự chuẩn bị này mà trong ngôn ngữ C, tất cả các lệnh khai báo biến đều phải đặt ở đầu hàm) để lưu trữ các biến local. Khi hàm returns, frame không được sử dụng nữa và bộ nhớ tại đó sẽ được giải phóng sẵn sàng để dùng cho lời gọi hàm tiếp theo. Stack thật sự là một stack — có nghĩa nó có cấu trúc dữ liệu và thuật toán LIFO đi kèm; frame được chuẩn bị gần nhất luôn là frame tiếp theo được giải phóng. Máy tính thao tác với LIFO vô cùng đơn giản và tự nhiên, giải phóng một frame đơn giản chỉ là chuyển con trỏ tới frame trước đó.

Heap là vùng nhớ phân bổ động cho chương trình. Không giống stack, Heap không có được ốp dưới thuật toán và cấu trúc dữ liệu quá đặc thù nào; các ô nhớ tại heap có để được phân bổ và giải phóng bất kỳ lúc nào. Việc nhận biết một ô nhớ nào đó tại heap có đang được phân bổ hay không do đó trở nên khó khăn hơn; dẫn đến có khá nhiều công cụ giúp việc phân bổ ô nhớ tại heap hiệu quả hơn được tạo ra.

Hệ điều hành phân bổ một (và thường chỉ một) heap cho một chương trình, nhưng phân bổ một stack cho mỗi thread của chương trình đó.

Dưới đây là một minh họa đơn giản về dữ liệu nằm trên stack và heap.

Stack nhanh hơn

Stack nhanh hơn heap là điều không phải bàn cãi. Có nhiều nguyên nhân.

Bản chất cấu trúc dữ liệu khiến ô nhớ của stack có thể được phân bổ và giải phóng rất nhanh, điều không có được ở heap.

Đọc dữ liệu tại stack không cần phải dựa vào các con trỏ. Khi bước vào một scope thì phân bổ một frame, frame này dài bao nhiêu là biết được trước dựa vào kiểu dữ liệu và thành phần biến trong scope. Biến thứ 3 khai báo trong scope bắt đầu từ byte thứ mấy là cũng suy ra được từ kiểu dữ liệu, không cần viện tới con trỏ. Do đó tốc độ đọc rất nhanh.

Mỗi byte của stack đều được sử dụng rất thường xuyên và vì lý do đó nó thường được gắn vào cache của vi xử lý, điều này cũng ảnh hưởng tới tốc độ.

Trái lại, phân bổ hay giải phóng bất kỳ thứ gì ở heap cũng đều phải đề phòng memory leak, loằng ngoằng hơn stack nhiều.

Mọi thứ ở heap muốn truy cập tới được thì đều cần viện đến cơ chế con trỏ, cơ chế này cũng chậm chứ không nhanh.

Heap thường được sử dụng ở phạm vi toàn cục, buộc phải áp dụng an toàn luồng — và do đó mọi thao tác phân bổ và giải phóng đều phải là thao tác synchronize có hiệu năng tệ hơn.

Tuy vậy,

Stack không phải vô đối

Stack gắn chặt với thread

Các frame của một stack là để cho thread của nó đọc, thread khác không thể can thiệp, và do đó dữ liệu của thread không có tính global. Nếu có một dữ liệu global, ta sẽ không thể đặt nó tại stack.

Stack gắn chặt với scope

Frame trên stack được phân bổ mỗi khi sự thực thi đi vào một scope (lời gọi hàm chẳng hạn). Sau khi thoát ra khỏi scope, frame này sẽ được giải phóng, điều này gây hệ lụy tương tự như ý trên, nếu có những dữ liệu global, ta không thể đặt nó tại stack.

Stack chỉ lưu chứa được những dữ liệu có độ dài bit không đổi

Stack không thể lưu chứa những thứ có khả năng thay đổi độ dài bit khi function đang thực thi — sẽ không có cách nào phân bổ và giải phóng bộ nhớ hiệu quả. Dữ liệu String là ví dụ kinh điển nhất.

Stack thường không có dung lượng lớn

Do stack thường được phân bổ tại cache của CPU — một không gian nhớ nhỏ hẹp, việc phình lớn stack thường gây nhiều hệ lụy không mong muốn. Do đó, stack chưa bao giờ được phân bổ bộ nhớ một cách thả cửa mà thường bị giới hạn một dung lượng rất nhỏ.

Stack không thể lưu chứa dữ liệu tham chiếu

Nếu lưu chứa dữ liệu tham chiếu lên stack, dữ liệu đó sẽ biến mất ngay tắp lự sau khi scope chấm dứt thực thi, và làm tất cả các tham chiếu chỉ tới dữ liệu đó chết sạch.

Trong đa số các ngôn ngữ, các đối tượng luôn là dữ liệu tham chiếu, do đó, không thể nào chương trình hoạt động mà không cần tới vùng nhớ heap được.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade