Capacidad inicial de vector en C ++

92

¿Cuál es el capacity()de un std::vectorque se crea usando el constructor predeterminado? Sé que size()es cero. ¿Podemos afirmar que un vector construido por defecto no llama asignación de memoria de pila?

De esta manera sería posible crear una matriz con una reserva arbitraria utilizando una sola asignación, como std::vector<int> iv; iv.reserve(2345);. Digamos que, por alguna razón, no quiero iniciar el size()2345.

Por ejemplo, en 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;
}

impreso 0,10. ¿Es una regla o depende del proveedor de STL?

No en lista
fuente
7
El estándar no especifica nada sobre la capacidad inicial del vector, pero la mayoría de las implementaciones usan 0.
Señor Anubis
11
No hay garantía, pero cuestionaría seriamente la calidad de cualquier implementación que asigne memoria sin que yo la solicite.
Mike Seymour
2
@MikeSeymour En desacuerdo. Una implementación de muy alto rendimiento podría contener un pequeño búfer en línea, en cuyo caso establecer la capacidad inicial () a eso tendría sentido.
alastair
6
@alastair Cuando se utilizan swaptodos los iteradores y las referencias siguen siendo válidas (excepto end()s). Eso significa que no es posible un búfer en línea.
Notinlist

Respuestas:

74

El estándar no especifica cuál capacitydebe ser la inicial de un contenedor, por lo que confía en la implementación. Una implementación común iniciará la capacidad en cero, pero no hay garantía. Por otro lado, no hay forma de mejorar tu estrategia, std::vector<int> iv; iv.reserve(2345);así que apégate a ella.

Mark Ransom
fuente
1
No compro tu última declaración. Si no puede confiar en que la capacidad sea 0 inicialmente, puede reestructurar su programa para permitir que su vector tenga un tamaño inicial. Esto equivaldría a la mitad del número de solicitudes de memoria dinámica (de 2 a 1).
máscara de bits
4
@bitmask: Siendo práctico: ¿conoce alguna implementación en la que un vector asigne memoria en el constructor predeterminado? No está garantizado por el estándar, pero como señala Mike Seymour, activar una asignación sin la necesidad sería un mal olor con respecto a la calidad de la implementación .
David Rodríguez - dribeas
3
@ DavidRodríguez-dribeas: Ese no es el punto. La premisa era "no se puede hacer mejor que su estrategia actual, por lo que no se molestan preguntando si hay fuerzas haber implementaciones estúpidas". Si la premisa fuera "no existen tales implementaciones, así que no te molestes", la compraría. La conclusión resulta ser cierta, pero la implicación no funciona. Lo siento, tal vez estoy recogiendo liendres.
máscara de bits
3
@bitmask Si existe una implementación que asigna memoria en la construcción predeterminada, hacer lo que dijo reduciría a la mitad el número de asignaciones. Pero vector::reserveno es lo mismo que especificar un tamaño inicial. Los constructores de vectores que toman un valor de tamaño inicial / copia inicializan nobjetos y, por lo tanto, tienen complejidad lineal. OTOH, llamar a la reserva solo significa copiar / mover size()elementos si se activa una reasignación. En un vector vacío no hay nada que copiar. Por lo tanto, esto último podría ser deseable incluso si la implementación asigna memoria para un vector construido predeterminado.
Pretoriano
4
@bitmask, si le preocupan las asignaciones en este grado, debe mirar la implementación de su biblioteca estándar en particular y no confiar en la especulación.
Mark Ransom
36

Las implementaciones de almacenamiento de std :: vector varían significativamente, pero todas las que he encontrado comienzan desde 0.

El siguiente 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;
}

Da el siguiente resultado:

0
1
2
4
4
8
8
8
8
16
16

bajo GCC 5.1 y:

0
1
2
3
4
6
6
9
9
9
13

bajo MSVC 2013.

metamorfosis
fuente
3
Esto está tan subestimado @Andrew
Valentin Mercier
Bueno, encontrará prácticamente en todas partes que la recomendación para fines de velocidad es casi siempre usar un vector, por lo que si está haciendo algo que involucre datos escasos ...
Andrew
@Andrew, ¿en qué deberían haber comenzado? asignar cualquier cosa sería perder tiempo asignando y desasignando esa memoria si el programador quiere reservar más de la predeterminada. si asume que deben comenzar con 1, lo asignará tan pronto como alguien asigne 1 de todos modos.
Charco
@Puddle Estás leyendo entre líneas en lugar de tomarlo al pie de la letra. La pista de que no es sarcasmo es la palabra "inteligente", así como mi segundo comentario que menciona datos escasos.
Andrew
@Andrew Oh, bien, te sentiste bastante aliviado de que comenzaran en 0. ¿Por qué siquiera comentarlo en broma?
Charco
7

Por lo que entendí el estándar (aunque en realidad no podría nombrar una referencia), la instanciación del contenedor y la asignación de memoria se han desacoplado intencionalmente por una buena razón. Por lo tanto, tiene llamadas distintas e independientes para

  • constructor para crear el propio contenedor
  • reserve() para preasignar un bloque de memoria suficientemente grande para acomodar al menos (!) un número determinado de objetos

Y esto tiene mucho sentido. El único derecho por el que existe reserve()es para darle la oportunidad de codificar en torno a reasignaciones posiblemente costosas al hacer crecer el vector. Para que sea útil, debe saber la cantidad de objetos que debe almacenar o, al menos, debe poder hacer una conjetura. Si no se le da esto, es mejor que se mantenga alejado, reserve()ya que solo cambiará la reasignación por memoria desperdiciada.

Así que poniendo todo junto:

  • El estándar intencionalmente no especifica un constructor que le permita preasignar un bloque de memoria para un número específico de objetos (lo cual sería al menos más deseable que asignar un "algo" fijo y específico de implementación bajo el capó).
  • La asignación no debe ser implícita. Entonces, para preasignar un bloque, necesita hacer una llamada separada areserve() y no es necesario que esté en el mismo lugar de construcción (por supuesto, podría / debería ser más tarde, después de que se dio cuenta del tamaño requerido para acomodar)
  • Por lo tanto, si un vector siempre preasignaría un bloque de memoria de tamaño definido por la implementación, esto frustraría el trabajo previsto reserve(), ¿no es así?
  • ¿Cuál sería la ventaja de preasignar un bloque si el STL naturalmente no puede conocer el propósito previsto y el tamaño esperado de un vector? Será bastante absurdo, si no contraproducente.
  • En cambio, la solución adecuada es asignar un bloque específico de implementación con el primero push_back(), si no lo ha asignado explícitamentereserve() .
  • En caso de una reasignación necesaria, el aumento en el tamaño del bloque también es específico de la implementación. Las implementaciones vectoriales que conozco comienzan con un aumento exponencial de tamaño, pero limitarán la tasa de incremento a un cierto máximo para evitar desperdiciar grandes cantidades de memoria o incluso arruinarla.

Todo esto llega a un funcionamiento completo y una ventaja solo si no lo perturba un constructor de asignación. Tiene valores predeterminados razonables para escenarios comunes que pueden ser anulados a pedido por reserve()(y shrink_to_fit()). Entonces, incluso si el estándar no lo dice explícitamente, estoy bastante seguro de que asumir que un vector recién construido no preasigna es una apuesta bastante segura para todas las implementaciones actuales.

Don pedro
fuente
4

Como una pequeña adición a las otras respuestas, descubrí que cuando se ejecuta en condiciones de depuración con Visual Studio, un vector construido predeterminado aún se asignará en el montón aunque la capacidad comience en cero.

Específicamente si _ITERATOR_DEBUG_LEVEL! = 0, el vector asignará algo de espacio para ayudar con la verificación del iterador.

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

Encontré esto un poco molesto ya que estaba usando un asignador personalizado en ese momento y no esperaba la asignación adicional.

David Woo
fuente
Interesante, se rompen las garantías noexcept-(al menos para C + 17, antes?): En.cppreference.com/w/cpp/container/vector/vector
Deduplicator
4

Esta es una pregunta antigua, y todas las respuestas aquí han explicado correctamente el punto de vista del estándar y la forma en que puede obtener una capacidad inicial de manera portátil usando std::vector::reserve;

Sin embargo, explicaré por qué no tiene sentido que ninguna implementación de STL asigne memoria al construir un std::vector<T>objeto ;

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

    Antes de C ++ 17, era un comportamiento indefinido construir un std::vector<T>si la definición de Taún se desconoce en el punto de instanciación. Sin embargo, esa restricción se relajó en C ++ 17 .

    Para asignar memoria de manera eficiente a un objeto, necesita conocer su tamaño. Desde C ++ 17 y posteriores, sus clientes pueden tener casos en los que sustd::vector<T> clase no conoce el tamaño de T. ¿Tiene sentido que las características de asignación de memoria dependan de la completitud del tipo?

  2. Unwanted Memory allocations

    Hay muchas, muchas, muchas veces que necesitará modelar un gráfico en software. (Un árbol es un gráfico); Lo más probable es que lo modele como:

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

    Ahora piense por un momento e imagine si tuviera muchos nodos terminales. Estaría muy enojado si su implementación de STL asigna memoria adicional simplemente en anticipación de tener objetos children.

    Este es solo un ejemplo, no dude en pensar en más ...

WhiZTiM
fuente
2

El estándar no especifica el valor inicial para la capacidad, pero el contenedor STL crece automáticamente para acomodar todos los datos que ingresa, siempre que no exceda el tamaño máximo (use la función miembro max_size para saberlo). Para el vector y la cadena, el crecimiento se gestiona mediante reasignación cada vez que se necesita más espacio. Suponga que desea crear un valor de retención de vector de 1 a 1000. Sin usar la reserva, el código normalmente dará como resultado entre 2 y 18 reasignaciones durante el siguiente ciclo:

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

Modificar el código para usar la reserva puede resultar en 0 asignaciones durante el ciclo:

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

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

En términos generales, las capacidades de los vectores y las cadenas crecen en un factor de entre 1,5 y 2 cada vez.

Archie Yalakki
fuente