std :: vector (ab) sử dụng lưu trữ tự động


46

Hãy xem xét đoạn trích sau:

#include <array>
int main() {
  using huge_type = std::array<char, 20*1024*1024>;
  huge_type t;
}

Rõ ràng là nó sẽ sập trên hầu hết các nền tảng, vì kích thước ngăn xếp mặc định thường nhỏ hơn 20MB.

Bây giờ hãy xem xét các mã sau đây:

#include <array>
#include <vector>

int main() {
  using huge_type = std::array<char, 20*1024*1024>;
  std::vector<huge_type> v(1);
}

Đáng ngạc nhiên là nó cũng gặp sự cố! Truy nguyên (với một trong các phiên bản libstdc ++ gần đây) dẫn đến include/bits/stl_uninitialized.htệp, nơi chúng ta có thể thấy các dòng sau:

typedef typename iterator_traits<_ForwardIterator>::value_type _ValueType;
std::fill(__first, __last, _ValueType());

Hàm vectorxây dựng thay đổi kích thước phải khởi tạo mặc định các phần tử và đây là cách nó được triển khai. Rõ ràng, _ValueType()tạm thời sụp đổ các ngăn xếp.

Câu hỏi là liệu đó có phải là một triển khai phù hợp. Nếu có, điều đó thực sự có nghĩa là việc sử dụng một loại vectơ khổng lồ khá hạn chế, phải không?


Không nên lưu trữ các đối tượng lớn trong một kiểu mảng. Làm như vậy có khả năng đòi hỏi một vùng rất lớn của bộ nhớ liên kết có thể không có mặt. Thay vào đó, hãy có một vectơ con trỏ (std :: unique_ptr điển hình) để bạn không đặt yêu cầu cao như vậy vào bộ nhớ của mình.
NathanOliver

2
Chỉ là ký ức. Có các triển khai C ++ đang chạy không sử dụng bộ nhớ ảo.
NathanOliver

3
Trình biên dịch nào, btw? Tôi không thể sao chép với VS 2019 (16.4.2)
ChrisMM

3
Từ việc xem xét mã libstdc ++, việc triển khai này chỉ được sử dụng nếu loại phần tử là tầm thường và sao chép có thể gán và nếu mặc định std::allocatorđược sử dụng.
quả óc chó

1
@Damon Như tôi đã đề cập ở trên, dường như chỉ được sử dụng cho các loại tầm thường với bộ cấp phát mặc định, do đó không nên có bất kỳ sự khác biệt nào có thể quan sát được.
quả óc chó

Câu trả lời:


19

Không có giới hạn về số lượng lưu trữ tự động mà bất kỳ API std nào sử dụng.

Tất cả chúng có thể cần 12 terabyte không gian ngăn xếp.

Tuy nhiên, API đó chỉ yêu cầu Cpp17DefaultInsertablevà việc triển khai của bạn tạo ra một thể hiện bổ sung đối với những gì được yêu cầu bởi nhà xây dựng. Trừ khi nó bị kiểm soát đằng sau việc phát hiện đối tượng là tầm thường và có thể sao chép, việc thực hiện đó có vẻ bất hợp pháp.


8
Từ việc xem xét mã libstdc ++, việc triển khai này chỉ được sử dụng nếu loại phần tử là tầm thường và sao chép có thể gán và nếu mặc định std::allocatorđược sử dụng. Tôi không chắc tại sao trường hợp đặc biệt này được thực hiện ở nơi đầu tiên.
quả óc chó

3
@walnut Có nghĩa là trình biên dịch có thể tự do nếu không thực sự tạo đối tượng tạm thời đó; Tôi đoán có một cơ hội tốt trên một bản dựng được tối ưu hóa mà nó không được tạo?
Yakk - Adam Nevraumont

4
Vâng, tôi đoán nó có thể, nhưng đối với các yếu tố lớn GCC dường như không. Clang với libstdc ++ không tối ưu hóa tạm thời, nhưng dường như chỉ khi kích thước vectơ được truyền cho hàm tạo là hằng số thời gian biên dịch, xem godbolt.org/z/-2ZDMm .
quả óc chó

1
@walnut trường hợp đặc biệt là ở đó để chúng tôi gửi đến std::fillcác loại tầm thường, sau đó sử dụng memcpyđể tạo các byte vào các vị trí, có khả năng nhanh hơn nhiều so với việc xây dựng nhiều đối tượng riêng lẻ trong một vòng lặp. Tôi tin rằng việc triển khai libstdc ++ là phù hợp, nhưng gây ra lỗi tràn ngăn xếp cho các đối tượng lớn là lỗi Chất lượng thực hiện (QoI). Tôi đã báo cáo là gcc.gnu.org/PR94540 và sẽ sửa nó.
Jonathan Wakely

@JonathanWakely Vâng, điều đó có ý nghĩa. Tôi không nhớ tại sao tôi không nghĩ về điều đó khi tôi viết bình luận của mình. Tôi đoán tôi đã nghĩ rằng phần tử được xây dựng mặc định đầu tiên sẽ được xây dựng trực tiếp tại chỗ và sau đó người ta có thể sao chép từ đó, để không có đối tượng bổ sung nào của loại phần tử sẽ được xây dựng. Nhưng tất nhiên tôi chưa thực sự nghĩ đến điều này một cách chi tiết và tôi không biết trong và ngoài việc thực hiện thư viện chuẩn. (Tôi nhận ra quá muộn rằng đây cũng là đề xuất của bạn trong báo cáo lỗi.)
quả óc chó

9
huge_type t;

Rõ ràng là nó sẽ sụp đổ trên hầu hết các nền tảng ...

Tôi tranh luận về giả định "nhất". Vì bộ nhớ của đối tượng khổng lồ không bao giờ được sử dụng, trình biên dịch hoàn toàn có thể bỏ qua nó và không bao giờ phân bổ bộ nhớ trong trường hợp đó sẽ không có sự cố.

Câu hỏi là liệu đó có phải là một triển khai phù hợp.

Tiêu chuẩn C ++ không giới hạn việc sử dụng ngăn xếp hoặc thậm chí thừa nhận sự tồn tại của ngăn xếp. Vì vậy, có nó phù hợp với tiêu chuẩn. Nhưng người ta có thể coi đây là một vấn đề chất lượng thực hiện.

điều đó thực sự có nghĩa là việc sử dụng một loại vectơ khổng lồ khá hạn chế phải không?

Điều đó dường như là trường hợp với libstdc ++. Sự cố không được sao chép bằng libc ++ (sử dụng tiếng kêu), vì vậy có vẻ như đây không phải là giới hạn trong ngôn ngữ, mà chỉ là trong triển khai cụ thể đó.


6
"Sẽ không nhất thiết bị sập mặc dù tràn ngăn xếp vì bộ nhớ được phân bổ không bao giờ được chương trình truy cập" - nếu ngăn xếp được sử dụng theo bất kỳ cách nào sau này (ví dụ để gọi một hàm), điều này sẽ sụp đổ ngay cả trên các nền tảng quá mức cam kết .
Ruslan

Bất kỳ nền tảng nào mà điều này không gặp sự cố (giả sử đối tượng không được phân bổ thành công) đều dễ bị Stack Clash.
dùng253751

@ user253751 Sẽ là lạc quan khi cho rằng hầu hết các nền tảng / chương trình không dễ bị tổn thương.
eerorika

Tôi nghĩ rằng overcommit chỉ áp dụng cho heap, không phải stack. Các ngăn xếp có một giới hạn trên cố định trên kích thước của nó.
Jonathan Wakely

@JonathanWakely Bạn nói đúng. Có vẻ như lý do tại sao nó không sụp đổ là do trình biên dịch không bao giờ phân bổ đối tượng không được sử dụng.
eerorika

5

Tôi không phải là một luật sư ngôn ngữ cũng không phải là một chuyên gia tiêu chuẩn C ++, nhưng cppreference.com nói:

explicit vector( size_type count, const Allocator& alloc = Allocator() );

Xây dựng bộ chứa với các phiên bản được chèn mặc định của T. Không có bản sao nào được tạo.

Có lẽ tôi đang hiểu nhầm "được chèn mặc định", nhưng tôi sẽ mong đợi:

std::vector<huge_type> v(1);

tương đương với

std::vector<huge_type> v;
v.emplace_back();

Phiên bản sau không nên tạo một bản sao ngăn xếp mà xây dựng một Huge_type trực tiếp trong bộ nhớ động của vectơ.

Tôi không thể ủy quyền nói rằng những gì bạn đang thấy là không tuân thủ, nhưng chắc chắn đó không phải là điều tôi mong đợi từ một triển khai chất lượng.


4
Như tôi đã đề cập trong một nhận xét về câu hỏi, libstdc ++ chỉ sử dụng triển khai này cho các loại tầm thường với gán sao chép và std::allocatordo đó, không nên có sự khác biệt có thể quan sát được giữa việc chèn trực tiếp vào bộ nhớ vectơ và tạo một bản sao trung gian.
quả óc chó

@walnut: Đúng, nhưng phân bổ ngăn xếp khổng lồ và tác động hiệu suất của init và sao chép vẫn là những điều tôi không mong đợi từ một triển khai chất lượng cao.
Adrian McCarthy

2
Vâng tôi đồng ý. Tôi nghĩ rằng đây là một sự giám sát trong việc thực hiện. Quan điểm của tôi chỉ là nó không quan trọng về mặt tuân thủ tiêu chuẩn.
quả óc chó

IIRC bạn cũng cần khả năng sao chép hoặc di chuyển emplace_backnhưng không chỉ để tạo một vectơ. Điều đó có nghĩa là bạn có thể có vector<mutex> v(1)nhưng không phải vector<mutex> v; v.emplace_back();Đối với những thứ như huge_typebạn vẫn có thể có sự phân bổ và di chuyển hoạt động nhiều hơn với phiên bản thứ hai. Không nên tạo đối tượng tạm thời.
dyp

1
@IgorR. vector::vector(size_type, Allocator const&)yêu cầu (Cpp17) DefaultInsertable
dyp
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.