Performance Optimization
Move Semantics and Avoiding Copies
Move semantics allow transferring resource ownership from one object to another, avoiding expensive deep copies:
std::vector<int> create_large_vector() {
std::vector<int> v(1000000);
std::iota(v.begin(), v.end(), 0);
return v; // NRVO or move, O(1)
}
// Accept by value — caller can choose move or copy
void process(std::vector<int> data) { /* ... */ }
process(create_large_vector()); // move
process(std::move(my_vec)); // move
// Perfect forwarding — preserves value category
template <typename T>
void wrapper(T&& arg) { target(std::forward<T>(arg)); }
// ⚠️ Do not use std::move on return values — it prevents NRVO
std::string make_string() {
std::string s = "hello";
return s; // ✅ allows compiler optimization (NRVO)
}Small String Optimization (SSO)
Most standard library implementations use SSO for std::string: short strings (typically ≤ 15–22 bytes) are stored directly inside the object, avoiding heap allocation.
std::string short_str = "hello"; // zero heap allocation (SSO buffer)
std::string long_str(100, 'x'); // one heap allocation
// GCC: empty capacity = 15, libc++: 22SSO implications: short string operations are fast (no heap allocation), but sizeof(std::string) is larger (24 bytes for libc++, 32 bytes for libstdc++/MSVC), and moving a short string may require a copy (data is inline).
Cache-Friendly Data Structures
CPU cache effects on performance often far exceed algorithmic complexity. O(n) traversal of contiguous memory is typically much faster than O(log n) traversal of scattered nodes.
// Cache-unfriendly — linked list nodes scattered across the heap
struct ListNode { int value; ListNode* next; };
// SoA (Structure of Arrays) — most friendly for SIMD and cache prefetch
struct Particles { std::vector<float> x, y, z; };
// When only x is needed, fills entire cache line; AoS wastes 2/3 of space
// C++23: flat_map — sorted vector instead of red-black tree
#include <flat_map>
std::flat_map<int, std::string> fm; // several times faster for small datasets
// Object pool — avoids fragmented allocation
template <typename T, std::size_t BlockSize = 4096>
class ObjectPool {
struct Block { alignas(T) char data[sizeof(T) * BlockSize]; };
std::vector<std::unique_ptr<Block>> blocks_;
std::size_t used_ = 0;
public:
template <typename... Args>
T* allocate(Args&&... args) {
if (used_ >= BlockSize * blocks_.size())
blocks_.push_back(std::make_unique<Block>());
auto* block = blocks_.back().get();
auto offset = used_ % BlockSize;
++used_;
return new (block->data + offset * sizeof(T)) T(std::forward<Args>(args)...);
}
};Branch Prediction Hints
C++20's [[likely]]/[[unlikely]] help the compiler optimize branch layout:
int process(int value) {
if (value >= 0) [[likely]] { return value * 2; }
else [[unlikely]] { throw std::out_of_range("negative"); }
}
// GCC/Clang legacy approach
#define LIKELY(x) __builtin_expect(!!(x), 1)
#define UNLIKELY(x) __builtin_expect(!!(x), 0)Affects code layout (hot/cold path separation), does not control conditional move instruction generation.
SIMD Vectorization
Status note (2026-06-02):
std::simdhas been incorporated into the C++26 draft (P1928) and has been voted in. GCC 14+ provides<experimental/simd>as an experimental implementation (namespacestd::experimental). Clang/LLVM and MSVC do not yet provide full support. The examples below use the TS namespace; switch tostd::simdonce compilers provide official support.
#include <experimental/simd>
namespace stdx = std::experimental;
void vector_add(const float* a, const float* b, float* out, std::size_t n) {
using V = stdx::native_simd<float>; // 4(SSE)/8(AVX)/16(AVX-512)
constexpr auto w = V::size();
std::size_t i = 0;
for (; i + w <= n; i += w) {
// ⚠️ vector_aligned requires pointers to satisfy native_simd alignment requirements
// In practice, ensure a/b/out are allocated with V::alignment() alignment
V va(a + i, stdx::vector_aligned);
V vb(b + i, stdx::vector_aligned);
(va + vb).copy_to(out + i, stdx::vector_aligned);
}
for (; i < n; ++i) out[i] = a[i] + b[i]; // scalar tail handling
}Compile example (GCC 14+): g++ -std=c++26 -O3 -march=native -I/path/to/experimental/simd
Allocator-Aware Containers
// C++17 PMR allocator
#include <memory_resource>
char buffer[4096];
std::pmr::monotonic_buffer_resource pool{buffer, sizeof(buffer)};
std::pmr::vector<int> vec{&pool}; // zero system calls
// Thread-local reuse avoids hot-path allocation
thread_local std::string buf;
void hot_path(const char* input) {
buf.clear(); buf.append(input);
}Benchmarking Tools
// Google Benchmark — industry standard
#include <benchmark/benchmark.h>
static void BM_PushBack(benchmark::State& state) {
for (auto _ : state) {
std::vector<int> v;
for (int i = 0; i < state.range(0); ++i) v.push_back(i);
benchmark::DoNotOptimize(v.data());
}
}
BENCHMARK(BM_PushBack)->Range(8, 1 << 20);- quick-bench.com: online comparative benchmarking
- perf (Linux):
perf stat -e cache-misses,branch-misses ./bench - Tracy Profiler: real-time frame-level profiling, cross-platform
Agner Fog Guidelines
struct alignas(64) CacheLineAligned { float data[16]; }; // cache line aligned
// __restrict__ hints to the compiler that vectorization is safe
void add(float* __restrict__ a, const float* __restrict__ b, std::size_t n) {
for (std::size_t i = 0; i < n; ++i) a[i] += b[i];
}Core discipline: measure first, optimize second. Without benchmark data, any optimization is guesswork.