3

I want to improve the performance of the following code. What aspect might affect the performance of the code when it's executed?

Also, considering that there is no limit to how many objects you can add to the container, what improvements could be made to “Object” or “addToContainer” to improve the performance of the program?

I was wondering if std::push_back in C++ affects performance of the code in any way? Especially if there is no limit to adding to list.

struct Object{
    string name;
    string description;
};

vector<Object> container;
void addToContainer(Object object) {
    container.push_back(object);
}

int main() {
    addToContainer({ "Fira", "+5 ATTACK" });
    addToContainer({ "Potion", "+10 HP" });
}
John Kugelman
  • 349,597
  • 67
  • 533
  • 578
AaySquare
  • 123
  • 10
  • What is `std::push_back`? Most likely, it will be slower than not doing `std::push_back`. – eerorika May 30 '19 at 00:51
  • That's a depends. Normally `push_back` is pretty cheap, but every now and then it has to resize to fit more elements, and that can be costly. In this case you can gain a lot by passing references or taking advantage of move semantics so that you aren't copying `Object`s around when you could be moving them. That said compiler am smart and might already be doing some of this for you. – user4581301 May 30 '19 at 00:52
  • this question needs clarification. optimization is a tradeoff. it would seem you ate looking to optimize adds, but at what cost to reads, deletes, etc. – Glenn Teitelbaum May 30 '19 at 00:58
  • 1
    Wouldn't std::vector::emplace_back be faster in this case? – Alyssa Haroldsen May 30 '19 at 01:03
  • possible duplicate for https://stackoverflow.com/questions/4303513/push-back-vs-emplace-back. In specific to your answer, you are paying the cost for moving the object after creation there – Abdurrahim May 30 '19 at 01:05
  • `push_back` a `shared_ptr` to an object instead of the object itself. use `reserve` to set the initial size of the vector to avoid copy and allocation of vector when the vector is full. – Oblivion May 30 '19 at 01:06
  • 2
    `shared_ptr` can be surprisingly expensive, there is a lot of hidden machinery needed to support the sharing, plus using a pointer adds an additional level of indirection that the cache is going to hate when iterating the `vector`. In addition, if you aren't sharing the ownership of the object being pointed at, don't use `shared_ptr`. It leads to people making bad inferences about the behaviour of the code. – user4581301 May 30 '19 at 01:32
  • possibly `unique_ptr` as opposed to `shared_ptr` might make sense if you just use `char *` instead of `string` – Glenn Teitelbaum May 30 '19 at 05:43
  • @user4581301 If you aren't sharing, and know you aren't, you might also want a release operation; shared smart ptr inherently can't have a (guaranteed) release operation. – curiousguy May 30 '19 at 08:10

3 Answers3

4

Before you do ANYTHING profile the code and get a benchmark. After you make a change profile the code and get a benchmark. Compare the benchmarks. If you do not do this, you're rolling dice. Is it faster? Who knows.

Profile profile profile.

With push_back you have two main concerns:

  1. Resizing the vector when it fills up, and
  2. Copying the object into the vector.

There are a number of improvements you can make to the resizing cost cost of push_back depending on how items are being added.

Strategic use of reserve to minimize the amount of resizing, for example. If you know how many items are about to be added, you can check the capacity and size to see if it's worth your time to reserve to avoid multiple resizes. Note this requires knowledge of vector's expansion strategy and that is implementation-specific. An optimization for one vector implementation could be a terribly bad mistake on another.

You can use insert to add multiple items at a time. Of course this is close to useless if you need to add another container into the code in order to bulk-insert.

If you have no idea how many items are incoming, you might as well let vector do its job and optimize HOW the items are added.

For example

void addToContainer(Object object) // pass by value. Possible copy 
{
    container.push_back(object); // copy
}

Those copies can be expensive. Get rid of them.

void addToContainer(Object && object) //no copy and can still handle temporaries
{
    container.push_back(std::move(object)); // moves rather than copies 
}

std::string is often very cheap to move.

This variant of addToContainer can be used with

addToContainer({ "Fira", "+5 ATTACK" });
addToContainer({ "Potion", "+10 HP" });

and might just migrate a pointer and as few book-keeping variables per string. They are temporaries, so no one cares if it will rips their guts out and throws away the corpses.

As for existing Objects

Object o{"Pizza pop", "+5 food"};
addToContainer(std::move(o));

If they are expendable, they get moved as well. If they aren't expendable...

void addToContainer(const Object & object) // no copy
{
    container.push_back(object); // copy
}

You have an overload that does it the hard way.

Tossing this one out there

If you already have a number of items you know are going to be in the list, rather than appending them all one at a time, use an initialization list:

vector<Object> container{
    {"Vorpal Cheese Grater", "Many little pieces"},
    {"Holy Hand Grenade", "OMG Damage"}
};
Community
  • 1
  • 1
user4581301
  • 33,082
  • 7
  • 33
  • 54
  • I have also been seeing people mentioning `emplace_back`. How efficient is using `emplace_back(object)` compared to`push_back(std::move(object))`? – AaySquare May 30 '19 at 15:06
  • @AaySquare `emplace_back` allows you to build the object directly inside the `vector` rather than copying it in. Some good savings to be had here, but if you already have the object built, it is not going to be appreciably different from `push_back` because the in-place build is going to be a copy. I wouldn't be surprised if the compiler generates the same code or calls one in place of the other once the optimizer is turned on. – user4581301 May 30 '19 at 15:47
3

push_back can be extremely expensive, but as with everything, it depends on the context. Take for example this terrible code:

std::vector<float> slow_func(const float* ptr)
{
   std::vector<float> v;
   for(size_t i = 0; i < 256; ++i)
     v.push_back(ptr[i]);
   return v;
}

each call to push_back has to do the following:

  1. Check to see if there is enough space in the vector
  2. If not, allocate new memory, and copy the old values into the new vector
  3. copy the new item to the end of the vector
  4. increment end

Now there are two big problems here wrt performance. Firstly each push_back operation depends upon the previous operation (since the previous operation modified end, and possibly the entire contents of the array if it had to be resized). This pretty much destroys any vectorisation possibilities in the code. Take a look here:

https://godbolt.org/z/RU2tM0

The func that uses push_back does not make for very pretty asm. It's effectively hamstrung into being forced to copy a single float at a time. Now if you compare that to an alternative approach where you resize first, and then assign; the compiler just replaces the whole lot with a call to new, and a call to memcpy. This will be a few orders of magnitude faster than the previous method.

std::vector<float> fast_func(const float* ptr)
{
   std::vector<float> v(256);
   for(size_t i = 0; i < 256; ++i)
     v[i] = ptr[i];
   return v;
}

BUT, and it's a big but, the relative performance of push_back very much depends on whether the items in the array can be trivially copied (or moved). If you example you do something silly like:

struct Vec3 {
   float x = 0;
   float y = 0;
   float z = 0;
};

Well now when we did this:

std::vector<Vec3> v(256);

The compiler will allocate memory, but also be forced to set all the values to zero (which is pointless if you are about to overwrite them again!). The obvious way around this is to use a different constructor:

std::vector<Vec3> v(ptr, ptr + 256);

So really, only use push_back (well, really you should prefer emplace_back in most cases) when either:

  1. additional elements are added to your vector occasionally
  2. or, The objects you are adding are complex to construct (in which case, use emplace_back!)
robthebloke
  • 9,331
  • 9
  • 12
0

without any other requirements, unfortunately this is the most efficient:

 void addToContainer(Object) { } 

to answer the rest of your question. In general push_back will just add to the end of the allocated vector O(1), but will need to grow the vector on occasion, which can be amortized out but is O(N)

also, it would likely be more efficient not to use string, but to keep char * although memory management might be tricky unless it is always a literal being added

Glenn Teitelbaum
  • 10,108
  • 3
  • 36
  • 80
  • OP:there is no limit to how many objects you can add to the container – Glenn Teitelbaum May 30 '19 at 01:07
  • Regardless, if you suspect the amount of entries to be large you can use `reserve` to eliminate some early resizes. For example, reserving 100 could save a resize at 2, 4, 8, 16, 32, 64 or whatever the resize multiple and strategy is. Not so helpful in the long run, but you can't always win. – user4581301 May 30 '19 at 01:18
  • while it is true that a resize strategy would help initially, albeit trivially in the long run, and might help with amortization if used to control growth, with no actual use case, this would be difficult to design – Glenn Teitelbaum May 30 '19 at 01:23