GaiaExGaiaEx
저지연 트레이딩 시스템을 위한 C++
개발자프로그래밍13 min read

저지연 트레이딩 시스템을 위한 C++

가장 빠른 트레이딩 회사들이 모든 것을 C++로 작성하는 이유

게시물 공유

C++가 속도의 언어인 이유

지연 시간에 민감한 스택에서 C++는 여전히 매칭 경로를 지배합니다: Globex 스타일 거래소, 다수의 주식 피드, 크립토 엔진(Hyperliquid급 인프라 포함)은 모두 GC(가비지 컬렉션) 정지가 중간에 숨어 있지 않은, 예측 가능한 기계어 코드로 컴파일됩니다.

무엇이 C++를 저지연 트레이딩에 유독 적합하게 만드는가?

  • 가비지 컬렉터가 없음 — 예측 불가능한 정지가 없습니다. 메모리가 언제 할당되고 해제되는지를 정확히 제어합니다.
  • 하드웨어와의 근접성 — CPU 캐시 라인, SIMD 명령어, 메모리 매핑 I/O, 커널 바이패스 네트워킹에 직접 접근합니다.
  • 컴파일 타임 연산 — 템플릿 메타프로그래밍이 작업을 런타임에서 컴파일 타임으로 옮겨, 손으로 작성한 어셈블리만큼 빠른 코드를 만들어냅니다.
  • 예측 가능한 지연 시간 — 신중한 코딩을 거치면 지터를 최소화한 서브마이크로초 수준의 틱-투-트레이드 지연 시간을 달성할 수 있습니다.

현실 점검: 가장 빠른 데스크의 틱-투-트레이드 예산은 흔히 서브마이크로초입니다. 하나의 어긋난 할당이나 캐시 미스가 전체 예산을 다 먹어버릴 수 있습니다 — 그래서 팀들은 핫 패스를 생산 라인처럼 지킵니다.

Why the hot path stays in native code Deterministic • No GC safepoints on path • Explicit memory & layout • SIMD / cache control • Kernel bypass friendly Measured in ns/µs • p99 > p50 matters • Jitter kills co-location edge • Replayable binaries • Fixed pools / arenas Interop • NIC / FPGA vendors ship C/C++ • DPDK, kernel modules • FIX / binary feeds • Same ABI as OS
거래소 스택은 예측 가능한 지연 시간과 하드웨어와의 밀접한 결합을 중시합니다 — C++는 그 임무에 대한 기본 도구입니다.

메모리 관리: 스택, 힙, 그리고 커스텀 할당자

저지연 C++에서는 무엇을 계산하는지보다 메모리를 어떻게 할당하는지가 더 중요합니다. 스택 할당과 힙 할당의 차이는 지연 시간에서 100배가 될 수 있습니다.

// Stack allocation — near-instant, deterministic
struct OrderUpdate {
    uint64_t order_id;
    double price;
    uint32_t quantity;
    char side;  // 'B' or 'S'
};

void process_tick(const MarketData& tick) {
    OrderUpdate update{};  // Stack — zero allocation cost
    update.price = tick.mid_price();
    // ... process on the hot path
}

// Heap allocation — slow, non-deterministic (avoid on hot path)
auto* order = new OrderUpdate{};  // Calls malloc — BAD on hot path

프로덕션 시스템은 핫 패스가 절대 범용 힙을 호출하지 않도록 커스텀 할당자에 의존합니다:

  • 풀 할당자(Pool allocators) — 큰 블록을 미리 할당하고 고정 크기 조각을 잘라냅니다. 파편화가 없고 O(1) 할당입니다.
  • 아레나 할당자(Arena allocators) — 각 할당마다 포인터를 앞으로 밀고, 한꺼번에 전부 해제합니다. 메시지 단위 처리에 완벽합니다.
  • 휴지 페이지(Huge pages) — 2MB 또는 1GB 페이지는 TLB 미스를 줄여주며, 오더북 데이터가 메가바이트 단위로 걸쳐 있을 때 중요합니다.

현대 C++는 RAII(Resource Acquisition Is Initialization)와 스마트 포인터로 안전한 메모리 관리를 인체공학적으로 만듭니다:

// RAII — resource lifetime tied to scope
{
    auto connection = std::make_unique<TcpConnection>(endpoint);
    connection->send(order_message);
}  // Connection automatically closed here

// Shared ownership for reference-counted resources
auto config = std::make_shared<TradingConfig>(load_config());
engine.set_config(config);  // Multiple owners, automatic cleanup
Stack vs heap on the hot path Stack / thread-local OrderUpdate on stack — bounded, LIFO, cache-hot Pool / arena — O(1) reuse, no malloc churn Heap (generic) new/malloc — allocator locks, fragmentation Unpredictable latency — avoid per tick
구조체는 스택이나 미리 워밍업된 풀에 두세요; 틱 핸들러에서 힙 접근이 발생하면 버그로 취급하세요.

락 프리 데이터 구조와 동시성

뮤텍스는 저지연 코드의 적입니다. 단 한 번의 std::mutex::lock() 호출은 경합이 없어도 20~100나노초가 걸릴 수 있으며 — 경합이 있으면 스레드를 마이크로초 단위로 정지시킬 수 있습니다. 트레이딩 시스템은 대신 락 프리(lock-free) 데이터 구조를 사용합니다.

트레이딩에서 가장 중요한 락 프리 구조는 단일 생산자 단일 소비자(SPSC) 큐입니다:

template<typename T, size_t Capacity>
class SPSCQueue {
    alignas(64) std::array<T, Capacity> buffer_;
    alignas(64) std::atomic<size_t> head_{0};
    alignas(64) std::atomic<size_t> tail_{0};

public:
    bool try_push(const T& item) {
        const auto tail = tail_.load(std::memory_order_relaxed);
        const auto next = (tail + 1) % Capacity;
        if (next == head_.load(std::memory_order_acquire))
            return false;  // Full
        buffer_[tail] = item;
        tail_.store(next, std::memory_order_release);
        return true;
    }

    bool try_pop(T& item) {
        const auto head = head_.load(std::memory_order_relaxed);
        if (head == tail_.load(std::memory_order_acquire))
            return false;  // Empty
        item = buffer_[head];
        head_.store((head + 1) % Capacity, std::memory_order_release);
        return true;
    }
};

핵심 설계 원칙:

  • alignas(64) — 각 원자 변수가 자신만의 캐시 라인을 가져, CPU 코어 간의 거짓 공유(false sharing)를 방지합니다.
  • 메모리 순서(Memory ordering)acquire/release 세맨틱은 seq_cst보다 저렴하며 생산자-소비자 패턴에 충분합니다.
  • 2의 거듭제곱 크기 — 프로덕션에서는 1024나 4096 같은 크기를 사용해 모듈로 연산을 비트단위 AND로 만듭니다.

일반적인 배선 방식: NIC 스레드가 인큐하고, 전략 스레드가 디큐합니다 — 토폴로지를 제대로 잡으면 전송 경로에 뮤텍스가 필요 없습니다.

SPSC ring buffer (one writer, one reader) Producer feed / I/O thread Power-of-two slots • acquire/release atomics Consumer strategy thread alignas(64) head/tail — separate cache lines to kill false sharing Memory order: relaxed on local index, acquire/release across handoff
생산자 하나와 소비자 하나만 있으면 뮤텍스를 건너뛸 수 있습니다; 올바른 정렬은 코어들이 같은 캐시 라인을 두고 싸우지 않게 해줍니다.

템플릿: 컴파일 타임 연산

C++ 템플릿은 작업을 런타임에서 컴파일 타임으로 옮길 수 있게 해줍니다. 트레이딩에서 이는 여러분의 바이너리가 거래하는 정확한 프로토콜, 상품, 전략에 특화된다는 뜻입니다 — 핫 패스에는 런타임 분기가 없습니다.

// Compile-time FIX protocol field parsing
template<int Tag>
struct FIXField;

template<> struct FIXField<35> {  // MsgType
    static constexpr const char* name = "MsgType";
    using type = char;
};

template<> struct FIXField<44> {  // Price
    static constexpr const char* name = "Price";
    using type = double;
};

// Zero-overhead dispatch based on message type
template<char MsgType>
void handle_message(const char* raw, size_t len) {
    if constexpr (MsgType == 'D') {
        // New Order Single — inline at compile time
        parse_new_order(raw, len);
    } else if constexpr (MsgType == '8') {
        // Execution Report
        parse_execution(raw, len);
    }
}

if constexpr 분기는 완전히 컴파일 타임에 해결됩니다 — 생성된 기계어 코드는 관련된 경로만 포함하며 분기 오버헤드가 전혀 없습니다. 이 기법은 링크 타임 최적화(LTO)와 결합되어, 프로토콜 파싱이 본질적으로 순차적인 메모리 읽기 나열로 풀려 있는 바이너리를 만들어냅니다.

현대 C++20/23의 consteval, 컨셉(concepts), 컴파일 타임 컨테이너 같은 기능은 이를 한층 더 밀어붙여, 전체 주문 검증 파이프라인을 컴파일 타임에 계산하는 것까지 가능하게 합니다.

캐시 친화적 설계와 커널 바이패스 네트워킹

서브마이크로초 지연 시간에서는 CPU 캐시 계층이 가장 중요한 최적화 대상이 됩니다. 메인 메모리로의 캐시 미스는 약 100나노초의 비용을 치릅니다 — 전체 지연 시간 예산이 500ns라면 이는 영원과 같은 시간입니다.

// BAD: Array of Pointers (AoP) — cache-hostile
std::vector<std::unique_ptr<Order>> orders;  // Each access = pointer chase + cache miss

// GOOD: Struct of Arrays (SoA) — cache-friendly
struct OrderBook {
    std::vector<double> prices;      // Contiguous in memory
    std::vector<uint32_t> quantities; // Contiguous in memory
    std::vector<uint64_t> order_ids;  // Contiguous in memory
};
// Iterating prices = sequential cache line reads = fast

네트워킹에서는 Linux 커널의 TCP/IP 스택이 패킷당 5~15마이크로초의 지연 시간을 추가합니다. 트레이딩 회사들은 이를 완전히 우회합니다:

  • DPDK(Data Plane Development Kit) — 커널을 우회해 사용자 공간에서 NIC를 직접 폴링합니다. 서브마이크로초 패킷 처리를 달성합니다.
  • Solarflare OpenOnload — 익숙한 소켓 API를 유지하는 커널 바이패스입니다. 주식과 선물 거래에서 광범위하게 사용됩니다.
  • FPGA NIC — Xilinx Alveo 같은 카드는 하드웨어에서 시장 데이터를 파싱하고 주문을 생성할 수 있어 나노초 수준의 지연 시간을 달성합니다.

탈중앙화 거래소조차 이런 원칙의 혜택을 봅니다. GaiaEx 같은 플랫폼을 구동하는 Hyperliquid의 L1 체인은 고처리량, 저지연 합의를 염두에 두고 설계되었으며, 이에 연결하는 마켓 메이커들은 가격 업데이트를 받은 시점과 주문을 제출한 시점 사이의 시간을 최소화하기 위해 최적화된 C++ 클라이언트를 사용합니다.

거래소 매칭 엔진은 어떻게 만들어지는가

모든 거래소의 심장에는 매칭 엔진이 있습니다 — 매수와 매도 주문을 짝지어주는 구성요소입니다. 이는 모든 금융 소프트웨어 중 가장 지연 시간에 민감한 부분이며, 거의 항상 C++로 작성됩니다.

단순화한 매칭 엔진 아키텍처:

class MatchingEngine {
    // One order book per instrument
    std::unordered_map<Symbol, OrderBook> books_;

    void on_new_order(const Order& order) {
        auto& book = books_[order.symbol];
        auto matches = book.match(order);

        for (const auto& fill : matches) {
            publish_execution(fill);      // To trading firms
            update_market_data(fill);     // To data feed
        }

        if (order.remaining_qty > 0) {
            book.insert(order);           // Rest on the book
        }
    }
};

프로덕션 매칭 엔진은 이 골격을 훨씬 뛰어넘는 최적화를 합니다:

  • 가격-시간 우선순위 — 같은 가격의 주문은 도착 순서대로 체결되며, 나노초 단위 타임스탬프로 추적됩니다.
  • 사전 할당된 주문 풀 — 매칭 중 힙 할당이 없습니다. 주문은 고정된 풀에서 재활용됩니다.
  • 락 없는 설계 — 각 상품의 오더북은 전용 코어에서 실행됩니다. 오더북 간 락이 필요 없습니다.
  • 결정적 재생(Deterministic replay) — 모든 주문과 매칭은 규제 준수와 재해 복구를 위해 영구 저장소에 저널링됩니다.

이 수준의 시스템을 구축하는 것이 C++가 진정으로 빛나는 지점입니다. 다른 어떤 주류 언어도 메모리 레이아웃, 스레드 스케줄링, 캐시 동작, 네트워크 I/O를 동시에 통제할 수 있는 능력을 주지 않습니다 — 이 네 가지가 초저지연 엔지니어링의 네 기둥입니다. 다음 거래소를 만들든, 마켓 메이커로 거래소에 연결하든, 프랍 트레이딩 회사에서 실행을 최적화하든, C++는 여전히 의심할 여지 없는 왕입니다.