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.