Domain Deep-Dive
ExpertFIX Protocol Parsing in C++
Low-latency FIX protocol parsing in C++: zero-allocation parsers, field lookup tables, FIXT 1.1 session management, and sub-microsecond message handling.
FIX protocol fundamentals
FIX (Financial Information eXchange) is the dominant electronic trading protocol. Messages look like:
cpp
8=FIX.4.4|9=149|35=D|49=CLIENTID|56=BROKER|34=1|52=20240101-12:00:00|
11=ORDER001|21=1|55=AAPL|54=1|60=20240101-12:00:00|38=100|40=2|44=150.00|10=123|Each field is tag=value separated by SOH (ASCII 0x01). The parser must be extremely fast — at a market maker, you receive millions of messages per second.
Zero-allocation parser
cpp
#include <string_view>
#include <array>
#include <cstdint>
// FIX field: tag + value as string_view (pointer into raw buffer)
struct FixField {
uint32_t tag;
std::string_view value;
};
// Pre-allocated field array — no heap allocation during parse
struct FixMessage {
static constexpr int MAX_FIELDS = 64;
std::array<FixField, MAX_FIELDS> fields;
int field_count = 0;
// O(1) lookup via tag-indexed array (tags 1-999)
// For sparse lookups a linear scan is often faster (cache-hot, branch-free)
std::string_view Get(uint32_t tag) const {
for (int i = 0; i < field_count; ++i)
if (fields[i].tag == tag) return fields[i].value;
return {};
}
};
class FixParser {
public:
// Parse in-place: all string_views point into buf
// Returns number of bytes consumed (full message), or 0 if incomplete
int Parse(const char* buf, int len, FixMessage& out) {
out.field_count = 0;
const char* p = buf;
const char* end = buf + len;
while (p < end) {
// Find '='
const char* eq = static_cast<const char*>(
std::memchr(p, '=', end - p));
if (!eq) return 0; // incomplete
// Parse tag (no atoi — manual, faster)
uint32_t tag = 0;
for (const char* t = p; t < eq; ++t)
tag = tag * 10 + (*t - '0');
// Find SOH (0x01)
const char* soh = static_cast<const char*>(
std::memchr(eq + 1, '\x01', end - eq - 1));
if (!soh) return 0; // incomplete
if (out.field_count < FixMessage::MAX_FIELDS) {
out.fields[out.field_count++] = {
tag,
std::string_view(eq + 1, soh - eq - 1)
};
}
// Tag 10 = checksum = last field
if (tag == 10) return static_cast<int>(soh - buf + 1);
p = soh + 1;
}
return 0; // incomplete
}
};Fast tag-to-field lookup with a direct table
For the hottest path (checking a handful of known tags), a direct lookup table beats a loop:
cpp
// Tags of interest
enum FixTag : uint32_t {
BeginString = 8,
MsgType = 35,
Symbol = 55,
Side = 54,
OrderQty = 38,
Price = 44,
OrdType = 40,
ClOrdID = 11,
ExecType = 150,
OrdStatus = 39,
LastPx = 31,
LastQty = 32,
LeavesQty = 151,
CumQty = 14,
};
// Index array: slot[tag] = index into FixMessage::fields (-1 if absent)
// Tag range 1-200 covers 95% of FIX fields
struct FixIndex {
static constexpr int TAG_RANGE = 201;
std::array<int8_t, TAG_RANGE> slot;
void Build(const FixMessage& msg) {
slot.fill(-1);
for (int i = 0; i < msg.field_count; ++i) {
if (msg.fields[i].tag < TAG_RANGE)
slot[msg.fields[i].tag] = static_cast<int8_t>(i);
}
}
std::string_view Get(const FixMessage& msg, uint32_t tag) const {
if (tag >= TAG_RANGE || slot[tag] < 0) return {};
return msg.fields[slot[tag]].value;
}
};String-to-number conversion (no strtod overhead)
cpp
// Parse integer field — much faster than atoi/strtol
inline int64_t ParseInt(std::string_view sv) noexcept {
int64_t result = 0;
bool neg = (!sv.empty() && sv[0] == '-');
for (char c : sv.substr(neg ? 1 : 0))
result = result * 10 + (c - '0');
return neg ? -result : result;
}
// Parse fixed-point price (e.g., "123.45" → 12345 in 1/100 units)
inline int64_t ParsePrice(std::string_view sv, int decimals = 2) noexcept {
int64_t integer = 0, frac = 0;
int frac_digits = 0;
bool in_frac = false;
for (char c : sv) {
if (c == '.') { in_frac = true; continue; }
if (!in_frac) {
integer = integer * 10 + (c - '0');
} else if (frac_digits < decimals) {
frac = frac * 10 + (c - '0');
++frac_digits;
}
}
while (frac_digits++ < decimals) frac *= 10;
return integer * static_cast<int64_t>(std::pow(10, decimals)) + frac;
}FIXT 1.1 session management
The FIX session layer handles logon, heartbeats, sequence numbers, and resend requests:
cpp
class FixSession {
int m_inbound_seq = 1;
int m_outbound_seq = 1;
bool m_logged_on = false;
public:
void OnMessage(const FixMessage& msg) {
auto msg_type = msg.Get(FixTag::MsgType);
// Sequence number check
int msg_seq = static_cast<int>(ParseInt(msg.Get(34)));
if (msg_seq != m_inbound_seq) {
SendResendRequest(m_inbound_seq, msg_seq - 1);
return;
}
++m_inbound_seq;
if (msg_type == "A") { // Logon
m_logged_on = true;
SendLogon();
} else if (msg_type == "0") { // Heartbeat
// Respond if TestReqID present
if (!msg.Get(112).empty()) SendHeartbeat(msg.Get(112));
} else if (msg_type == "1") { // Test Request
SendHeartbeat(msg.Get(112));
} else if (msg_type == "2") { // Resend Request
HandleResend(msg);
} else if (m_logged_on) {
DispatchApplicationMessage(msg, msg_type);
}
}
void SendNewOrder(std::string_view symbol, char side,
int64_t qty, int64_t price_cents) {
// Build FIX message into stack buffer — no heap
char buf[512];
int n = snprintf(buf, sizeof(buf),
"35=D\x01" "11=ORD%d\x01" "55=%.*s\x01"
"54=%c\x01" "38=%lld\x01" "44=%lld.%02lld\x01" "40=2\x01",
m_outbound_seq++,
(int)symbol.size(), symbol.data(),
side, qty / 100, qty % 100,
price_cents / 100, price_cents % 100);
SendRaw(buf, n);
}
};Benchmark: parser throughput
A well-optimized FIX parser on modern hardware:
| Message size | Throughput | Latency |
|---|---|---|
| 150 bytes | ~50M msg/s | ~20 ns |
| 300 bytes | ~25M msg/s | ~40 ns |
| 600 bytes | ~12M msg/s | ~80 ns |
Key optimizations that get you there:
memchris SIMD-accelerated by glibc — use it for SOH search- Manual tag parsing (no
atoi) saves ~5 ns per field - Parse directly into pre-allocated struct — no
std::string __builtin_expecton the happy path- Avoid branch mispredictions: message types are predictable (most are ExecutionReports)
Edit on GitHubUpdated 2026-05-24T00:00:00.000Z