C++ for Quants
  • Home
  • Contact
  • About
Category:

Performance

Multithreading in C++
LibrariesPerformance

Multithreading in C++ for Quantitative Finance

by cppforquants August 23, 2025

Multithreading in C++ is one of those topics that every developer eventually runs into, whether they’re working in finance, gaming, or scientific computing. The language gives you raw primitives, but it also integrates with a whole ecosystem of libraries that scale from a few threads on your laptop to thousands of cores in a data center.

Choosing the right tool matters: what are the right libraries for your quantitative finance use case?

Multithreading in C++

1. Standard C++ Threads (Low-Level Control)

Since C++11, <thread>, <mutex>, and <future> are part of the standard. You manage threads directly, making it portable and dependency-free.

Example: Parallel computation of moving averages in a trading engine

#include <iostream>
#include <thread>
#include <vector>

void moving_average(const std::vector<double>& data, int start, int end) {
    for (int i = start; i < end; i++) {
        if (i >= 2) {
            double avg = (data[i] + data[i-1] + data[i-2]) / 3.0;
            std::cout << "Index " << i << " avg = " << avg << "\n";
        }
    }
}

int main() {
    std::vector<double> prices = {100,101,102,103,104,105,106,107};
    std::thread t1(moving_average, std::ref(prices), 0, 4);
    std::thread t2(moving_average, std::ref(prices), 4, prices.size());

    t1.join();
    t2.join();
}


2. Intel oneTBB (Task-Based Parallelism)

oneTBB (Threading Building Blocks) provides parallel loops, pipelines, and task graphs. Perfect for HPC or financial risk simulations.

Example: Monte Carlo option pricing

#include <tbb/parallel_for.h>
#include <vector>
#include <random>

int main() {
    const int N = 1'000'000;
    std::vector<double> results(N);

    std::mt19937 gen(42);
    std::normal_distribution<> dist(0, 1);

    tbb::parallel_for(0, N, [&](int i) {
        double z = dist(gen);
        results[i] = std::exp(-0.5 * z * z); // toy payoff
    });
}

3. OpenMP (Loop Parallelism for HPC)

OpenMP is widely used in scientific computing. You add pragmas, and the compiler generates parallel code.

#include <vector>
#include <omp.h>

int main() {
    const int N = 500;
    std::vector<std::vector<double>> A(N, std::vector<double>(N, 1));
    std::vector<std::vector<double>> B(N, std::vector<double>(N, 2));
    std::vector<std::vector<double>> C(N, std::vector<double>(N, 0));

    #pragma omp parallel for
    for (int i = 0; i < N; i++)
        for (int j = 0; j < N; j++)
            for (int k = 0; k < N; k++)
                C[i][j] += A[i][k] * B[k][j];
}

4. Boost.Asio (Async Networking and Thread Pools)

Boost.Asio is ideal for low-latency servers, networking, and I/O-heavy workloads (e.g. trading gateways).

#include <boost/asio.hpp>
using boost::asio::ip::tcp;

int main() {
    boost::asio::io_context io;
    tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), 12345));

    std::function<void()> do_accept = [&]() {
        auto socket = std::make_shared<tcp::socket>(io);
        acceptor.async_accept(*socket, [&, socket](boost::system::error_code ec) {
            if (!ec) {
                boost::asio::async_read_until(*socket, boost::asio::dynamic_buffer(std::string()), '\n',
                    [socket](auto, auto) {
                        boost::asio::write(*socket, boost::asio::buffer("pong\n"));
                    });
            }
            do_accept();
        });
    };

    do_accept();
    io.run();
}


5. Parallel STL (<execution>)

C++17 added execution policies for standard algorithms. This makes parallelism easy.

#include <algorithm>
#include <execution>
#include <vector>

int main() {
    std::vector<int> trades = {5,1,9,3,2,8};
    std::sort(std::execution::par, trades.begin(), trades.end());
}



6. Conclusion

Multithreading in C++ offers many models, each fit for different workloads. Use std::thread for low-level control of system tasks. Adopt oneTBB or OpenMP for data-parallel HPC simulations. Leverage Boost.Asio for async networking and trading engines. Rely on CUDA/SYCL for GPU acceleration in Monte Carlo or ML. Enable Parallel STL (<execution>) for easy speed-ups in modern code. Try actor frameworks (CAF/HPX) for distributed, message-driven systems.

Compiler flags also make a big difference in multithreaded performance. Always build with -O3 -march=native (or /O2 in MSVC). Use -fopenmp or link to TBB scalable allocators when relevant. Prevent false sharing with alignas(64) and prefer thread_local scratchpads. Mark non-aliasing pointers with __restrict__ to help vectorization. Consider specialized allocators (jemalloc, TBB) in multi-threaded apps. Profile with -fsanitize=thread to catch race conditions early.

The key: match the concurrency model + compiler setup to your workload for maximum speed.

August 23, 2025 0 comments
Memory management in C++
Performance

Memory Management in C++ High-Frequency Trading Systems

by cppforquants July 12, 2025

High-Frequency Trading (HFT) systems operate under extreme latency constraints where microseconds matter. In this environment, memory management is not just an implementation detail. The ability to predict and control memory allocations, avoid page faults, minimize cache misses, and reduce heap fragmentation can directly influence trading success. What are the best tricks for memory management in C++?

C++ offers low-level memory control unmatched by most modern languages, making it a staple in the HFT tech stack. However, this power comes with responsibility: careless allocations or unexpected copies can introduce jitter, latency spikes, and subtle bugs that are unacceptable in production systems.

In this article, we’ll explore how memory management principles apply in HFT, the common patterns and pitfalls, and how to use modern C++ tools to build robust, deterministic, and lightning-fast trading systems.

1. Preallocation and Memory Pools

A common mitigation strategy is preallocating memory up front and using a memory pool to manage object lifecycles efficiently. This approach ensures allocations are fast, deterministic, and localized, which also improves cache performance.

Let’s walk through a simple example using a custom fixed-size memory pool.

C++ Example: Fixed-Size Memory Pool for Order Objects

#include <iostream>
#include <vector>
#include <bitset>
#include <cassert>

constexpr size_t MAX_ORDERS = 1024;

struct Order {
    int id;
    double price;
    int quantity;

    void reset() {
        id = 0;
        price = 0.0;
        quantity = 0;
    }
};

class OrderPool {
public:
    OrderPool() {
        for (size_t i = 0; i < MAX_ORDERS; ++i) {
            free_slots.set(i);
        }
    }

    Order* allocate() {
        for (size_t i = 0; i < MAX_ORDERS; ++i) {
            if (free_slots.test(i)) {
                free_slots.reset(i);
                return &orders[i];
            }
        }
        return nullptr; // Pool exhausted
    }

    void deallocate(Order* ptr) {
        size_t index = ptr - orders;
        assert(index < MAX_ORDERS);
        ptr->reset();
        free_slots.set(index);
    }

private:
    Order orders[MAX_ORDERS];
    std::bitset<MAX_ORDERS> free_slots;
};

Performance Benefits:

  • No heap allocation: All Order objects are stack-allocated as part of the orders array.
  • O(1) deallocation: Releasing an object is just a bitset flip and a reset.
  • Cache locality: Contiguous storage means fewer cache misses during iteration.

2. Object Reuse and Freelist Patterns

Even with preallocated memory, repeatedly constructing and destructing objects introduces CPU overhead and memory churn. In HFT systems, where throughput is immense and latency must be consistent, reusing objects via a freelist is a proven strategy to reduce jitter and improve performance via a simple trick of memory management in C++.

A freelist is a lightweight structure that tracks unused objects for quick reuse. Instead of releasing memory, objects are reset and pushed back into the freelist for future allocations: a near-zero-cost operation.

C++ Example: Freelist for Reusing Order Objects

#include <iostream>
#include <stack>

struct Order {
    int id;
    double price;
    int quantity;

    void reset() {
        id = 0;
        price = 0.0;
        quantity = 0;
    }
};

class OrderFreelist {
public:
    Order* acquire() {
        if (!free.empty()) {
            Order* obj = free.top();
            free.pop();
            return obj;
        }
        return new Order();  // Fallback allocation
    }

    void release(Order* obj) {
        obj->reset();
        free.push(obj);
    }

    ~OrderFreelist() {
        while (!free.empty()) {
            delete free.top();
            free.pop();
        }
    }

private:
    std::stack<Order*> free;
};

Performance Benefits:

  • Reusing instead of reallocating: Objects are reset, not destroyed — drastically reduces allocation pressure.
  • Stack-based freelist: LIFO behavior benefits CPU cache reuse due to temporal locality (recently used objects are reused soon).
  • Amortized heap usage: The heap is only touched when the freelist is empty, which should rarely happen in a tuned system.

3. Use Arena Allocators

When stack allocation isn’t viable — e.g., for large datasets or objects with dynamic lifetimes — heap usage becomes necessary. But in HFT, direct new/delete or malloc/free calls are risky due to latency unpredictability and fragmentation.

This is where placement new and arena allocators come into play.

  • Placement new gives you explicit control over where an object is constructed.
  • Arena allocators preallocate a large memory buffer and dole out chunks linearly, eliminating the overhead of general-purpose allocators and enabling bulk deallocation.

These techniques are foundational for building fast, deterministic allocators in performance-critical systems like trading engines and improve memory management in C++.

C++ Example: Arena Allocator with Placement new

#include <iostream>
#include <vector>
#include <cstdint>
#include <new>      // For placement new
#include <cassert>

constexpr size_t ARENA_SIZE = 4096;

class Arena {
public:
    Arena() : offset(0) {}

    void* allocate(size_t size, size_t alignment = alignof(std::max_align_t)) {
        size_t aligned_offset = (offset + alignment - 1) & ~(alignment - 1);
        if (aligned_offset + size > ARENA_SIZE) {
            return nullptr; // Out of memory
        }
        void* ptr = &buffer[aligned_offset];
        offset = aligned_offset + size;
        return ptr;
    }

    void reset() {
        offset = 0; // Bulk deallocation
    }

private:
    alignas(std::max_align_t) char buffer[ARENA_SIZE];
    size_t offset;
};

// Sample object to construct inside arena
struct Order {
    int id;
    double price;
    int qty;

    Order(int i, double p, int q) : id(i), price(p), qty(q) {}
};

Performance Benefits

  • Deterministic allocation: Constant-time, alignment-safe, no system heap calls.
  • Zero-cost deallocation: arena.reset() clears all allocations in one go — no destructor calls, no fragmentation.
  • Minimal overhead: Perfect for short-lived objects in bursty, time-sensitive workloads.

Ideal Use Cases in HFT

  • Message parsing and object hydration (e.g., FIX messages → Order objects).
  • Per-frame or per-tick memory lifetimes.
  • Temporary storage in pricing or risk models where objects live for microseconds.

4. Use Custom Allocators in STL (e.g., std::pmr)

Modern C++ introduced a powerful abstraction for memory control in the standard library: polymorphic memory resources (std::pmr). This allows you to inject custom memory allocation behavior into standard containers like std::vector, std::unordered_map, etc., without writing a full custom allocator class.

This is especially valuable in HFT where STL containers may be needed temporarily (e.g., per tick or per packet) and where you want tight control over allocation patterns, lifetime, and performance.

C++ Example: Using std::pmr::vector with an Arena

#include <iostream>
#include <memory_resource>
#include <vector>
#include <string>

int main() {
    constexpr size_t BUFFER_SIZE = 1024;
    char buffer[BUFFER_SIZE];

    // Set up a monotonic buffer resource using stack memory
    std::pmr::monotonic_buffer_resource resource(buffer, BUFFER_SIZE);

    // Create a pmr vector that uses the custom memory resource
    std::pmr::vector<std::string> symbols{&resource};

    // Populate the vector
    symbols.emplace_back("AAPL");
    symbols.emplace_back("MSFT");
    symbols.emplace_back("GOOG");

    for (const auto& s : symbols) {
        std::cout << s << "\n";
    }

    // All memory is deallocated at once when `resource` goes out of scope or is reset
}

Benefits for HFT Systems

  • Scoped allocations: The monotonic_buffer_resource allocates from the buffer and never deallocates until reset — perfect for short-lived containers (e.g., market snapshots).
  • No heap usage: Memory is pulled from the stack or a preallocated slab, avoiding malloc/free.
  • STL compatibility: Works with all std::pmr:: containers (vector, unordered_map, string, etc.).
  • Ease of integration: Drop-in replacement for standard containers — no need to write full allocator classes.

pmr Design Philosophy

  • Polymorphic behavior: Containers store a pointer to an std::pmr::memory_resource, enabling allocator reuse without changing container types.
  • Composable: You can plug in arenas, pools, fixed-size allocators, or even malloc-based resources depending on the use case.

Common pmr Resources

ResourceUse Case
monotonic_buffer_resourceFast, one-shot allocations (e.g., per tick)
unsynchronized_pool_resourceSmall object reuse with subpooling (no mutex)
synchronized_pool_resourceThread-safe version of above
CustomArena/slab allocators for domain-specific control

July 12, 2025 0 comments
c++ for performance
Performance

C++ for Performance: 5 Ideas to Speed Up Your Quantitative Code

by cppforquants May 16, 2024

In quantitative finance, milliseconds can mean millions. Whether you’re pricing exotic derivatives, processing high-frequency trades, or running Monte Carlo simulations, performance is non-negotiable. C++ remains the go-to language for building ultra-fast systems thanks to its low-level control and fine-tuned memory management. What are 5 tricks to optimize C++ for performance?

1. Prefer Stack Allocation Over Heap

Heap allocations (new/delete) are costly due to the overhead of dynamic memory management and potential fragmentation. Stack allocation, on the other hand, is faster, safer, and automatically cleaned up when the scope ends:

Stack vs. Heap Memory Table

ParameterStackHeap
Data type structureLinear (LIFO: Last In, First Out)Hierarchical access possible; no fixed structure
Basic allocationMemory is allocated contiguously and sequentiallyMemory can be contiguous, but is not guaranteed (depends on allocator and fragmentation)
Allocation & DeallocationAutomatic by compiler (on function entry/exit)Manual (new/delete, malloc/free) or via smart pointers
CostVery low overheadHigher overhead due to allocator logic and possible fragmentation
Limit of space sizeFixed limit per thread (set by OS, typically MBs)Limited by total available system memory
Access timeVery fast (predictable layout, cache-friendly)Slower (more indirection, potential page faults)
FlexibilityFixed-size, defined at compile timeDynamically resizable
SizeTypically smallCan grow large (useful for big data structures)
ResizeNot resizable after allocationResizable (e.g., with realloc, std::vector::resize())

So if you want speed, choose the stack allocation:

// Slower: heap allocation
MyMatrix* mat = new MyMatrix(1000, 1000); 
process(*mat);
delete mat;

// Faster: stack allocation
MyMatrix mat(1000, 1000); 
process(mat);

Use smart pointers or containers only when dynamic allocation is necessary, and favor std::array or std::vector with reserved capacity for fixed-size needs.

2. Avoid Virtual Functions in Hot Paths

A virtual function lets C++ decide at runtime which version of a function to call, based on the actual object type, not the pointer or reference type.

Virtual functions use vtables for dynamic dispatch, introducing a level of indirection that prevents inlining and hurts CPU branch prediction.

A vtable (short for virtual table) is a mechanism used by C++ to implement runtime polymorphism — specifically, virtual function calls.

When a class has at least one virtual function, the compiler generates:

  • A vtable: a table of function pointers for that class.
  • A vptr (virtual table pointer): a hidden pointer added to each object instance, pointing to the appropriate vtable.

In tight loops or latency-critical sections, replacing virtual calls with alternatives like templates or function pointers can significantly improve performance.

// Slower: virtual dispatch
struct Instrument {
    virtual double price() const = 0;
};

double sumPrices(const std::vector<Instrument*>& instruments) {
    double total = 0;
    for (const auto* instr : instruments) {
        total += instr->price(); // Virtual call
    }
    return total;
}

Using templates is way more performant:

#include <iostream>
#include <vector>

struct Bond {
    double price() const { return 100.0; }
};

template<typename T>
double sumPrices(const std::vector<T>& instruments) {
    double total = 0;
    for (const auto& instr : instruments) {
        total += instr.price();  // Resolved at compile time, can be inlined
    }
    return total;
}

3. Use reserve() for Vectors

When using std::vector, every time you push back an element beyond its current capacity, it must allocate a new memory block, copy existing elements, and deallocate the old one — which is expensive. In performance-critical paths like simulations or data loading, this overhead adds up quickly.

If you know (or can estimate) the number of elements in advance, call vector.reserve(n) to allocate memory once upfront. This avoids repeated reallocations and boosts speed significantly.

std::vector<double> prices;

// Inefficient: multiple reallocations as vector grows
for (int i = 0; i < 1'000'000; ++i) {
    prices.push_back(i * 0.01);
}

// Better: allocate memory once
std::vector<double> fast_prices;
fast_prices.reserve(1'000'000);  // Preallocate
for (int i = 0; i < 1'000'000; ++i) {
    fast_prices.push_back(i * 0.01);
}

Why Not Always Use the Stack?
The stack is fast because:

  • Allocation/deallocation is automatic.
  • It’s contiguous and cache-friendly.
  • No fragmentation or dynamic bookkeeping.

But it comes with strict limitations: the stack size is limited and it’s not resizable (fixed at compile time or needs C99-style VLAs with compiler extension).

4. Leverage Compiler Optimizations

Modern C++ compilers are incredibly powerful but you have to ask for the performance. By default, they prioritize portability and safety over speed. Turning on aggressive optimization flags like -O2, -O3, -march=native, and -flto enables advanced techniques like loop unrolling, inlining, vectorization, and dead code elimination.

These flags can deliver huge speedups especially for compute-heavy quant workloads like Monte Carlo simulations, matrix operations, or pricing curves.

# Basic optimization
g++ -O2 mycode.cpp -o myapp

# Aggressive + hardware-specific + link-time optimization
g++ -O3 -march=native -flto mycode.cpp -o myapp

🧠 Key Flags:

  • -O2: General optimizations (safe default).
  • -O3: Adds aggressive loop optimizations and inlining.
  • -march=native: Tailors code to your CPU (uses AVX, SSE, etc.).
  • -flto: Link-time optimization — lets compiler optimize across translation units.

⚠️ Use profiling tools like perf, gprof, or valgrind to validate the gains and sometimes -O3 can make things faster, but also larger or harder to debug.

5. Minimize Lock Contention

In multi-threaded quant systems, excessive use of std::mutex can serialize threads, causing performance bottlenecks. Lock contention happens when multiple threads fight to acquire the same lock, leading to context switches and degraded latency.

To reduce contention:

  • Keep critical sections short.
  • Use std::atomic for simple shared data.
  • Prefer lock-free structures or per-thread buffers where possible.

Example: Avoiding mutex with std::atomic

std::atomic<int> counter = 0;

// Thread-safe increment without a lock
void safeIncrement() {
    counter.fetch_add(1, std::memory_order_relaxed);
}


May 16, 2024 0 comments
  • 1
  • 2

@2025 - All Right Reserved.


Back To Top
  • Home
  • Contact
  • About