Dung lượng ban đầu của vector trong C ++


92

Cái gì trong capacity()số một std::vectorcái được tạo bằng cách sử dụng hằng số mặc định? Tôi biết rằng size()số không. Chúng ta có thể nói rằng một vectơ được xây dựng mặc định không gọi cấp phát bộ nhớ heap không?

Bằng cách này, có thể tạo một mảng với một khoản dự trữ tùy ý bằng cách sử dụng một phân bổ duy nhất, chẳng hạn như std::vector<int> iv; iv.reserve(2345);. Hãy nói rằng vì một số lý do, tôi không muốn bắt đầu size()vào 23 giờ 45.

Ví dụ: trên Linux (g ++ 4.4.5, kernel 2.6.32 amd64)

#include <iostream>
#include <vector>

int main()
{
  using namespace std;
  cout << vector<int>().capacity() << "," << vector<int>(10).capacity() << endl;
  return 0;
}

đã in 0,10. Nó là một quy tắc, hay nó phụ thuộc vào nhà cung cấp STL?


7
Tiêu chuẩn không chỉ định bất cứ điều gì về dung lượng ban đầu của vectơ nhưng hầu hết các triển khai sử dụng 0.
Mr.Anubis

11
Không có gì đảm bảo, nhưng tôi sẽ nghiêm túc đặt câu hỏi về chất lượng của bất kỳ triển khai nào đã cấp phát bộ nhớ mà không cần tôi yêu cầu.
Mike Seymour

2
@MikeSeymour Không đồng ý. Một triển khai hiệu suất thực sự cao có thể chứa một bộ đệm nội tuyến nhỏ, trong trường hợp đó, việc đặt dung lượng ban đầu () thành điều đó sẽ có ý nghĩa.
alastair,

6
@alastair Khi sử dụng swaptất cả các trình lặp và tham chiếu vẫn hợp lệ (ngoại trừ end()các). Điều đó có nghĩa là không thể sử dụng bộ đệm nội tuyến.
Notinlist

Câu trả lời:


74

Tiêu chuẩn không chỉ định phần đầu capacitycủa vùng chứa phải là gì, vì vậy bạn đang dựa vào việc triển khai. Một triển khai thông thường sẽ bắt đầu công suất ở mức 0, nhưng không có gì đảm bảo. Mặt khác, không có cách nào để tốt hơn chiến lược của bạn std::vector<int> iv; iv.reserve(2345);vì vậy hãy kiên trì với nó.


1
Tôi không mua tuyên bố cuối cùng của bạn. Nếu ban đầu bạn không thể dựa vào khả năng là 0, bạn có thể cấu trúc lại chương trình của mình để cho phép vectơ của bạn có kích thước ban đầu. Điều này sẽ giảm một nửa số lượng yêu cầu bộ nhớ heap (từ 2 đến 1).
bitmask

4
@bitmask: Thực tế: bạn có biết bất kỳ cách triển khai nào trong đó vector cấp phát bộ nhớ trong hàm tạo mặc định không? Nó không được đảm bảo bởi tiêu chuẩn, nhưng như Mike Seymour chỉ ra rằng việc kích hoạt phân bổ mà không cần thiết sẽ có mùi khó chịu về chất lượng thực hiện .
David Rodríguez - dribeas

3
@ DavidRodríguez-dribeas: Đó không phải là vấn đề. Tiền đề là "bạn không thể làm tốt hơn chiến lược hiện tại của mình, vì vậy đừng bận tâm tự hỏi nếu có thể có những triển khai ngu ngốc". Nếu tiền đề là "không có triển khai như vậy, vì vậy đừng bận tâm" tôi sẽ mua nó. Kết luận là đúng, nhưng hàm ý không hoạt động. Xin lỗi, có lẽ tôi đang nhặt nitơ.
bitmask

3
@bitmask Nếu tồn tại một triển khai cấp phát bộ nhớ trên cấu trúc mặc định, việc làm như bạn đã nói sẽ giảm một nửa số lượng phân bổ. Nhưng vector::reservekhông giống như chỉ định kích thước ban đầu. Các hàm tạo vectơ nhận giá trị kích thước ban đầu / sao chép khởi tạo ncác đối tượng và do đó có độ phức tạp tuyến tính. OTOH, gọi dự trữ chỉ có nghĩa là sao chép / di chuyển các size()phần tử nếu một phân bổ lại được kích hoạt. Trên một vector trống không có gì để sao chép. Vì vậy, điều sau có thể mong muốn ngay cả khi việc triển khai cấp phát bộ nhớ cho một vectơ được xây dựng mặc định.
Praetorian

4
@bitmask, nếu bạn lo lắng về việc phân bổ cho mức độ này thì bạn nên xem xét việc triển khai thư viện tiêu chuẩn cụ thể của mình và không dựa vào suy đoán.
Mark Ransom

36

Việc triển khai lưu trữ của std :: vector khác nhau đáng kể, nhưng tất cả những cái tôi đã gặp đều bắt đầu từ 0.

Đoạn mã sau:

#include <iostream>
#include <vector>

int main()
{
  using namespace std;

  vector<int> normal;
  cout << normal.capacity() << endl;

  for (unsigned int loop = 0; loop != 10; ++loop)
  {
      normal.push_back(1);
      cout << normal.capacity() << endl;
  }

  cin.get();
  return 0;
}

Cung cấp đầu ra sau:

0
1
2
4
4
8
8
8
8
16
16

theo GCC 5.1 và:

0
1
2
3
4
6
6
9
9
9
13

theo MSVC 2013.


3
Đây là nên đánh giá thấp @ Andrew
Valentin Mercier

Bạn có thể thấy hầu như ở khắp mọi nơi rằng khuyến nghị cho mục đích tốc độ hầu như luôn luôn chỉ sử dụng một vectơ, vì vậy nếu bạn đang làm bất cứ điều gì liên quan đến dữ liệu thưa thớt ...
Andrew

@Andrew họ nên bắt đầu nó lúc nào? cấp phát bất cứ thứ gì sẽ chỉ lãng phí thời gian phân bổ và phân bổ bộ nhớ đó nếu lập trình viên muốn dự trữ nhiều hơn mặc định. nếu bạn giả sử chúng nên bắt đầu bằng 1, nó sẽ phân bổ ngay sau khi ai đó phân bổ 1.
Puddle

@Puddle Bạn đang đọc giữa các dòng thay vì coi nó theo mệnh giá. Đầu mối cho thấy nó không phải là lời mỉa mai là từ "thông minh", cũng như nhận xét thứ hai của tôi đề cập đến dữ liệu thưa thớt.
Andrew

@Andrew Ồ tốt, bạn đã đủ nhẹ nhõm khi họ bắt đầu nó ở mức 0. Tại sao lại bình luận về nó một cách đùa cợt?
Puddle

7

Theo như tôi hiểu tiêu chuẩn (mặc dù tôi thực sự không thể đặt tên cho một tham chiếu), việc cài đặt vùng chứa và phân bổ bộ nhớ đã cố ý được tách ra vì lý do chính đáng. Vì vậy, bạn có các cuộc gọi riêng biệt, riêng biệt cho

  • constructor để tự tạo vùng chứa
  • reserve() phân bổ trước một khối bộ nhớ lớn thích hợp để chứa ít nhất (!) một số lượng đối tượng nhất định

Và điều này rất có ý nghĩa. Quyền duy nhất để tồn tại reserve()là cho bạn cơ hội để viết mã xung quanh việc tái phân bổ có thể tốn kém khi phát triển vectơ. Để trở nên hữu ích, bạn phải biết số lượng đồ vật cần lưu trữ hoặc ít nhất cần phải có khả năng phỏng đoán có học. Nếu điều này không được cung cấp, tốt hơn bạn nên tránh xa reserve()vì bạn sẽ chỉ thay đổi sự phân bổ lại cho bộ nhớ lãng phí.

Vì vậy, tổng hợp tất cả lại với nhau:

  • Tiêu chuẩn cố ý không chỉ định một phương thức khởi tạo cho phép bạn cấp phát trước một khối bộ nhớ cho một số đối tượng cụ thể (điều này ít nhất sẽ tốt hơn so với việc cấp phát một "cái gì đó" cụ thể, cố định dưới mui xe).
  • Phân bổ không được ngầm hiểu. Vì vậy, để phân bổ trước một khối, bạn cần thực hiện một cuộc gọi riêng reserve()và khối này không cần phải ở cùng một địa điểm xây dựng (tất nhiên có thể / nên muộn hơn, sau khi bạn biết về kích thước cần thiết để chứa)
  • Vì vậy, nếu một vectơ luôn định vị trước một khối bộ nhớ có kích thước được xác định thì điều này sẽ làm hỏng công việc dự định của nó reserve(), phải không?
  • Lợi thế của việc định vị trước một khối là gì nếu STL tự nhiên không thể biết mục đích dự định và kích thước dự kiến ​​của một vectơ? Nó sẽ khá vô nghĩa, nếu không muốn nói là phản tác dụng.
  • Giải pháp thích hợp thay vào đó là phân bổ và triển khai khối cụ thể với khối đầu tiên push_back()- nếu chưa được phân bổ rõ ràng trước đó reserve().
  • Trong trường hợp phân bổ lại cần thiết, việc tăng kích thước khối cũng là việc thực hiện cụ thể. Các triển khai vectơ mà tôi biết bắt đầu với sự gia tăng kích thước theo cấp số nhân nhưng sẽ giới hạn tốc độ tăng ở một mức tối đa nhất định để tránh lãng phí một lượng lớn bộ nhớ hoặc thậm chí thổi bay nó.

Tất cả điều này chỉ hoạt động đầy đủ và thuận lợi nếu không bị làm phiền bởi một hàm tạo cấp phát. Bạn có các giá trị mặc định hợp lý cho các tình huống phổ biến có thể được ghi đè theo yêu cầu bởi reserve()(và shrink_to_fit()). Vì vậy, ngay cả khi tiêu chuẩn không nêu rõ ràng như vậy, tôi khá chắc chắn rằng giả định rằng một vectơ mới được xây dựng không phân bổ trước là một đặt cược khá an toàn cho tất cả các triển khai hiện tại.


4

Như một bổ sung nhỏ cho các câu trả lời khác, tôi thấy rằng khi chạy trong điều kiện gỡ lỗi với Visual Studio, một vectơ được xây dựng mặc định sẽ vẫn phân bổ trên heap mặc dù dung lượng bắt đầu bằng 0.

Cụ thể nếu _ITERATOR_DEBUG_LEVEL! = 0 thì vectơ sẽ phân bổ một số không gian để giúp kiểm tra trình lặp.

https://docs.microsoft.com/en-gb/cpp/standard-library/iterator-debug-level

Tôi chỉ thấy điều này hơi khó chịu vì tôi đang sử dụng trình phân bổ tùy chỉnh vào thời điểm đó và không mong đợi phân bổ thêm.


Thật thú vị, chúng phá vỡ các đảm bảo không chấp nhận (ít nhất là đối với C + 17, trước đó?): En.cppreference.com/w/cpp/container/vector/vector
Deduplicator

4

Đây là một câu hỏi cũ, và tất cả các câu trả lời ở đây đã giải thích đúng quan điểm của tiêu chuẩn và cách bạn có thể có được dung lượng ban đầu theo cách di động bằng cách sử dụng std::vector::reserve;

Tuy nhiên, tôi sẽ giải thích lý do tại sao không có ý nghĩa đối với bất kỳ triển khai STL nào để cấp phát bộ nhớ khi xây dựng một std::vector<T>đối tượng ;

  1. std::vector<T> loại không hoàn chỉnh;

    Trước C ++ 17, hành vi không xác định là xây dựng một std::vector<T>nếu định nghĩa của Tvẫn chưa được biết tại thời điểm khởi tạo. Tuy nhiên, ràng buộc đó đã được nới lỏng trong C ++ 17 .

    Để phân bổ hiệu quả bộ nhớ cho một đối tượng, bạn cần biết kích thước của nó. Từ C ++ 17 trở lên, khách hàng của bạn có thể gặp trường hợp std::vector<T>lớp của bạn không biết kích thước của T. Có hợp lý không khi có các đặc tính phân bổ bộ nhớ phụ thuộc vào độ đầy đủ của kiểu?

  2. Unwanted Memory allocations

    Có rất nhiều, rất nhiều lần bạn sẽ cần lập mô hình biểu đồ trong phần mềm. (Một cây là một đồ thị); Bạn rất có thể sẽ mô hình hóa nó như sau:

    class Node {
        ....
        std::vector<Node> children; //or std::vector< *some pointer type* > children;
        ....
     };
    

    Bây giờ hãy suy nghĩ một chút và tưởng tượng nếu bạn có nhiều nút đầu cuối. Bạn sẽ rất khó chịu nếu việc triển khai STL của bạn phân bổ thêm bộ nhớ đơn giản với dự đoán có các đối tượng trong đó children.

    Đây chỉ là một ví dụ, vui lòng nghĩ thêm ...


2

Tiêu chuẩn không chỉ định giá trị ban đầu cho dung lượng nhưng vùng chứa STL tự động phát triển để chứa nhiều dữ liệu khi bạn đưa vào, miễn là bạn không vượt quá kích thước tối đa (sử dụng hàm thành viên max_size để biết). Đối với vector và chuỗi, sự tăng trưởng được xử lý bởi realloc bất cứ khi nào cần thêm dung lượng. Giả sử bạn muốn tạo giá trị giữ vector 1-1000. Nếu không sử dụng dự trữ, mã thường sẽ dẫn đến từ 2 đến 18 lần phân bổ lại trong vòng lặp sau:

vector<int> v;
for ( int i = 1; i <= 1000; i++) v.push_back(i);

Việc sửa đổi mã để sử dụng dự trữ có thể dẫn đến 0 lần phân bổ trong vòng lặp:

vector<int> v;
v.reserve(1000);

for ( int i = 1; i <= 1000; i++) v.push_back(i);

Nói một cách đại khái, dung lượng vectơ và chuỗi tăng lên theo hệ số từ 1,5 đến 2 mỗi lần.

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.