Vì vậy, đây không phải là một bộ nhớ trên đầu?
Có .... không ... có lẽ?
Đây là một câu hỏi khó xử vì hãy tưởng tượng phạm vi địa chỉ bộ nhớ trên máy và một phần mềm cần liên tục theo dõi vị trí của mọi thứ trong bộ nhớ theo cách không thể gắn với ngăn xếp.
Ví dụ: hãy tưởng tượng một trình phát nhạc trong đó tệp nhạc được tải bởi người dùng ấn nút và dỡ khỏi bộ nhớ dễ bay hơi khi người dùng cố tải một tệp nhạc khác.
Làm thế nào để chúng tôi theo dõi nơi dữ liệu âm thanh được lưu trữ? Chúng tôi cần một địa chỉ bộ nhớ cho nó. Chương trình không chỉ cần theo dõi đoạn dữ liệu âm thanh trong bộ nhớ mà còn ở nơi nó nằm trong bộ nhớ. Vì vậy, chúng ta cần giữ xung quanh một địa chỉ bộ nhớ (nghĩa là một con trỏ). Và kích thước của bộ lưu trữ cần thiết cho địa chỉ bộ nhớ sẽ khớp với phạm vi địa chỉ của máy (ví dụ: con trỏ 64 bit cho phạm vi địa chỉ 64 bit).
Vì vậy, đó là loại "có", nó yêu cầu lưu trữ để theo dõi địa chỉ bộ nhớ, nhưng không phải là chúng ta có thể tránh nó cho bộ nhớ được phân bổ động của loại này.
Điều này được bồi thường như thế nào?
Nói về kích thước của chính con trỏ, bạn có thể tránh chi phí trong một số trường hợp bằng cách sử dụng ngăn xếp, ví dụ: Trong trường hợp đó, trình biên dịch có thể tạo hướng dẫn mã hóa hiệu quả địa chỉ bộ nhớ tương đối, tránh chi phí của con trỏ. Tuy nhiên, điều này khiến bạn dễ bị chồng chất nếu bạn làm điều này cho các phân bổ lớn, có kích thước thay đổi và cũng có xu hướng không thực tế (nếu không hoàn toàn không thể) để thực hiện cho một loạt các nhánh phức tạp do đầu vào của người dùng điều khiển (như trong ví dụ âm thanh ở trên).
Một cách khác là sử dụng các cấu trúc dữ liệu liền kề hơn. Ví dụ, một chuỗi dựa trên mảng có thể được sử dụng thay vì danh sách liên kết đôi đòi hỏi hai con trỏ trên mỗi nút. Chúng ta cũng có thể sử dụng kết hợp cả hai giống như một danh sách không được kiểm soát, chỉ lưu trữ các con trỏ ở giữa mỗi nhóm N phần tử liền kề.
Là con trỏ được sử dụng trong thời gian ứng dụng bộ nhớ thấp quan trọng?
Có, rất phổ biến, vì nhiều ứng dụng quan trọng về hiệu năng được viết bằng C hoặc C ++, bị chi phối bởi việc sử dụng con trỏ (chúng có thể nằm sau một con trỏ thông minh hoặc một thùng chứa như std::vector
hoặc std::string
, nhưng các cơ chế cơ bản luồn xuống một con trỏ được sử dụng để theo dõi địa chỉ đến một khối bộ nhớ động).
Bây giờ trở lại câu hỏi này:
Điều này được bồi thường như thế nào? (Phần hai)
Con trỏ thường rất rẻ trừ khi bạn lưu trữ như một triệu trong số chúng (vẫn là 8 megabyte trên máy 64 bit).
* Lưu ý như Ben đã chỉ ra rằng 8 megs "sởi" vẫn là kích thước của bộ đệm L3. Ở đây tôi đã sử dụng "bệnh sởi" nhiều hơn theo nghĩa tổng sử dụng DRAM và kích thước tương đối điển hình cho khối bộ nhớ mà việc sử dụng con trỏ khỏe mạnh sẽ chỉ ra.
Con trỏ đắt ở đâu không phải là con trỏ mà là:
Phân bổ bộ nhớ động. Phân bổ bộ nhớ động có xu hướng đắt tiền vì nó phải trải qua cấu trúc dữ liệu cơ bản (ví dụ: bộ cấp phát bạn bè hoặc bản sàn). Mặc dù những thứ này thường được tối ưu hóa cho đến chết, chúng có mục đích chung và được thiết kế để xử lý các khối có kích thước thay đổi, đòi hỏi chúng phải thực hiện ít nhất một chút công việc giống như "tìm kiếm" (mặc dù nhẹ và thậm chí có thể là thời gian không đổi) tìm một bộ miễn phí các trang liền kề trong bộ nhớ.
Truy cập bộ nhớ. Điều này có xu hướng là chi phí lớn hơn để lo lắng về. Bất cứ khi nào chúng tôi truy cập bộ nhớ được cấp phát lần đầu tiên, sẽ có một lỗi trang bắt buộc cũng như bộ nhớ cache bị mất khi di chuyển bộ nhớ xuống hệ thống phân cấp bộ nhớ và xuống một thanh ghi.
Truy cập bộ nhớ
Truy cập bộ nhớ là một trong những khía cạnh quan trọng nhất của hiệu suất ngoài thuật toán. Rất nhiều lĩnh vực quan trọng về hiệu năng như công cụ trò chơi AAA tập trung rất nhiều năng lượng của họ vào việc tối ưu hóa theo định hướng dữ liệu, giúp cải thiện các kiểu và bố cục truy cập bộ nhớ hiệu quả hơn.
Một trong những khó khăn hiệu năng lớn nhất của các ngôn ngữ cấp cao hơn muốn phân bổ riêng từng loại do người dùng xác định thông qua bộ thu gom rác, ví dụ, chúng có thể phân mảnh bộ nhớ khá nhiều. Điều này có thể đặc biệt đúng nếu không phải tất cả các đối tượng được phân bổ cùng một lúc.
Trong các trường hợp đó, nếu bạn lưu trữ danh sách một triệu trường hợp của loại đối tượng do người dùng xác định, việc truy cập các trường hợp đó theo trình tự trong một vòng lặp có thể khá chậm vì nó tương tự như danh sách một triệu con trỏ trỏ đến các vùng bộ nhớ khác nhau. Trong những trường hợp đó, kiến trúc muốn tìm nạp bộ nhớ ở cấp trên, cấp độ chậm hơn, lớn hơn của hệ thống phân cấp theo khối lớn, được căn chỉnh với hy vọng rằng dữ liệu xung quanh trong các khối đó sẽ được truy cập trước khi bị trục xuất. Khi mỗi đối tượng trong danh sách như vậy được phân bổ riêng biệt, thì cuối cùng chúng ta sẽ trả tiền cho nó bằng các lỗi nhớ cache khi mỗi lần lặp tiếp theo có thể phải tải từ một khu vực hoàn toàn khác trong bộ nhớ mà không có đối tượng liền kề nào được truy cập trước khi bị trục xuất.
Ngày nay, rất nhiều trình biên dịch cho các ngôn ngữ như vậy đang thực hiện công việc tuyệt vời khi lựa chọn hướng dẫn và phân bổ đăng ký, nhưng việc thiếu kiểm soát trực tiếp hơn đối với việc quản lý bộ nhớ ở đây có thể là kẻ giết người (mặc dù thường ít bị lỗi hơn) và vẫn tạo ra các ngôn ngữ như C và C ++ khá phổ biến.
Tối ưu hóa truy cập con trỏ gián tiếp
Trong các tình huống quan trọng nhất về hiệu năng, các ứng dụng thường sử dụng nhóm bộ nhớ chứa bộ nhớ từ các khối liền kề để cải thiện vị trí tham chiếu. Trong các trường hợp như vậy, ngay cả một cấu trúc được liên kết như cây hoặc danh sách được liên kết cũng có thể được tạo thân thiện với bộ đệm với điều kiện là bố cục bộ nhớ của các nút của nó liền kề nhau về bản chất. Điều này có hiệu quả làm cho hội thảo con trỏ rẻ hơn, mặc dù gián tiếp bằng cách cải thiện địa phương của tài liệu tham khảo khi tham gia hội nghị.
Đuổi theo xung quanh
Giả sử chúng ta có một danh sách liên kết đơn lẻ như:
Foo->Bar->Baz->null
Vấn đề là nếu chúng ta phân bổ tất cả các nút này một cách riêng biệt cho một công cụ phân bổ mục đích chung (và có thể không phải tất cả cùng một lúc), bộ nhớ thực tế có thể bị phân tán phần nào như thế này (sơ đồ đơn giản):
Khi chúng tôi bắt đầu đuổi theo con trỏ xung quanh và truy cập vào Foo
nút, chúng tôi bắt đầu với một lỗi bắt buộc (và có thể là lỗi trang) di chuyển một đoạn từ vùng nhớ của nó từ vùng nhớ chậm hơn sang vùng nhớ nhanh hơn, như vậy:
Điều này khiến chúng tôi lưu trữ bộ đệm (có thể cả trang) một vùng bộ nhớ chỉ để truy cập vào một phần của nó và đuổi phần còn lại khi chúng tôi đuổi theo con trỏ xung quanh danh sách này. Tuy nhiên, bằng cách kiểm soát bộ cấp phát bộ nhớ, chúng ta có thể phân bổ một danh sách liên tục như vậy:
... và do đó cải thiện đáng kể tốc độ mà chúng ta có thể hủy bỏ các con trỏ này và xử lý các điểm của chúng. Vì vậy, mặc dù rất gián tiếp, chúng ta có thể tăng tốc truy cập con trỏ theo cách này. Tất nhiên, nếu chúng ta chỉ lưu trữ chúng liên tục trong một mảng, chúng ta sẽ không gặp vấn đề này ngay từ đầu, nhưng bộ cấp phát bộ nhớ ở đây cho phép chúng ta kiểm soát rõ ràng bố cục bộ nhớ có thể tiết kiệm được ngày khi cần cấu trúc được liên kết.
* Lưu ý: đây là sơ đồ và thảo luận rất đơn giản về phân cấp bộ nhớ và địa phương của tài liệu tham khảo, nhưng hy vọng nó phù hợp với cấp độ của câu hỏi.