Neil Kirk
Neil Kirk

Reputation: 21773

Initializing a vector with initial value efficiently

Given this type for testing:

struct Counter
{
    static int count;

    Counter(int)
    {
        count++;
    }

    Counter(const Counter&)
    {
        count++;
    }

    Counter(Counter&&) noexcept
    {

    }
};

int Counter::count = 0;

Suppose we have the following:

std::vector<Counter> vec(5, 0);

According to VS2015, 6 Counter objects are created. I know that there are only 5 permanent objects. Why doesn't the compiler emplace the objects from the constructor parameters, or move the temporary object into the first position and then copy the rest from it?

Even if the initial size of the vector is set to 0, 1 object is still created.

std::vector<Counter> vec(0, 0);

This could be important if the size isn't known until run-time and often it is 0 (a no-op) and the type in the container is expensive to copy or construct.

It's often convenient to initialize vectors in one statement, especially if they are class members in the initialization list or constants. How can I do that as efficiently as the following code:

std::vector<Counter> vec;
vec.reserve(size);
for (size_t i = 0; i < size; i++)
{
    vec.emplace_back(0);
}

Which constructs only as many contained objects as there are stored in the vector.

Upvotes: 2

Views: 1900

Answers (2)

g24l
g24l

Reputation: 3135

I guess it is efficient, based on your definition.

Testing the different versions;

Copies: 100000005 , Construct: 1, Equal copies 0
real    0m0.075s
user    0m0.073s
sys 0m0.002s

and with emplace_back:

Copies: 0 , Construct: 100000005, Equal copies 0
real    0m0.195s
user    0m0.191s
sys 0m0.004s

you may mean it is space efficient. However, that is a choice based on use case, and seems compiler designers prefer speed.

Here is the code ( I track also equal )

struct Counter
{
static int count_ctor;
static int count_copy;
static int count_equal;

Counter(int){count_ctor++;}    
Counter(const Counter&){count_copy++;}    
Counter(Counter&&) noexcept{}
Counter & operator=(Counter const &){ count_equal++ ;}
};

int Counter::count_copy = 0;
int Counter::count_ctor = 0;
int Counter::count_equal = 0;

int main(void)
{
  int size(100000005);

#ifdef EMPLACE
  std::vector<Counter> v;
  v.reserve(size);
  for(int i = size; i>0 ; --i){ v.emplace_back(0);}
#else
  std::vector<Counter> v(size,0);
#endif    
  std::printf("Copies: %d , Construct: %d, Equal copies %d",Counter::count_copy, Counter::count_ctor, Counter::count_equal);
  return 0;
}

compile with g++ -DEMPLACE --std=c++11 -O3 or without EMPLACE to get the desired binary.

SECOND TEST

In order to rebute the assumptions made by the OP , the following test has been made:

  1. Creation of many small vectors created within multiple larger classes
  2. All objects created either with the default construct-copy policy or by calling an emplace wrapper function.

We produced two binaries with

g++ -DEMPLACE --std=c++11 -O3 copyc.cpp -o copyc && g++ --std=c++11 -O3 copyc.cpp -o copyc_copy

and in order to avoid any of the two having preferential treatement from the OS , we set a standard pause of 10s in between them, and we launch with the system at idle.

An exemplary run is below.

export K=10192 ; time ./copyc_copy $K ; sleep 10; time ./copyc $K
Copies: 10192 , Construct: 1, Equal copies 0
real    0m2.888s
user    0m0.666s
sys 0m2.219s
Copies: 0 , Construct: 10192, Equal copies 0
real    0m3.376s
user    0m1.105s
sys 0m2.270s

I've run this in multiple cases , and also in reverse

Copies: 0 , Construct: 10192, Equal copies 0
real    0m3.154s
user    0m0.886s
sys 0m2.267s
Copies: 10192 , Construct: 1, Equal copies 0
real    0m2.573s
user    0m0.531s
sys 0m2.025s

That being said this is an incoclusive test, but having spent this match time on this, I bet the compiler designers did more , and all from gnu to clang and VS decided to implement a construct-copy policy. I am certain they had other reasons as well.

The code for the second test is below:

#include <vector>
#include <iostream>
#include <cstdlib>

template<typename T> static std::vector<T> get5()
{
std::vector<T> s;
s.reserve(5);
for(int i=5; i!=0 ;--i)
{
s.emplace_back(T());
}
return s;
}

struct test_struct
{
    volatile int internals[255];
};

struct test_create
{
std::vector<test_struct> s;
test_create() : s(5){}
};

struct test_emplace
{
std::vector<test_struct> s;
test_emplace() : s(get5<test_struct>()){}
};


struct Counter
{
static int count_ctor;
static int count_copy;
static int count_equal;

#ifdef EMPLACE
test_emplace t[100];
#else
test_create t[100];
#endif

Counter(int)
{
count_ctor++;
}

Counter(const Counter&)
{
count_copy++;
}

Counter(Counter&&) noexcept
{

}
Counter & operator=(Counter const &){ count_equal++ ;}
};

int Counter::count_copy = 0;
int Counter::count_ctor = 0;
int Counter::count_equal = 0;

int main(int arg, char const * argv[])
{
int size(std::atoi(argv[1]));

#ifdef EMPLACE
std::vector<Counter> v;
v.reserve(size);
for(int i = size; i>0 ; --i)
{
    v.emplace_back(0);
}
#else
std::vector<Counter> v(size,0);
#endif

std::printf("Copies: %d , Construct: %d, Equal copies %d",Counter::count_copy, Counter::count_ctor, Counter::count_equal);

return 0;

}

Upvotes: 1

Cheers and hth. - Alf
Cheers and hth. - Alf

Reputation: 145279

You can simply define a function that creates a vector the way that you want.

As a function the initialization code is exception safe.

#include <iostream>
#include <vector>
#include <stddef.h>     // ptrdiff_t
#include <utility>      // std::forward
using namespace std;

struct Counter
{
    static int n_constructor_calls;

    Counter( int )
    {
        ++n_constructor_calls;
    }

    Counter( Counter const& )
    {
        ++n_constructor_calls;
    }

    Counter( Counter&& ) noexcept
    {
        ++n_constructor_calls;
    }
};

int Counter::n_constructor_calls = 0;

//--------------------------------------

using Size = ptrdiff_t;
using Index = Size;

template< class Item, class... Args >
auto make_vector( Size const n, Args&&... args )
    -> vector<Item>
{
    vector<Item>    result;
    result.reserve( n );
    for( Index i = 0; i < n; ++i )
    {
        result.emplace_back( forward<Args>( args )... );
    }
    return result;
}

auto main() -> int
{
    auto vec = make_vector<Counter>( 5, 42 );
    cout << Counter::n_constructor_calls << " constructor calls.\n";
}

(This outputs “5 constructor calls.”)

You essentially ask, why isn't the vector constructor defined to do this,

Why doesn't the compiler emplace the objects from the constructor parameters, or move the temporary object into the first position and then copy the rest from it?

One reason is that this constructor was defined before move semantics was introduced in C++11.

Introducing additional constructors (which changes overload behavior), or changing the behavior of an existing constructor, is expensive with respect to the existing C++ code base, which is very large.

Upvotes: 1

Related Questions