c++ optimization gcc default-value

c++ - ¿Cómo puedo evitar std:: vector<> para inicializar todos sus elementos?



optimization gcc (9)

EDITAR: edité tanto la pregunta como su título para ser más precisos.

Teniendo en cuenta el siguiente código fuente:

#include <vector> struct xyz { xyz() { } // empty constructor, but the compiler doesn''t care xyz(const xyz& o): v(o.v) { } xyz& operator=(const xyz& o) { v=o.v; return *this; } int v; // <will be initialized to int(), which means 0 }; std::vector<xyz> test() { return std::vector<xyz>(1024); // will do a memset() :-( }

... ¿cómo puedo evitar que la memoria asignada por el vector <> se inicialice con copias de su primer elemento, que es una operación O (n) que prefiero omitir por motivos de velocidad, ya que mi constructor predeterminado no hace nada? ?

Una solución específica de g ++ funcionará, si no existe una genérica (pero no pude encontrar ningún atributo para hacerlo).

EDITAR : el código generado sigue (línea de comando: arm-elf-g ++ - 4.5 -O3 -S -fno-verbose-asm -o - test.cpp | arm-elf-c ++ filt | grep -vE ''^ [[: espacio:]] + [. @]. * $ '')

test(): mov r3, #0 stmfd sp!, {r4, lr} mov r4, r0 str r3, [r0, #0] str r3, [r0, #4] str r3, [r0, #8] mov r0, #4096 bl operator new(unsigned long) add r1, r0, #4096 add r2, r0, #4080 str r0, [r4, #0] stmib r4, {r0, r1} add r2, r2, #12 b .L4 @ .L8: @ add r0, r0, #4 @ .L4: @ cmp r0, #0 @ fill the memory movne r3, #0 @ strne r3, [r0, #0] @ cmp r0, r2 @ bne .L8 @ str r1, [r4, #4] mov r0, r4 ldmfd sp!, {r4, pc}

EDITAR: En aras de la integridad, aquí está el conjunto para x86_64:

.globl test() test(): LFB450: pushq %rbp LCFI0: movq %rsp, %rbp LCFI1: pushq %rbx LCFI2: movq %rdi, %rbx subq $8, %rsp LCFI3: movq $0, (%rdi) movq $0, 8(%rdi) movq $0, 16(%rdi) movl $4096, %edi call operator new(unsigned long) leaq 4096(%rax), %rcx movq %rax, (%rbx) movq %rax, 8(%rbx) leaq 4092(%rax), %rdx movq %rcx, 16(%rbx) jmp L4 @ L8: @ addq $4, %rax @ L4: @ testq %rax, %rax @ memory-filling loop je L2 @ movl $0, (%rax) @ L2: @ cmpq %rdx, %rax @ jne L8 @ movq %rcx, 8(%rbx) movq %rbx, %rax addq $8, %rsp popq %rbx leave LCFI4: ret LFE450: EH_frame1: LSCIE1: LECIE1: LSFDE1: LASFDE1: LEFDE1:

EDIT: Creo que la conclusión es no usar std::vector<> cuando se quiere evitar una inicialización innecesaria. Terminé de desenrollar mi propio contenedor con plantilla, que funciona mejor (y tiene versiones especializadas para neon y armv7).


Con la forma en que se declara su struct en este momento, no hay ningún mecanismo para inicializar por defecto el miembro int de su estructura, por lo tanto, obtiene el comportamiento predeterminado de C, que es una inicialización indeterminada. Para inicializar la variable miembro int con un valor de inicialización predeterminado, tendría que agregarla a la lista de inicialización del constructor de la estructura. Por ejemplo,

struct xyz { xyz(): v() { } //initialization list sets the value of int v to 0 int v; };

Mientras

struct xyz { xyz(): { } //no initialization list, therefore ''v'' remains uninitialized int v; };


Envuelves todas tus primitivas en una estructura:

struct IntStruct { IntStruct(); int myInt; }

con IntStruct () definido como un constructor vacío. Así declaras v como IntStruct v; por lo tanto, cuando un vector de xyzs todo el valor de inicialización, todo lo que hacen es el valor de inicialización v que es un no-op.

EDITAR: He leído mal la pregunta. Esto es lo que debe hacer si tiene un vector de tipos primitivos, porque vector se define para inicializar con valor al crear elementos a través del método resize() . Las estructuras no están obligadas a inicializar con valor sus miembros en la construcción, aunque estos valores "no inicializados" todavía pueden establecerse en 0 por otra cosa: bueno, podrían ser cualquier cosa.


Este es un rincón extraño del vector . El problema no es que su elemento se esté inicializando con valor ... es que el contenido aleatorio en el primer elemento prototípico se copia a todos los demás elementos del vector. (Este comportamiento cambió con C ++ 11, cuyo valor inicializa cada elemento).

Esto se hace (/ se hizo) por una buena razón: considere algún objeto de referencia contado ... si construye un vector solicitando 1000 elementos inicializados para tal objeto, obviamente desea un objeto con un recuento de referencia de 1000, en lugar de tener 1000 "clones" independientes. Digo "obviamente" porque haber hecho la referencia del objeto contada en primer lugar implica que eso es altamente deseable.

De todos modos, ya casi no tienes suerte. Efectivamente, el vector se asegura de que todos los elementos sean iguales, incluso si el contenido que se está sincronizando es basura no inicializada.

En la tierra de los hackers felices específicos de g ++ no estándar, podemos explotar cualquier función miembro pública con plantilla en la interfaz vector como una puerta trasera para cambiar los datos de miembros privados simplemente especializando la plantilla para un nuevo tipo.

ADVERTENCIA : no solo para esta "solución", sino para todo este esfuerzo para evitar la construcción predeterminada ... no haga esto para los tipos con invariantes importantes : rompe la encapsulación y puede tener un vector propio o alguna operación que intente invocar operator=() , constructores de copia y / o destructores donde *this / lado izquierdo y / o derecho no respetan esos invariantes. Por ejemplo, evite los tipos de valor con punteros que espera que sean NULL o para objetos válidos, contadores de referencia, manejadores de recursos, etc.

#include <iostream> #include <vector> struct Uninitialised_Resize { explicit Uninitialised_Resize(int n) : n_(n) { } explicit Uninitialised_Resize() { } int n_; }; namespace std { template <> template <> void vector<int>::assign(Uninitialised_Resize ur, Uninitialised_Resize) { this->_M_impl._M_finish = this->_M_impl._M_start + ur.n_; // note: a simpler alternative (doesn''t need "n_") is to set... // this->_M_impl._M_finish = this->_M_impl._M_end_of_storage; // ...which means size() will become capacity(), which may be more // you reserved() (due to rounding; good) or have data for // (bad if you have to track in-use elements elsewhere, // which makes the situation equivalent to just reserve()), // but if you can somehow use the extra elements then all''s good. } } int main() { { // try to get some non-0 values on heap ready for recycling... std::vector<int> x(10000); for (int i = 0; i < x.size(); ++i) x[i] = i; } std::vector<int> x; x.reserve(10000); for (int i = 1; i < x.capacity(); ++i) if (x[0] != x[i]) { std::cout << "lucky/n"; break; } x.assign(Uninitialised_Resize(1000), Uninitialised_Resize()); for (int i = 1; i < x.size(); ++i) if (x[0] != x[i]) { std::cout << "success [0] " << x[0] << " != [" << i << "] " << x[i] << ''/n''; break; } }

Mi salida:

lucky success [0] 0 != [1] 1

Esto sugiere que el nuevo vector se reasignó al montón que el primer vector lanzó cuando se salió del alcance, y muestra que los valores no están obstruidos por la asignación. Por supuesto, no hay forma de saber si otras invariantes de clase importantes se han invalidado sin inspeccionar las fuentes vector mucho cuidado, y los nombres / importes exactos de los miembros privados pueden variar en cualquier momento ...


La inicialización de los elementos asignados se controla mediante el argumento de la plantilla Asignador. Si necesita personalizarlo, personalícelo. Pero recuerde que esto puede ser fácilmente enrollado en el ámbito de la piratería sucia, así que use con precaución. Por ejemplo, aquí hay una solución bastante sucia. Evitará la inicialización, pero lo más probable es que sea peor en el rendimiento, pero en aras de la demostración (¡como la gente ha dicho que esto es imposible! ... ¡imposible no está en el vocabulario de un programador de C ++!):

template <typename T> class switch_init_allocator : public std::allocator< T > { private: bool* should_init; public: template <typename U> struct rebind { typedef switch_init_allocator<U> other; }; //provide the required no-throw constructors / destructors: switch_init_allocator(bool* aShouldInit = NULL) throw() : std::allocator<T>(), should_init(aShouldInit) { }; switch_init_allocator(const switch_init_allocator<T>& rhs) throw() : std::allocator<T>(rhs), should_init(rhs.should_init) { }; template <typename U> switch_init_allocator(const switch_init_allocator<U>& rhs, bool* aShouldInit = NULL) throw() : std::allocator<T>(rhs), should_init(aShouldInit) { }; ~switch_init_allocator() throw() { }; //import the required typedefs: typedef typename std::allocator<T>::value_type value_type; typedef typename std::allocator<T>::pointer pointer; typedef typename std::allocator<T>::reference reference; typedef typename std::allocator<T>::const_pointer const_pointer; typedef typename std::allocator<T>::const_reference const_reference; typedef typename std::allocator<T>::size_type size_type; typedef typename std::allocator<T>::difference_type difference_type; //redefine the construct function (hiding the base-class version): void construct( pointer p, const_reference cr) { if((should_init) && (*should_init)) new ((void*)p) T ( cr ); //else, do nothing. }; }; template <typename T> class my_vector : public std::vector<T, switch_init_allocator<T> > { public: typedef std::vector<T, switch_init_allocator<T> > base_type; typedef switch_init_allocator<T> allocator_type; typedef std::vector<T, allocator_type > vector_type; typedef typename base_type::size_type size_type; private: bool switch_flag; //the order here is very important!! vector_type vec; public: my_vector(size_type aCount) : switch_flag(false), vec(aCount, allocator_type(&switch_flag)) { }; //... and the rest of this wrapper class... vector_type& get_vector() { return vec; }; const vector_type& get_vector() const { return vec; }; void set_switch(bool value) { switch_flag = value; }; }; class xyz{}; int main(){ my_vector<xyz> v(1024); //this won''t initialize the memory at all. v.set_switch(true); //set back to true to turn initialization back on (needed for resizing and such) }

Por supuesto, lo anterior es incómodo y no se recomienda, y ciertamente no será mejor que dejar que la memoria se llene con copias del primer elemento (especialmente porque el uso de este control de bandera impedirá cada construcción de elemento) . Pero es una avenida para explorar cuando se busca optimizar la asignación e inicialización de elementos en un contenedor de STL, así que quería mostrarlo. El punto es que el único lugar donde puede inyectar código que impida que el contenedor std :: vector llame al constructor de copia para inicializar sus elementos es en la función de construcción del objeto asignador del vector.

Además, puede eliminar el "interruptor" y simplemente hacer un "no-init-allocator", pero luego, también desactiva la construcción de copia, que es necesaria para copiar los datos durante el cambio de tamaño (lo que haría que esta clase de vector fuera mucho más Menos útil).


No estoy viendo la memoria inicializada. El constructor predeterminado int() no hace nada, al igual que en C.

Programa:

#include <iostream> #include <vector> struct xyz { xyz() {} xyz(const xyz& o): v(o.v) {} xyz& operator=(const xyz& o) { v=o.v; return *this; } int v; }; std::vector<xyz> test() { return std::vector<xyz>(1024); } int main() { std::vector<xyz> foo = test(); for(int i = 0; i < 10; ++i) { std::cout << i << ": " << foo[i].v << std::endl; } return 0; }

Salida:

$ g++ -o foo foo.cc $ ./foo 0: 1606418432 1: 1606418432 2: 1606418432 3: 1606418432 4: 1606418432 5: 1606418432 6: 1606418432 7: 1606418432 8: 1606418432 9: 1606418432

EDITAR:

Si solo está tratando de inicializar el vector a algo no trivial, y no quiere perder tiempo en la creación de sus contenidos, puede intentar crear un iterador personalizado y pasarlo al constructor del vector.

Ejemplo modificado:

#include <iostream> #include <vector> #include <iterator> struct xyz { xyz() {} xyz(int init): v(init) {} xyz(const xyz& o): v(o.v) {} xyz& operator=(const xyz& o) { v=o.v; return *this; } int v; }; class XYZInitIterator: public std::iterator<std::input_iterator_tag, xyz> { public: XYZInitIterator(int init): count(init) {} XYZInitIterator(const XYZInitIterator& iter) : count(iter.count) {} XYZInitIterator& operator=(const XYZInitIterator& iter) { count = iter.count; return *this; } value_type operator*() const { return xyz(count); } bool operator==(const XYZInitIterator& other) const { return count == other.count; } bool operator!=(const XYZInitIterator& other) const { return count != other.count; } value_type operator++() { return xyz(++count); } value_type operator++(int) { return xyz(count++); } private: int count; }; std::vector<xyz> test() { XYZInitIterator start(0), end(1024); return std::vector<xyz>(start, end); } int main() { std::vector<xyz> foo = test(); for(int i = 0; i < 10; ++i) { std::cout << std::dec << i << ": " << std::hex << foo[i].v << std::endl; } return 0; }

Salida:

$ g++ -o foo foo.cc $ ./foo 0: 0 1: 1 2: 2 3: 3 4: 4 5: 5 6: 6 7: 7 8: 8 9: 9


No se puede evitar la inicialización de los elementos de std :: vector.

Yo uso una clase derivada std :: vector por esa razón. resize() se implementa en este ejemplo. Usted debe implementar constructores también.

Aunque esto no es C ++ estándar, pero la implementación del compilador :-(

#include <vector> template<typename _Tp, typename _Alloc = std::allocator<_Tp>> class uvector : public std::vector<_Tp, _Alloc> { typedef std::vector<_Tp, _Alloc> parent; using parent::_M_impl; public: using parent::capacity; using parent::reserve; using parent::size; using typename parent::size_type; void resize(size_type sz) { if (sz <= size()) parent::resize(sz); else { if (sz > capacity()) reserve(sz); _M_impl._M_finish = _M_impl._M_start + sz; } } };


Para referencia, el siguiente código lleva a un ensamblaje óptimo en g ++: No estoy diciendo que lo vaya a usar nunca y no lo aliento a que lo haga. No es correcto C ++! Es un truco muy, muy sucio! Supongo que incluso podría depender de la versión g ++, así que, realmente, no la use. Yo vomitaría si lo viera usado en alguna parte.

#include <vector> template<typename T> static T create_uninitialized(size_t size, size_t capacity) { T v; #if defined(__GNUC__) // Don''t say it. I know -_-; // Oddly, _M_impl is public in _Vector_base !? typedef typename T::value_type value_type; typedef typename T::allocator_type allocator_type; typedef std::_Vector_base<value_type, allocator_type> base_type; base_type& xb(reinterpret_cast<base_type&>(v)); value_type* p(new value_type[capacity]); #if !defined(__EXCEPTIONS) size=p?size:0; // size=0 if p is null capacity=p?capacity:0; // capacity=0 if p is null #endif capacity=std::max(size, capacity); // ensure size<=capacity xb._M_impl._M_start = p; xb._M_impl._M_finish = p+size; xb._M_impl._M_end_of_storage = p+capacity; #else // Fallback, for the other compilers capacity=std::max(size, capacity); v.reserve(capacity); v.resize(size); #endif return v; } struct xyz { // empty default constructor xyz() { } xyz(const xyz& o): v(o.v) { } xyz& operator=(const xyz& o) { v=o.v; return *this; } int v; typedef std::vector<xyz> vector; }; // test functions for assembly dump extern xyz::vector xyz_create() { // Create an uninitialized vector of 12 elements, with // a capacity to hold 256 elements. return create_uninitialized<xyz::vector>(12,256); } extern void xyz_fill(xyz::vector& x) { // Assign some values for testing for (int i(0); i<x.size(); ++i) x[i].v = i; } // test #include <iostream> int main() { xyz::vector x(xyz_create()); xyz_fill(x); // Dump the vector for (int i(0); i<x.size(); ++i) std::cerr << x[i].v << "/n"; return 0; }

EDITAR: se realizó _Vector_impl público, lo que simplifica las cosas.

EDITAR: aquí se encuentra el ensamblado ARM generado para xyz_create (), compilado con -fno-exceptions (desmangle usando c ++ filt) y sin ningún bucle de inicialización de memoria:

xyz_create(): mov r3, #0 stmfd sp!, {r4, lr} mov r4, r0 str r3, [r0, #0] str r3, [r0, #4] str r3, [r0, #8] mov r0, #1024 bl operator new[](unsigned long)(PLT) cmp r0, #0 moveq r3, r0 movne r3, #1024 moveq r2, r0 movne r2, #48 add r2, r0, r2 add r3, r0, r3 stmia r4, {r0, r2, r3} @ phole stm mov r0, r4 ldmfd sp!, {r4, pc}

..y aquí para x86_64:

xyz_create(): pushq %rbp movq %rsp, %rbp pushq %rbx movq %rdi, %rbx subq $8, %rsp movq $0, (%rdi) movq $0, 8(%rdi) movq $0, 16(%rdi) movl $1024, %edi call operator new[](unsigned long) cmpq $1, %rax movq %rax, (%rbx) sbbq %rdx, %rdx notq %rdx andl $1024, %edx cmpq $1, %rax sbbq %rcx, %rcx leaq (%rax,%rdx), %rdx notq %rcx andl $48, %ecx movq %rdx, 16(%rbx) leaq (%rax,%rcx), %rcx movq %rbx, %rax movq %rcx, 8(%rbx) addq $8, %rsp popq %rbx leave ret


Si desea un vector con solo memoria reservada, pero sin elementos inicializados, use reserve lugar del constructor:

std::vector<xyz> v; v.reserve(1024); assert(v.capacity() >= 1024); assert(v.size() == 0);


También soy curioso. ¿Solo quieres que la memoria se inicialice al azar?

Los elementos vectoriales se almacenan en ubicaciones de memoria consecutivas, por lo que la inicialización aleatoria es una posibilidad.