Capacidade inicial do vetor em C ++

96

Qual é o capacity()de um std::vectorque é criado usando o construtor padrão? Eu sei que o size()é zero. Podemos afirmar que um vetor construído padrão não chama a alocação de memória heap?

Dessa forma seria possível criar um array com reserva arbitrária usando uma única alocação, como std::vector<int> iv; iv.reserve(2345);. Digamos que, por algum motivo, eu não queira iniciar size()no 2345.

Por exemplo, no 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;
}

impresso 0,10. É uma regra ou depende do fornecedor de STL?

Não está na lista
fonte
7
Padrão não especifica nada sobre a capacidade inicial do vetor, mas a maioria das implementações usa 0.
Sr.Anubis
11
Não há garantia, mas eu questionaria seriamente a qualidade de qualquer implementação que alocou memória sem minha solicitação.
Mike Seymour
2
@MikeSeymour Discord. Uma implementação de desempenho realmente alto pode conter um pequeno buffer embutido, caso em que definir a capacidade inicial () para isso faria sentido.
Alastair
6
@alastair Ao usar swaptodos os iteradores e referências permanecem válidos (exceto end()s). Isso significa que um buffer embutido não é possível.
Notinlist de

Respostas:

75

O padrão não especifica qual capacitydeve ser a inicial de um contêiner, então você está contando com a implementação. Uma implementação comum iniciará a capacidade em zero, mas não há garantia. Por outro lado, não há como melhorar sua estratégia de std::vector<int> iv; iv.reserve(2345);ficar com ela.

Mark Ransom
fonte
1
Eu não compro sua última declaração. Se você não pode confiar que a capacidade seja 0 inicialmente, você pode reestruturar seu programa para permitir que seu vetor tenha um tamanho inicial. Isso seria a metade do número de solicitações de memória heap (de 2 para 1).
bitmask
4
@bitmask: Sendo prático: você conhece alguma implementação em que um vetor aloque memória no construtor padrão? Não é garantido pelo padrão, mas como Mike Seymour aponta, acionar uma alocação sem a necessidade seria um mau cheiro em relação à qualidade da implementação .
David Rodríguez - dribeas
3
@ DavidRodríguez-dribeas: Esse não é o ponto. A premissa era "você não pode fazer melhor do que sua estratégia atual, então não se preocupe em imaginar se pode haver implementações estúpidas". Se a premissa fosse "não existem tais implementações, então não se preocupe", eu compraria. A conclusão é verdadeira, mas a implicação não funciona. Desculpe, talvez eu seja picuinhas.
bitmask
3
@bitmask Se houver uma implementação que aloca memória na construção padrão, fazer o que você disse reduziria pela metade o número de alocações. Mas vector::reservenão é o mesmo que especificar um tamanho inicial. Os construtores de vetor que recebem um valor de tamanho / cópia inicial inicializam nobjetos e, portanto, têm complexidade linear. OTOH, chamar reserva significa apenas copiar / mover size()elementos se uma realocação for acionada. Em um vetor vazio, não há nada para copiar. Portanto, o último pode ser desejável mesmo se a implementação alocar memória para um vetor construído padrão.
Pretoriano,
4
@bitmask, se você está preocupado com as alocações até este grau, você deve olhar para a implementação de sua biblioteca padrão particular e não confiar em especulações.
Mark Ransom
38

As implementações de armazenamento de std :: vector variam significativamente, mas todas as que encontrei começam em 0.

O seguinte código:

#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;
}

Dá a seguinte saída:

0
1
2
4
4
8
8
8
8
16
16

sob GCC 5.1 e:

0
1
2
3
4
6
6
9
9
9
13

sob MSVC 2013.

metamorfose
fonte
3
Isso é tão subestimado @Andrew
Valentin Mercier
Bem, você descobre praticamente em todos os lugares que a recomendação para fins de velocidade quase sempre é apenas usar um vetor, então se você estiver fazendo algo que envolva dados esparsos ...
Andrew
@Andrew, onde eles deveriam ter começado? alocar qualquer coisa seria apenas perder tempo alocando e desalocando essa memória se o programador quiser reservar mais do que o padrão. se você está assumindo que eles devem começar com 1, ele o alocará assim que alguém estiver alocando 1 de qualquer maneira.
Poça de
@Puddle Você está lendo nas entrelinhas em vez de interpretar pelo valor de face. A dica de que não é sarcasmo é a palavra "inteligente", assim como meu segundo comentário mencionando dados esparsos.
Andrew
@Andrew: Que bom, você ficou aliviado o suficiente por eles começarem em 0. Por que comentar sobre isso de uma forma jocosa?
Poça de
7

Até onde eu entendi o padrão (embora eu não pudesse nomear uma referência), a instanciação do contêiner e a alocação de memória foram intencionalmente desacopladas por um bom motivo. Portanto, você tem chamadas distintas e separadas para

  • constructor para criar o próprio contêiner
  • reserve() para pré-alocar um bloco de memória adequadamente grande para acomodar pelo menos (!) um determinado número de objetos

E isso faz muito sentido. O único direito de existir reserve()é dar a você a oportunidade de codificar em torno de realocações possivelmente caras ao aumentar o vetor. Para ser útil, você deve saber o número de objetos a armazenar ou, pelo menos, ser capaz de fazer um palpite. Se isso não for fornecido, é melhor você ficar longe, reserve()pois você apenas mudará a realocação para memória desperdiçada.

Então, juntando tudo:

  • O padrão intencionalmente não especifica um construtor que permite a você pré-alocar um bloco de memória para um número específico de objetos (o que seria pelo menos mais desejável do que alocar uma implementação específica, "algo" fixo sob o capô).
  • A alocação não deve ser implícita. Então, para pré-alocar um bloco, você precisa fazer uma chamada separada parareserve() e não precisa estar no mesmo local de construção (poderia / deveria ser mais tarde, depois que você tomar conhecimento do tamanho necessário para acomodar)
  • Portanto, se um vetor sempre pré-alocar um bloco de memória de tamanho definido pela implementação, isso frustraria o trabalho pretendido reserve(), não é?
  • Qual seria a vantagem de pré-alocar um bloco se o STL naturalmente não pode saber a finalidade pretendida e o tamanho esperado de um vetor? Vai ser um tanto sem sentido, se não contraproducente.
  • Em vez disso, a solução adequada é alocar e implementar bloco específico com o primeiro push_back()- se já não estiver explicitamente alocado antes por reserve().
  • No caso de uma realocação necessária, o aumento no tamanho do bloco também é específico da implementação. As implementações de vetor que conheço começam com um aumento exponencial de tamanho, mas limitarão a taxa de incremento a um determinado máximo para evitar o desperdício de grandes quantidades de memória ou até mesmo sua explosão.

Tudo isso só funciona e tem vantagem se não for perturbado por um construtor de alocação. Você tem padrões razoáveis ​​para cenários comuns que podem ser substituídos sob demanda por reserve()(e shrink_to_fit()). Portanto, mesmo se o padrão não declarar isso explicitamente, estou certo de que assumir que um vetor recém-construído não pré-aloca é uma aposta bastante segura para todas as implementações atuais.

Dom pedro
fonte
4

Como uma ligeira adição às outras respostas, descobri que, ao executar em condições de depuração com o Visual Studio, um vetor padrão construído ainda será alocado no heap, embora a capacidade comece em zero.

Especificamente se _ITERATOR_DEBUG_LEVEL! = 0 então o vetor alocará algum espaço para ajudar na verificação do iterador.

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

Achei isso um pouco irritante, já que estava usando um alocador personalizado na época e não esperava a alocação extra.

David Woo
fonte
Interessante, eles quebram as garantias noexcept (pelo menos para C + 17, anterior?): En.cppreference.com/w/cpp/container/vector/vector
Deduplicator
4

Esta é uma pergunta antiga, e todas as respostas aqui explicaram corretamente o ponto de vista do padrão e a maneira como você pode obter uma capacidade inicial de forma portátil usando std::vector::reserve;

No entanto, explicarei por que não faz sentido para nenhuma implementação STL alocar memória durante a construção de um std::vector<T>objeto ;

  1. std::vector<T> de tipos incompletos;

    Antes do C ++ 17, era um comportamento indefinido construir um std::vector<T>se a definição de Tainda for desconhecida no ponto de instanciação. No entanto, essa restrição foi relaxada em C ++ 17 .

    Para alocar memória de maneira eficiente para um objeto, você precisa saber seu tamanho. Do C ++ 17 e além, seus clientes podem ter casos em que sua std::vector<T>classe não sabe o tamanho de T. Faz sentido ter características de alocação de memória dependentes da integridade do tipo?

  2. Unwanted Memory allocations

    Muitas, muitas vezes você precisará modelar um gráfico no software. (Uma árvore é um gráfico); Você provavelmente irá modelá-lo como:

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

    Agora pense por um momento e imagine se você tivesse muitos nós terminais. Você ficaria muito chateado se sua implementação STL alocasse memória extra simplesmente na expectativa de ter objetos children.

    Este é apenas um exemplo, fique à vontade para pensar em mais ...

WhiZTiM
fonte
2

Padrão não especifica o valor inicial para capacidade, mas o contêiner STL cresce automaticamente para acomodar tantos dados quanto você coloca, desde que você não exceda o tamanho máximo (use a função de membro max_size para saber). Para vetor e string, o crescimento é controlado por realocar sempre que mais espaço for necessário. Suponha que você gostaria de criar um vetor contendo o valor de 1-1000. Sem usar reserva, o código normalmente resultará em 2 a 18 realocações durante o seguinte loop:

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

Modificar o código para usar reserva pode resultar em 0 alocações durante o loop:

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

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

A grosso modo, as capacidades do vetor e da string aumentam por um fator entre 1,5 e 2 a cada vez.

Archie Yalakki
fonte