AlgoDaily

Low-Latency C++ for HFT

Read at your own pace. Code execution, quizzes, progress tracking, and the AI tutor are available in the interactive view with JavaScript enabled.

Why Low Latency

From market data to decisions

In Course Overview & Environment Setup, you built the toolchain and measured a tiny program. In Market Microstructure & Data Feeds, you saw quotes, trades, sequence numbers, and timestamps.

Now we connect those ideas to C++ performance:

market event -> parse -> update state -> decide -> emit action

In HFT, latency is not just “make it fast.” It means:

  • Predictable runtime: fewer surprise pauses
  • Low tail latency: not just average speed, but rare slow cases
  • Cache-aware data: the CPU should find what it needs quickly
  • Measured decisions: optimize only after timing

This lesson stays inside one process and one thread. Later lessons will handle networking, kernel bypass, and lock-free concurrency.

Why Low Latency

Latency Budget

Think in budgets

A low-latency C++ system is easier to reason about when each stage has a budget.

Example target for a simplified hot path:

Read decoded event       50 ns
Update instrument state  80 ns
Check simple signal      40 ns
Create internal action   60 ns
--------------------------------
Total                   230 ns

The exact numbers are less important than the habit: every operation spends time.

Two important terms:

  • Hot path: code executed for every market event or order decision
  • Cold path: startup, configuration, logging setup, error reporting, admin tools

Beginner-friendly rule: put complexity in the cold path when possible, and keep the hot path boring, small, and measurable.

Latency Budget

Measure First

A tiny benchmark harness

Before optimizing, measure. C++ gives you low-level control, but also many ways to fool yourself.

This small program measures a simple integer loop. It is not a perfect benchmark, but it demonstrates the basic workflow:

  1. Run enough iterations
  2. Prevent the compiler from deleting the work
  3. Print total time and time per operation

Compile with optimization enabled, for example:

g++ -O2 -std=c++17 measure.cpp && ./a.out
```cpp
#include <chrono>
#include <cstdint>
#include <iostream>

volatile std::uint64_t sink = 0;

int main() {
    constexpr std::uint64_t N = 50'000'000;
    std::uint64_t x = 0;

    auto start = std::chrono::steady_clock::now();

    for (std::uint64_t i = 0; i < N; ++i) {
        x += (i * 3) ^ (i >> 2);
    }

    sink = x;

    auto end = std::chrono::steady_clock::now();
    auto ns = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count();

    std::cout << "result=" << sink << "\n";
    std::cout << "total_ns=" << ns << "\n";
    std::cout << "ns_per_iteration=" << static_cast<double>(ns) / N << "\n";
}
```

Hot Path Check

Which operation is usually most suspicious inside a per-market-event hot path?

Cache Shapes

The CPU is fast; memory is not

Modern CPUs can execute simple arithmetic extremely quickly. But if data is far away in memory, the CPU waits.

For HFT code, this means data layout matters.

Common low-latency preferences:

  • Keep hot fields close together
  • Avoid pointer chasing in the hot path
  • Prefer contiguous arrays when scanning many items
  • Keep rarely used fields away from frequently used fields

Imagine tracking many instruments. If your strategy only needs bid, ask, and last_trade_price, placing those fields in a compact, predictable layout can matter more than adding clever code.

Cache Shapes

Layout Experiment

Contiguous data helps scans

This example compares two ways to store market state for many instruments:

  • Array of structs: each instrument keeps all fields together
  • Struct of arrays: each field has its own contiguous array

If we only scan bid, struct-of-arrays often gives the CPU a simpler memory pattern. Real results depend on your CPU and compiler, so treat this as a measurement habit, not a universal law.

```cpp
#include <chrono>
#include <cstdint>
#include <iostream>
#include <vector>

struct QuoteAOS {
    double bid;
    double ask;
    double last;
    std::uint64_t seq;
};

volatile double double_sink = 0.0;

template <typename F>
long long time_ns(F&& f) {
    auto start = std::chrono::steady_clock::now();
    f();
    auto end = std::chrono::steady_clock::now();
    return std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count();
}

int main() {
    constexpr std::size_t N = 2'000'000;

    std::vector<QuoteAOS> aos(N);
    std::vector<double> bids(N), asks(N), lasts(N);

    for (std::size_t i = 0; i < N; ++i) {
        double price = 100.0 + static_cast<double>(i % 100) * 0.01;
        aos[i] = QuoteAOS{price, price + 0.01, price - 0.02, i};
        bids[i] = price;
        asks[i] = price + 0.01;
        lasts[i] = price - 0.02;
    }

    double sum1 = 0.0;
    auto aos_ns = time_ns([&] {
        for (const auto& q : aos) sum1 += q.bid;
    });

    double sum2 = 0.0;
    auto soa_ns = time_ns([&] {
        for (double b : bids) sum2 += b;
    });

    double_sink = sum1 + sum2;

    std::cout << "aos_bid_scan_ns=" << aos_ns << "\n";
    std::cout << "soa_bid_scan_ns=" << soa_ns << "\n";
    std::cout << "checksum=" << double_sink << "\n";
}
```

Allocation Claim

True or false: calling reserve() on a std::vector guarantees the vector will never allocate again, no matter how many elements are later inserted.

Preallocate Buffers

Move allocation out of the hot path

A common HFT C++ pattern is:

  1. Allocate during startup
  2. Reuse memory during event processing
  3. Avoid unexpected growth in the hot path

std::vector::reserve() is not magic, but it lets you prepare capacity before processing begins.

The program below logs when vector capacity changes. Capacity changes are a clue that allocation may have happened.

```cpp
#include <iostream>
#include <vector>

void push_and_log(const char* label, std::vector<int>& v, int count) {
    std::cout << label << "\n";
    std::size_t last_capacity = v.capacity();
    std::cout << "initial_capacity=" << last_capacity << "\n";

    for (int i = 0; i < count; ++i) {
        v.push_back(i);
        if (v.capacity() != last_capacity) {
            last_capacity = v.capacity();
            std::cout << "  capacity_changed_at_size=" << v.size()
                      << " new_capacity=" << last_capacity << "\n";
        }
    }
}

int main() {
    constexpr int events = 20;

    std::vector<int> without_reserve;
    push_and_log("without reserve", without_reserve, events);

    std::vector<int> with_reserve;
    with_reserve.reserve(events);
    push_and_log("with reserve", with_reserve, events);
}
```

Name The Path

Fill in the blank: Code executed for every market event or decision is called the ______.

Latency Checklist

What to carry forward

Low-latency C++ is not about using obscure tricks first. It is about building code that is measurable and predictable.

Use this checklist when writing HFT-style C++:

  • Measure before and after every optimization
  • Separate hot path from cold path
  • Preallocate buffers used during event processing
  • Prefer contiguous data for scans and tight loops
  • Keep hot structs small and focused on frequently used fields
  • Avoid surprise work: allocation, formatting, exceptions, excessive logging
  • Watch tail latency, not only average latency

In the next lesson, we will move closer to the machine boundary and discuss how market data reaches your process through networking choices.

Latency Checklist

Open interactive view →