Mối quan tâm cụ thể đối với ngôn ngữ C ++
Trước hết, không có cái gọi là phân bổ "stack" hay "heap" được ủy quyền bởi C ++ . Nếu bạn đang nói về các đối tượng tự động trong phạm vi khối, chúng thậm chí không được "phân bổ". (BTW, thời gian lưu trữ tự động trong C chắc chắn là KHÔNG như vậy để "phân bổ", sau này là "năng động" trong C ++ cách nói.) Bộ nhớ cấp phát động là trên cửa hàng miễn phí , không nhất thiết phải vào "đống", mặc dù sau này thường là việc thực hiện (mặc định) .
Mặc dù theo quy tắc ngữ nghĩa của máy trừu tượng , các đối tượng tự động vẫn chiếm bộ nhớ, việc triển khai C ++ tuân thủ được phép bỏ qua thực tế này khi nó có thể chứng minh điều này không quan trọng (khi nó không thay đổi hành vi có thể quan sát được của chương trình). Quyền này được cấp bởi quy tắc as-if trong ISO C ++, đây cũng là điều khoản chung cho phép tối ưu hóa thông thường (và cũng có một quy tắc gần như tương tự trong ISO C). Bên cạnh quy tắc as-if, ISO C ++ cũng các quy tắc bầu chọn sao chépphải cho phép bỏ qua các sáng tạo cụ thể của các đối tượng. Do đó, các hàm gọi và hàm hủy có liên quan được bỏ qua. Kết quả là, các đối tượng tự động (nếu có) trong các hàm tạo và hàm hủy này cũng bị loại bỏ, so với ngữ nghĩa trừu tượng ngây thơ ngụ ý bởi mã nguồn.
Mặt khác, phân bổ cửa hàng miễn phí chắc chắn là "phân bổ" theo thiết kế. Theo quy tắc ISO C ++, việc phân bổ như vậy có thể đạt được bằng cách gọi hàm phân bổ . Tuy nhiên, kể từ ISO C ++ 14, có một quy tắc mới (không phải là nếu) để cho phép hợp nhất ::operator new
các lệnh gọi hàm phân bổ toàn cầu (nghĩa là ) trong các trường hợp cụ thể. Vì vậy, các bộ phận của hoạt động phân bổ động cũng có thể không hoạt động như trường hợp của các đối tượng tự động.
Các chức năng phân bổ phân bổ tài nguyên của bộ nhớ. Các đối tượng có thể được phân bổ thêm dựa trên phân bổ sử dụng phân bổ. Đối với các đối tượng tự động, chúng được trình bày trực tiếp - mặc dù bộ nhớ bên dưới có thể được truy cập và được sử dụng để cung cấp bộ nhớ cho các đối tượng khác (theo vị trí new
), nhưng điều này không có ý nghĩa lớn như cửa hàng miễn phí, vì không có cách nào để di chuyển tài nguyên ở nơi khác.
Tất cả các mối quan tâm khác nằm ngoài phạm vi của C ++. Tuy nhiên, chúng có thể vẫn còn đáng kể.
Về triển khai C ++
C ++ không để lộ các bản ghi kích hoạt hợp nhất hoặc một số loại tiếp tục hạng nhất (ví dụ như nổi tiếng call/cc
), không có cách nào để thao tác trực tiếp các khung bản ghi kích hoạt - nơi thực hiện cần đặt các đối tượng tự động. Khi không có sự tương tác (không di động) với triển khai cơ bản (mã không di động "gốc", chẳng hạn như mã lắp ráp nội tuyến), việc bỏ qua phân bổ cơ bản của các khung có thể khá nhỏ. Ví dụ, khi hàm được gọi được nội tuyến, các khung có thể được hợp nhất một cách hiệu quả với các hàm khác, vì vậy không có cách nào để hiển thị "phân bổ" là gì.
Tuy nhiên, một khi sự can thiệp được tôn trọng, mọi thứ đang trở nên phức tạp. Một triển khai điển hình của C ++ sẽ cho thấy khả năng can thiệp vào ISA (kiến trúc tập lệnh) với một số quy ước gọi là ranh giới nhị phân được chia sẻ với mã gốc (máy cấp độ ISA). Điều này sẽ rất tốn kém, đáng chú ý, khi duy trì con trỏ ngăn xếp , thường được giữ trực tiếp bởi một thanh ghi cấp độ ISA (với các hướng dẫn máy cụ thể có thể truy cập). Con trỏ ngăn xếp chỉ ra ranh giới của khung trên cùng của lệnh gọi hàm (hiện đang hoạt động). Khi một lệnh gọi hàm được nhập, một khung mới là cần thiết và con trỏ ngăn xếp được thêm hoặc bớt (tùy theo quy ước của ISA) bởi một giá trị không nhỏ hơn kích thước khung yêu cầu. Khung được phân bổkhi con trỏ ngăn xếp sau các hoạt động. Các tham số của các chức năng cũng có thể được truyền vào khung ngăn xếp, tùy thuộc vào quy ước gọi được sử dụng cho cuộc gọi. Khung có thể chứa bộ nhớ của các đối tượng tự động (có thể bao gồm các tham số) được chỉ định bởi mã nguồn C ++. Theo nghĩa thực hiện như vậy, các đối tượng này được "phân bổ". Khi điều khiển thoát khỏi lệnh gọi hàm, khung không còn cần thiết nữa, nó thường được giải phóng bằng cách khôi phục con trỏ ngăn xếp trở lại trạng thái trước cuộc gọi (được lưu trước đó theo quy ước gọi). Điều này có thể được xem là "thỏa thuận". Các hoạt động này làm cho bản ghi kích hoạt có hiệu quả cấu trúc dữ liệu LIFO, do đó, nó thường được gọi là " ngăn xếp (cuộc gọi) ".
Bởi vì hầu hết các triển khai C ++ (đặc biệt là các triển khai nhắm mục tiêu mã gốc cấp độ ISA và sử dụng ngôn ngữ hợp ngữ làm đầu ra ngay lập tức của nó) sử dụng các chiến lược tương tự như thế này, nên sơ đồ "phân bổ" khó hiểu là phổ biến. Việc phân bổ như vậy (cũng như thỏa thuận) làm chi tiêu chu kỳ máy và có thể tốn kém khi các cuộc gọi (không được tối ưu hóa) xảy ra thường xuyên, ngay cả khi các cấu trúc vi mô CPU hiện đại có thể được tối ưu hóa phức tạp được triển khai bởi phần cứng cho mẫu mã chung (như sử dụng công cụ ngăn xếp trong thực hiện PUSH
/ POP
hướng dẫn).
Nhưng dù sao, nói chung, sự thật là chi phí phân bổ khung ngăn xếp ít hơn đáng kể so với một cuộc gọi đến chức năng phân bổ vận hành cửa hàng miễn phí (trừ khi nó được tối ưu hóa hoàn toàn) , bản thân nó có thể có hàng trăm (nếu không phải là hàng triệu :-) hoạt động để duy trì con trỏ ngăn xếp và các trạng thái khác. Các chức năng phân bổ thường dựa trên API được cung cấp bởi môi trường được lưu trữ (ví dụ: thời gian chạy do HĐH cung cấp). Khác với mục đích giữ các đối tượng tự động cho các cuộc gọi chức năng, các phân bổ như vậy có mục đích chung, vì vậy chúng sẽ không có cấu trúc khung như một ngăn xếp. Theo truyền thống, họ phân bổ không gian từ kho lưu trữ gọi là heap (hoặc một vài đống). Khác với "ngăn xếp", khái niệm "heap" ở đây không chỉ ra cấu trúc dữ liệu đang được sử dụng;nó bắt nguồn từ việc thực hiện ngôn ngữ sớm từ nhiều thập kỷ trước . (BTW, ngăn xếp cuộc gọi thường được phân bổ với kích thước cố định hoặc do người dùng chỉ định từ vùng heap theo môi trường khi khởi động chương trình hoặc luồng.) Bản chất của các trường hợp sử dụng khiến việc phân bổ và giải quyết từ một đống phức tạp hơn nhiều so với đẩy hoặc bật ngăn xếp khung) và hầu như không thể được tối ưu hóa trực tiếp bằng phần cứng.
Hiệu ứng truy cập bộ nhớ
Phân bổ ngăn xếp thông thường luôn đặt khung mới lên hàng đầu, vì vậy nó có một địa phương khá tốt. Điều này là thân thiện với bộ nhớ cache. OTOH, bộ nhớ được phân bổ ngẫu nhiên trong cửa hàng miễn phí không có thuộc tính đó. Kể từ ISO C ++ 17, có các mẫu tài nguyên nhóm được cung cấp bởi <memory>
. Mục đích trực tiếp của giao diện như vậy là cho phép kết quả phân bổ liên tiếp gần nhau trong bộ nhớ. Điều này thừa nhận thực tế rằng chiến lược này thường tốt cho hiệu suất với các triển khai hiện đại, ví dụ như thân thiện với bộ đệm trong các kiến trúc hiện đại. Đây là về hiệu suất truy cập hơn là phân bổ , mặc dù.
Đồng thời
Kỳ vọng truy cập đồng thời vào bộ nhớ có thể có các hiệu ứng khác nhau giữa ngăn xếp và đống. Một ngăn xếp cuộc gọi thường được sở hữu độc quyền bởi một luồng thực thi trong triển khai C ++. OTOH, đống thường được chia sẻ giữa các luồng trong một quy trình. Đối với các đống như vậy, các hàm phân bổ và phân bổ phải bảo vệ cấu trúc dữ liệu quản trị nội bộ được chia sẻ khỏi cuộc đua dữ liệu. Do đó, phân bổ heap và thỏa thuận có thể có thêm chi phí do hoạt động đồng bộ hóa nội bộ.
Hiệu quả không gian
Do tính chất của các trường hợp sử dụng và cấu trúc dữ liệu bên trong, các đống có thể bị phân mảnh bộ nhớ trong, trong khi ngăn xếp thì không. Điều này không có tác động trực tiếp đến hiệu suất phân bổ bộ nhớ, nhưng trong một hệ thống có bộ nhớ ảo , hiệu suất không gian thấp có thể làm suy giảm hiệu suất tổng thể của việc truy cập bộ nhớ. Điều này đặc biệt khủng khiếp khi HDD được sử dụng như một sự trao đổi bộ nhớ vật lý. Nó có thể gây ra độ trễ khá dài - đôi khi là hàng tỷ chu kỳ.
Hạn chế của phân bổ ngăn xếp
Mặc dù phân bổ ngăn xếp thường có hiệu suất vượt trội so với phân bổ heap trong thực tế, nhưng chắc chắn không có nghĩa là phân bổ ngăn xếp luôn có thể thay thế phân bổ heap.
Đầu tiên, không có cách nào để phân bổ không gian trên ngăn xếp với kích thước được chỉ định trong thời gian chạy theo cách di động với ISO C ++. Có các phần mở rộng được cung cấp bởi các triển khai như alloca
và VLA của G ++ (mảng có độ dài thay đổi), nhưng có những lý do để tránh chúng. (IIRC, nguồn Linux loại bỏ việc sử dụng VLA gần đây.) (Cũng lưu ý rằng ISO C99 không bắt buộc phải có VLA, nhưng ISO C11 biến tùy chọn hỗ trợ.)
Thứ hai, không có cách nào đáng tin cậy và di động để phát hiện cạn kiệt không gian ngăn xếp. Điều này thường được gọi là tràn ngăn xếp (hmm, từ nguyên của trang web này) , nhưng có lẽ chính xác hơn, chồng tràn . Trong thực tế, điều này thường gây ra truy cập bộ nhớ không hợp lệ và trạng thái của chương trình sau đó bị hỏng (... hoặc có thể tệ hơn là lỗ hổng bảo mật). Trên thực tế, ISO C ++ không có khái niệm về "ngăn xếp" và khiến nó không được xác định hành vi khi tài nguyên cạn kiệt . Hãy thận trọng về việc nên để lại bao nhiêu phòng cho các đối tượng tự động.
Nếu không gian ngăn xếp hết, có quá nhiều đối tượng được phân bổ trong ngăn xếp, điều này có thể được gây ra bởi quá nhiều lệnh gọi chức năng hoạt động hoặc sử dụng không đúng đối tượng tự động. Các trường hợp như vậy có thể gợi ý sự tồn tại của các lỗi, ví dụ như một hàm gọi đệ quy mà không có điều kiện thoát chính xác.
Tuy nhiên, các cuộc gọi đệ quy sâu đôi khi được mong muốn. Trong việc triển khai các ngôn ngữ yêu cầu hỗ trợ các cuộc gọi hoạt động không liên kết (trong đó độ sâu cuộc gọi chỉ bị giới hạn bởi tổng bộ nhớ), không thể sử dụng trực tiếp ngăn xếp cuộc gọi gốc (hiện đại) làm bản ghi kích hoạt ngôn ngữ đích như các triển khai C ++ thông thường. Để khắc phục sự cố, cần có các cách khác để xây dựng hồ sơ kích hoạt. Ví dụ, SML / NJ phân bổ rõ ràng các khung trên heap và sử dụng các ngăn xếp xương rồng . Việc phân bổ phức tạp các khung bản ghi kích hoạt như vậy thường không nhanh bằng các khung ngăn xếp cuộc gọi. Tuy nhiên, nếu các ngôn ngữ đó được triển khai hơn nữa với sự đảm bảo của đệ quy đuôi thích hợp, phân bổ ngăn xếp trực tiếp trong ngôn ngữ đối tượng (nghĩa là "đối tượng" trong ngôn ngữ không được lưu trữ dưới dạng tham chiếu, nhưng các giá trị nguyên thủy nguyên gốc có thể được ánh xạ một đến một đối tượng C ++ không được chia sẻ) thậm chí còn phức tạp hơn với thực hiện phạt chung. Khi sử dụng C ++ để thực hiện các ngôn ngữ như vậy, rất khó để ước tính các tác động hiệu suất.