Performance
|
This section documents C++23 (ISO/IEC 14882:2024), as published by ISO/IEC JTC1/SC22/WG21 (wg21), verified against the freely available working draft N5046 (eel.is/c++draft) and cppreference.com. This content was generated with the assistance of AI and should be verified against the working draft and cppreference.com before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
The Zero-Overhead Principle
Stroustrup’s founding design rule for C++: a feature you don’t use costs you nothing, and a feature you do use should cost no more than writing the equivalent by hand. Classes, templates, and exceptions (when not thrown) compile down to code as fast as the hand-written C equivalent — the abstraction is "free" at run time, paid only in compile time and, for virtual functions, one pointer indirection (see Inheritance and Polymorphism).
Measure Before Optimizing
Intuition about where time is spent is frequently wrong — profile first, always:
perf record ./myprogram
perf report
# or, for a quick wall-clock A/B comparison without a full profiler:
g++ -O2 -o bench bench.cpp && time ./bench
Optimizing code that isn’t actually the bottleneck wastes effort and adds complexity/risk for zero measured benefit — the classic "premature optimization" trap.
Avoiding Copies
Covered mechanically in Move Semantics and Value Categories and Strings and Text/ Containers; as a checklist:
#include <string>
#include <string_view>
#include <span>
#include <vector>
void takesByValue(std::string s); // copies (or moves, if the caller passes an rvalue)
void takesByConstRef(const std::string& s); // never copies
void takesByView(std::string_view s); // never copies, works on literals/substrings too
void takesVectorByConstRef(const std::vector<int>& v); // never copies
void takesSpan(std::span<const int> v); // never copies, ALSO works on C arrays/std::array
Default to const T&/std::string_view/std::span for read-only parameters; reserve by-value parameters for
when the function genuinely needs its own copy (and let move semantics make even that cheap for an rvalue
argument).
Reserving and Contiguous Containers
#include <vector>
std::vector<int> withoutReserve;
for (int i = 0; i < 10000; ++i) withoutReserve.push_back(i); // may reallocate (and copy/move existing
// elements) roughly log2(10000) times
std::vector<int> withReserve;
withReserve.reserve(10000); // one allocation, up front
for (int i = 0; i < 10000; ++i) withReserve.push_back(i); // zero reallocations
Prefer std::vector/std::array (contiguous, cache-friendly) over std::list/std::map unless a specific
access pattern (frequent middle insertion, ordered keys) genuinely needs them — see
Containers's decision chart.
constexpr Evaluation
Moving work from run time to compile time (see Compile-Time Programming) trades compile time for zero run-time cost — most valuable for values that are genuinely fixed at compile time (lookup tables, fixed configuration), not for anything depending on run-time input.
Inlining and [[likely]]
The compiler already inlines small, hot functions automatically at -O2/-O3 — inline today is mostly
about the ODR exception for header-defined functions (see
Program Structure and Compilation), not a
performance directive the compiler is obligated to follow. [[likely]]/[[unlikely]] (see
Control Flow) are the same: hints, not guarantees — both are
only worth adding once profiling shows a specific branch matters.
Allocation Strategies
Heap allocation (new, std::vector growth, std::string beyond its Small String Optimization threshold) is
one of the more expensive common operations in C++. Beyond reserve():
#include <memory_resource>
#include <array>
#include <vector>
std::array<std::byte, 4096> stackBuffer{};
std::pmr::monotonic_buffer_resource pool(stackBuffer.data(), stackBuffer.size());
std::pmr::vector<int> fastVector(&pool); // allocates from stackBuffer, not the heap, until exhausted
See Memory Management and Smart
Pointers for std::pmr in more depth; object pools and arena allocators follow the same idea for
higher-throughput allocation patterns.
Undefined Behavior and the Optimizer
The optimizer is allowed to assume undefined behavior never happens, and will silently produce surprising (not necessarily "safe-looking") code when it does:
int dangerous(int x) {
return x + 1 > x; // the compiler may assume signed overflow (UB) never happens, and optimize this
} // to "return true;" unconditionally -- NOT to "check whether x+1 actually overflowed"
This is why -Wall -Wextra alone does not catch every UB-adjacent bug, and why sanitizers (below) exist as a
complementary, run-time-checking layer.
Profiling and Sanitizers
# AddressSanitizer: catches heap/stack buffer overflows, use-after-free, use-after-return
g++ -fsanitize=address -g -o app app.cpp && ./app
# UndefinedBehaviorSanitizer: catches signed overflow, null derefs, misaligned access, and more
g++ -fsanitize=undefined -g -o app app.cpp && ./app
# ThreadSanitizer: catches data races
g++ -fsanitize=thread -g -o app app.cpp && ./app
Sanitizers add significant run-time overhead (2-20x, depending on which one) — run them in CI/testing, not in production; see Build and Tooling for how they fit into a project’s build configuration.
See Also
-
C: Performance — the same measure-first discipline,
restrict, and the "undefined behavior lets the optimizer assume" rule.