push_back() vs emplace_back(): The Benchmark That Made Me Delete a Post
- 21 hours ago
- 7 min read


I had to delete my previous post because the benchmark lied to me.
That is already a very C++ way to start a day.
The topic sounded simple enough: compare push_back() and emplace_back() for std::vector<std::string>, show the results for C++11 and C++23, and extract a nice practical lesson.
Simple, right?
Well, not quite.
At first, I used older compilers on Quick-Bench and got results that looked strange. Strange enough that after double-checking, I realized I had stepped on a compiler regression and the conclusions I published were just wrong. So I deleted the post, reran the benchmarks locally with clang 20.0.1, and got something much more reasonable.
And the funny part is that the corrected results were actually more interesting than the bogus ones.
Because once again, C++ reminded me of an important rule:
Performance advice that sounds universal usually is not.
The Myth
Since C++11, many of us absorbed the idea that emplace_back() is simply the modern, faster choice.
The rule is usually presented like this:
Always prefer emplace_back() over push_back() because it constructs the object in place and avoids extra copies or moves.
That sounds logical, and in some cases it is absolutely correct.
But like many performance rules in C++, it becomes dangerous the moment people stop asking what is actually being constructed, what kind of argument is passed, and what the compiler can optimize away anyway.
That is exactly where these benchmarks become useful.
What push_back() and emplace_back() Actually Do
Before jumping into the graphs, it helps to remember the conceptual difference.
push_back() takes an object that already exists and appends it to the vector. That usually means copying or moving that object into the vector’s storage.
emplace_back() constructs the object directly inside the vector’s storage using the arguments you provide.
This means that emplace_back() can save work when the arguments are raw constructor arguments, because no temporary object needs to be created first.
For example, this is where emplace_back() shines:
std::vector<std::string> v;v.emplace_back("hello");Here the vector can construct the std::string directly in place from the string literal.
Meanwhile, with push_back(), you would typically write:
v.push_back(std::string("hello"));Now a temporary std::string exists first, and then it is moved or copied into the vector.
At least, that is the classic explanation.
Modern compilers, however, are not asleep. And modern C++ has improved the rules around prvalues and copy elision. So the real-world difference is often much smaller than that simple story suggests.
The Benchmark Setup
I compared push_back() and emplace_back() in two different situations:
The first set uses strings that fit into SSO, short string optimization.
The second set uses strings that are large enough to miss SSO, which means heap allocation enters the picture.
I then looked at the results under both C++11 and C++23.
This gives us a nice matrix:
push_back() vs emplace_back()
short strings vs long strings
old standard vs modern standard
That is where the story gets interesting.
First Graph: Strings That Fit Into SSO
The first graph shows the behavior when inserting strings small enough to fit into the implementation’s short string optimization buffer.
This matters because SSO changes the cost model of std::string. When a string fits into SSO, it can often avoid heap allocation entirely. That means operations on the string become much cheaper, and things like moves, copies, and construction mechanics become more visible in the final timing.
And here the results are very instructive.
In C++11, emplace_back() clearly wins. This is especially obvious when inserting a raw string literal. That makes perfect sense: emplace_back() constructs the string directly inside the vector, while push_back() typically has to deal with a temporary std::string first and then move it.
When the work is cheap overall, even relatively small extra steps can show up clearly in the benchmark.
So in this first graph, the classic advice looks pretty good.
If all you saw were these results, you might walk away saying:
“See? emplace_back() is faster. End of story.”
But then C++ does what C++ always does: it adds more story.
What Changes in C++23?
When moving from C++11 to C++23, the advantage of emplace_back() is still visible for raw constructor arguments such as a string literal.
That is not surprising. Constructing directly in place is still a good path, and there were not some magical language changes that removed the benefit of direct construction from raw arguments.
But when we compare the case of an rvalue std::string, the difference becomes much smaller.
This is the important shift.
Modern compilers have become much better at optimizing these cases. On top of that, the language rules around prvalues and elision are more favorable than they used to be. So the old mental model of “temporary object means definitely slower” is much less reliable now.
In other words, in C++23, push_back(std::string(...)) does not automatically look like the clumsy, old-fashioned option people sometimes imagine.
That does not make push_back() universally equal.
It just means the gap is often much smaller than folklore would suggest.
Second Graph: Strings Without SSO
Now let’s look at the second graph.
Here the strings are larger, so they do not fit into SSO.
This changes everything.
Once a string grows beyond the SSO buffer, the implementation needs heap allocation. Now the cost is no longer dominated by whether we created one extra temporary or avoided one move. Instead, the real cost starts to come from:
allocating memory
managing buffers
copying character data
possibly deallocating or transferring ownership
This is where the bottleneck shifts.
And once that happens, emplace_back() is no longer the automatic hero of the story.
Why emplace_back() Helps Less Here
When the expensive part of the operation is heap allocation and internal string construction, the difference between “construct directly in place” and “construct temporary and then move” may become much less important.
Why?
Because moving a big std::string is usually already pretty cheap compared to building its heap-managed content in the first place. If the dominant cost is allocating and filling the buffer, saving one move may not matter much.
That is why, in the second graph, the advantage of emplace_back() shrinks dramatically.
And now we get the fun part.
The Surprising C++11 Result
In this non-SSO case, C++11 push_back() can actually be faster.
That is exactly the kind of result that annoys dogmatic advice and makes benchmarks worth doing.
At first glance, it feels wrong. If emplace_back() avoids extra work, how can it lose?
The answer is that the model “emplace avoids work, therefore emplace always wins” is too simplistic.
Once heap allocation is involved, the performance picture is shaped by a bigger chain of operations. The cost center moves away from copy/move mechanics and toward allocation and construction details. In that environment, a path that looked “obviously optimal” may no longer be the fastest.
And that is one of the nicest lessons from this benchmark:
Once the expensive work changes, the winning strategy can change too.
Why C++23 Flattens the Difference
By the time we get to C++23, the second graph becomes almost boring.
And boring is very educational here.
The results are so close that the difference nearly disappears.
That is not because push_back() and emplace_back() became the same function. They did not.
It is because modern compilation and language rules make the optimized paths so similar that, in practice, there is often very little left to measure between them in these scenarios.
That is especially true when the dominant cost is elsewhere, such as memory allocation.
So by C++23, once we are dealing with non-SSO strings, the timing differences become small enough that they often stop being interesting.
And honestly, that is useful knowledge.
Because when two options are effectively tied, the discussion should stop being “which one is theoretically cooler?” and start being “which one expresses intent more clearly?”
So Which One Should You Use?
This is the part where people want a clean rule.
Unfortunately, the clean rule is not clean enough to fit on a sticker.
Still, here is the practical version.
Use emplace_back() when you are passing constructor arguments directly and you want the object built in place.
That is exactly what it was designed for.
Use push_back() when you already have an object and want to append it.
That is exactly what it was designed for.
And do not force emplace_back() everywhere just because someone said it is modern and therefore must be better.
In fact, using emplace_back() with an already-constructed object often gives you no meaningful advantage and can even make code read as if some fancy construction is happening when it really is not.
So the practical guidance is:
If you already have the object, push_back() is usually the natural choice.
If you want to construct from arguments directly inside the vector, emplace_back() is often the natural choice.
If performance matters, benchmark the real case instead of relying on folklore.
That is already enough to make better decisions.
The Important Edge Case: Non-Copyable, Non-Movable Types
There is also one case where this is not just about performance.
Sometimes an object is not copyable and not movable. In that case, push_back() may simply not compile, because it needs an object to insert.
But emplace_back() can still work, because it constructs the object directly in place.
That means emplace_back() is not only a performance tool. It is also sometimes the only valid API for expressing what you want.
So while this post is mostly about benchmark behavior, it is worth remembering that emplace_back() can also be important for correctness and expressiveness.
Another important thing to know that in libstdc++ and libc++ you will have this:
#if __cplusplus >= 201103L
void
push_back(value_type&& __x)
{ emplace_back(std::move(__x)); }
What This Benchmark Really Teaches
For me, the biggest lesson is not “use push_back() more” or “use emplace_back() less.”
The real lesson is that performance intuition in C++ is fragile.
It is very easy to learn a rule that was once broadly true, simplify it too much, repeat it for years, and then get surprised when a real benchmark says otherwise.
That does not mean the old rule was useless.
It just means it was incomplete.
And that is normal in C++.
Because C++ performance depends on many layers at once:
language version
compiler quality
library implementation
argument category
object size
allocation behavior
optimization opportunities
Change any one of those, and the result can shift.
That is why “always” is such a dangerous word in performance discussions.
Final Takeaway
If I had to compress the whole article into one line, it would be this:
emplace_back() is not magically faster, and push_back() is not old and useless.
For SSO-friendly cases, emplace_back() can still show a clear advantage, especially with raw constructor arguments.
For larger strings, heap allocation can dominate so heavily that the difference mostly disappears.
And in modern C++, especially in C++23, compiler optimizations narrow the gap so much that many old assumptions become much less useful.
So the real rule is not:
Always use emplace_back().
The real rule is:
Don’t benchmark your assumptions. Benchmark your code.
And honestly, that is one of the reasons I love C++.
Even the “obvious best practice” can suddenly stop being obvious, laugh at your benchmark, and force you to think a little deeper.
That is frustrating.
And also kind of beautiful.
If you want, I can turn this into a more polished blog format with section subtitles optimized for publishing on Medium, LinkedIn articles, or your personal blog.


