AlgoDaily

Optimization and Performance Tuning

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

Milliseconds Matter

Optimization begins after correctness

In Implementing Trading Algorithms, you built a strategy layer that can generate signals without spam and with state. Now we tune the path around that strategy so the app can react quickly when market data arrives.

In algo trading, performance tuning is not just “make C++ faster.” It means:

  • measuring the real hot path
  • removing avoidable work
  • controlling memory allocation
  • keeping data cache-friendly
  • making latency predictable, not just occasionally fast

Today we stay focused on runtime performance. In the next lesson, Backtesting and Strategy Analysis, you’ll evaluate strategy quality over historical data.

Milliseconds Matter

Latency Budget

Think in budgets, not vibes

A trading decision is a chain. If the total target is, say, 5 ms, each stage needs a rough budget.

Market tick
   ↓
Parse / validate data      0.5 ms
Update indicators          1.0 ms
Run strategy logic         0.8 ms
Risk / sanity checks       0.7 ms
Create order request       0.5 ms
Network / API send         ? ms

You do not fully control the network, broker, or exchange. You do control your C++ process: data layout, allocations, copies, locks, logging, and algorithmic complexity.

A useful rule: optimize the parts that happen per tick before optimizing setup code that runs once.

Latency Budget

Measure First

Your stopwatch is a feature

Back in Course Introduction and Setting Up Development Environment, you saw the idea: measure early. Now we turn that into a habit.

Use std::chrono::steady_clock for local timing. It is monotonic, so it is safer for measuring elapsed time than wall-clock time.

This example compares pushing into a vector with and without reserve. In a tick-heavy application, repeated growth and reallocation can create latency spikes.

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

using Clock = std::chrono::steady_clock;

long long runWithoutReserve(int n) {
    auto start = Clock::now();
    std::vector<int> prices;
    for (int i = 0; i < n; ++i) {
        prices.push_back(100 + (i % 50));
    }
    auto end = Clock::now();
    return std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
}

long long runWithReserve(int n) {
    auto start = Clock::now();
    std::vector<int> prices;
    prices.reserve(n);
    for (int i = 0; i < n; ++i) {
        prices.push_back(100 + (i % 50));
    }
    auto end = Clock::now();
    return std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
}

int main() {
    const int n = 2'000'000;

    auto noReserve = runWithoutReserve(n);
    auto withReserve = runWithReserve(n);

    std::cout << "Without reserve: " << noReserve << " us\n";
    std::cout << "With reserve:    " << withReserve << " us\n";
    std::cout << "Lower is better. Run more than once to compare trends.\n";
}
```

Best First Move

Your strategy feels slow after connecting it to live market data. What should you do first?

Cache Friendly Data

The CPU likes nearby data

Modern CPUs are extremely fast, but memory access can still dominate runtime. Cache-friendly layouts keep related values close together so the CPU can load useful chunks efficiently.

For market data, avoid scattered heap objects in the hot path when a compact container will do.

Two common layouts:

Array of Structs (AoS)
[Tick][Tick][Tick]
 each tick has bid, ask, volume

Struct of Arrays (SoA)
[all bids] [all asks] [all volumes]
 good when processing one field across many ticks

Neither is always best. Choose based on access pattern.

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

struct Tick {
    double bid;
    double ask;
    int volume;
};

using Clock = std::chrono::steady_clock;

int main() {
    const int n = 1'000'000;
    std::vector<Tick> ticks;
    ticks.reserve(n);

    for (int i = 0; i < n; ++i) {
        ticks.push_back({100.0 + i * 0.001, 100.1 + i * 0.001, 10 + (i % 20)});
    }

    auto start = Clock::now();
    double spreadSum = 0.0;
    long long volumeSum = 0;

    for (const auto& t : ticks) {
        spreadSum += (t.ask - t.bid);
        volumeSum += t.volume;
    }

    auto end = Clock::now();
    auto us = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();

    std::cout << "Processed " << n << " ticks\n";
    std::cout << "Spread sum: " << spreadSum << "\n";
    std::cout << "Volume sum: " << volumeSum << "\n";
    std::cout << "Loop time: " << us << " us\n";
}
```

Reserve Reduces Spikes

True or false: if you know a vector’s expected size before filling it, calling reserve() can reduce reallocations and help make latency more predictable.

Avoid Hot Allocations

Allocation is not free

Heap allocation inside a per-tick path can create unpredictable latency. Sometimes it is fine; sometimes it causes spikes that are painful in trading systems.

Prefer:

  • pre-sized containers
  • object reuse
  • fixed-size buffers when limits are known
  • moving setup work outside the tick loop

Also be careful with excessive string creation and logging. Printing every tick may be useful while learning, but it can dominate runtime in a live-style loop.

```cpp
#include <array>
#include <chrono>
#include <iostream>
#include <string>
#include <vector>

using Clock = std::chrono::steady_clock;

int main() {
    const int n = 500'000;

    auto startDynamic = Clock::now();
    std::vector<std::string> messages;
    messages.reserve(n);
    for (int i = 0; i < n; ++i) {
        messages.push_back("ORDER:BUY:ABC:" + std::to_string(i));
    }
    auto endDynamic = Clock::now();

    auto startFixed = Clock::now();
    std::array<int, 500'000> orderIds{};
    for (int i = 0; i < n; ++i) {
        orderIds[i] = i;
    }
    auto endFixed = Clock::now();

    auto dynamicUs = std::chrono::duration_cast<std::chrono::microseconds>(endDynamic - startDynamic).count();
    auto fixedUs = std::chrono::duration_cast<std::chrono::microseconds>(endFixed - startFixed).count();

    std::cout << "Dynamic string messages: " << dynamicUs << " us\n";
    std::cout << "Fixed integer buffer:    " << fixedUs << " us\n";
    std::cout << "Different work, but notice the cost of building many strings.\n";
}
```

Fill The Tool

Complete the C++ timing type commonly used for monotonic elapsed-time measurement:

std::chrono::__________

Tuning Workflow

A practical optimization loop

Use a repeatable workflow instead of random changes:

  1. Define the target: average latency, p95/p99 latency, or max acceptable tick processing time.
  2. Measure baseline: capture current behavior before changing code.
  3. Find hot spots: identify the function or loop consuming time.
  4. Change one thing: data layout, allocation pattern, copy reduction, algorithmic complexity.
  5. Re-measure: keep the change only if it helps.
  6. Document tradeoffs: speed, readability, safety, and maintenance.

For compiler builds, compare debug and release modes. Debug builds are excellent for development, but performance numbers should usually come from optimized release builds.

Tuning Workflow

Optimization Checklist

What to inspect in your trading app

Before moving to Backtesting and Strategy Analysis, your strategy engine should be reasonably efficient and measurable.

Use this checklist on the components you built so far:

  • Market data path: Are ticks parsed into compact objects without unnecessary copies?
  • Recent storage: Are rolling windows bounded and pre-sized where possible?
  • Strategy logic: Is each tick handled in predictable time?
  • Signal generation: Are you avoiding repeated duplicate work?
  • API request creation: Are strings and payloads built only when needed?
  • Logging: Is high-frequency logging disabled or buffered in performance mode?
  • Build mode: Are you timing optimized release builds, not only debug builds?

The goal is not premature cleverness. The goal is disciplined C++: measure, simplify, and remove avoidable latency from the hot path.

Optimization Checklist

Open interactive view →