← Torna agli articoli
March 27, 2026
5 min di lettura

ZigBolt: Perché Abbiamo Costruito il Nostro Aeron in Zig e Raggiunto 20 Nanosecondi per Messaggio

#zigbolt
#zig
#hft
#bassa-latenza
#messaggistica
#aeron
#ipc
#open-source

ZigBolt — messaggistica a latenza ultra-bassa Ring buffer lock-free, codec zero-copy, cluster Raft — tutto in Zig puro, tutto open source.

Se lavori nel trading algoritmico o nel market making, conosci il prezzo di ogni microsecondo. Un context switch in più — e il tuo ordine arriva secondo. Una pausa GC della JVM — e il market maker dall'altra parte ha già aggiornato la quotazione. In un mondo dove il denaro si misura in nanosecondi, l'infrastruttura di messaggistica non è una noiosa tubatura tra servizi — è un vantaggio competitivo.

Abbiamo costruito ZigBolt — un sistema di messaggistica per il trading ad alta frequenza scritto in Zig. Da zero. Niente JVM, niente garbage collector, niente Media Driver, niente configurazioni XML. E abbiamo ottenuto 20 nanosecondi di latenza p50 su un ring buffer SPSC e 30 nanosecondi su IPC tramite memoria condivisa.

Questo articolo spiega perché ne avevamo bisogno, come funziona internamente e perché Zig.


TL;DR

  • ZigBolt — sistema di messaggistica open-source (MIT) per HFT in Zig puro
  • 20 ns p50 su SPSC, 30 ns p50 su IPC — più veloce dei numeri pubblicati da Aeron
  • Codec zero-copy funziona a 0 ns (generazione a compile-time, a runtime è solo un cast di puntatore)
  • Niente GC, niente JVM, niente Media Driver — la libreria si integra direttamente nella tua applicazione
  • Cluster Raft, archivio, sequencer — tutto incluso
  • Binding FFI per Rust, Python, Go, TypeScript, C — lavora nel linguaggio che preferisci

Il Problema: Perché Aeron È Ottimo Ma Non Sufficiente

Aeron di Real Logic è lo standard de facto per la messaggistica a bassa latenza nei mercati finanziari. Decine di aziende HFT lo usano, è battle-tested e ha un'architettura eccellente. Ma Aeron ha un problema fondamentale, e si chiama JVM.

JVM Safepoint: Il Nemico Invisibile

Anche se metti con cura tutti i dati nella memoria off-heap, anche se hai disabilitato l'ergonomia del GC e impostato GuaranteedSafepointInterval=300000 — la JVM si ferma comunque occasionalmente in un safepoint. Non è un bug, è una decisione architetturale: la JVM ha bisogno dei safepoint per la deottimizzazione, il biased locking e lo stack walking.

In pratica si presenta così: il tuo thread invia messaggi a p50 = 200 ns, e improvvisamente p99.9 sale a 50 µs. Senza motivo apparente. Perché uno dei thread della JVM ha deciso che era ora.

Media Driver: Un Hop in Più

Aeron funziona attraverso un Media Driver — un processo separato (o JVM embedded) che instrada i messaggi tra publisher e subscriber tramite memoria condivisa. Questo offre un buon isolamento ma aggiunge almeno un hop extra:

Aeron:    App → shm → Media Driver → shm → socket → NIC
ZigBolt:  App → ring buffer → io_uring → NIC

Ogni hop significa nanosecondi in più, cache miss in più, imprevedibilità in più.

SBE: Un Passo di Build Separato

Simple Binary Encoding — il codec FIX standard per i messaggi finanziari. Nell'ecosistema Aeron, è un'utility Java separata che genera codice da schemi XML. Una dipendenza separata, un passo di build separato, un insieme di problemi separato.


La Soluzione: ZigBolt

Ci siamo chiesti: e se prendessimo le migliori idee di Aeron — log triple-buffered, ring buffer lock-free, cluster Raft — e le implementassimo in un linguaggio che:

  1. Non ha overhead di runtime (niente GC, niente safepoint)
  2. Permette la generazione di codice a compile time (comptime)
  3. Si integra banalmente con le librerie C (DPDK, io_uring)
  4. Compila in un binario di ~100 KB

Quel linguaggio è Zig.


Architettura

┌─────────────────────────────────────────────────────────┐
│  Publisher/Subscriber API (typed generic wrappers)       │
├─────────────────────────────────────────────────────────┤
│  Transport Layer (channel factory & lifecycle)            │
├─────────────────────────────────────────────────────────┤
│  IPC Channel (shared memory)  │ UDP Channel (network)    │
├─────────────────────────────────────────────────────────┤
│  WireCodec (comptime, zero-copy) │ SBE Encoder/Decoder   │
├─────────────────────────────────────────────────────────┤
│  Ring Buffers (SPSC/MPSC) │ LogBuffer (triple-buffered)  │
├─────────────────────────────────────────────────────────┤
│  Archive (replay) │ Sequencer (total order) │ Raft (HA)  │
└─────────────────────────────────────────────────────────┘

Sette livelli, ognuno utilizzabile indipendentemente. Hai bisogno solo di un ring buffer SPSC per IPC tra due processi? Prendilo. Hai bisogno di un cluster completo con consenso Raft e archiviazione? C'è anche quello.


Benchmark: Numeri, Non Parole

ZigBolt Benchmark Results

Risultati reali dei benchmark su 10 milioni di iterazioni (Apple Silicon / macOS):

Ring Buffer SPSC

Dimensione Messaggio p50 p99 p99.9 Throughput
8 byte 20 ns 30 ns 120 ns 42,8M msg/s
32 byte 30 ns 50 ns 150 ns 28,5M msg/s
64 byte 50 ns 60 ns 320 ns 17,6M msg/s
256 byte 30 ns 50 ns 50 ns 29,5M msg/s

Canale IPC (memoria condivisa)

Dimensione Messaggio p50 p99 p99.9 Throughput
64 byte 30 ns 40 ns 40 ns 35,7M msg/s
256 byte 40 ns 40 ns 170 ns 27,4M msg/s
1024 byte 90 ns 260 ns 900 ns 9,9M msg/s

LogBuffer (triple-buffered stile Aeron)

Dimensione Messaggio p50 p99 p99.9 Throughput
32 byte 30 ns 40 ns 320 ns 33,6M msg/s
64 byte 30 ns 30 ns 160 ns 38,0M msg/s
256 byte 30 ns 40 ns 60 ns 31,1M msg/s

WireCodec (zero-copy comptime)

Operazione Latenza Throughput
Encode (32 byte) 0 ns memcpy inline
Decode (32 byte) ~0,4 ns 2,7 miliardi msg/s

Sì, hai letto bene: la codifica richiede zero nanosecondi. Perché WireCodec(T) valida la struct a compile time e trasforma encode/decode in un semplice @memcpy o cast di puntatore. Overhead a runtime = zero.

Per confronto: Aeron dichiara un RTT IPC (round-trip) di ~250 ns. La nostra latenza one-way è 30 ns. Anche contando il round-trip, siamo 4 volte più veloci.


Come Funziona Internamente

SPSC Lock-Free: La Semplicità Come Virtù

SPSC Ring Buffer

Un ring buffer single-producer single-consumer è la struttura dati lock-free più semplice e veloce. Lo scrittore sposta head, il lettore sposta tail, nessun CAS necessario — bastano gli atomici acquire/release.

Il trucco chiave è il padding della cache line. Se head e tail si trovano nella stessa cache line, ogni aggiornamento di un contatore invalida la cache per l'altro core (false sharing). La soluzione:

// Head (posizione di scrittura) — sulla propria cache line
head: std.atomic.Value(usize) align(128) = .init(0),

// 128 byte di padding — isolamento garantito
_pad0: [128 - @sizeOf(std.atomic.Value(usize))]u8 = .{0} ** ...,

// Tail (posizione di lettura) — sulla propria cache line
tail: std.atomic.Value(usize) align(128) = .init(0),

128 byte, non 64 — perché su Apple Silicon (e molti chip ARM) il prefetcher hardware può lavorare con coppie di cache line. Andiamo sul sicuro.

WireCodec: Comptime Invece della Generazione di Codice

WireCodec — codec compile-time

Nel mondo Java/C++, i codec binari richiedono un passaggio separato: scrivi uno schema, esegui un generatore di codice, ottieni il codice, compila. In Zig, tutto questo avviene a compile time:

const TickMsg = packed struct {
    symbol_id: u32,
    price: i64,
    quantity: u32,
    side: u8,
    _reserved: [3]u8,
    timestamp: u64,
};

const Codec = WireCodec(TickMsg);

// Encode — solo un memcpy di 32 byte. Si inlinea in 1-2 istruzioni.
Codec.encode(&msg, buf[0..Codec.wire_size]);

// Decode — cast di puntatore. Zero copie.
const tick = Codec.decode(buf[0..Codec.wire_size]);

Il compilatore Zig valida a comptime:

  • La struct è packed (nessun buco di padding)
  • La dimensione è un multiplo di 8 byte (allineamento per SIMD)
  • Tutti i campi sono tipi primitivi

Se qualcosa è sbagliato — errore di compilazione, non un'eccezione a runtime alle 3 di mattina in produzione.

IPC tramite Memoria Condivisa

Due processi mappano lo stesso file in /dev/shm. Il publisher scrive nel ring buffer, il subscriber legge. Niente socket, niente system call sull'hot path:

// Publisher
const channel = try IpcChannel.create("/market-data", .{
    .term_length = 1024 * 1024, // 1 MB
});
channel.publish(&msg_bytes, msg_type_id);

// Subscriber (un altro processo)
const channel = try IpcChannel.open("/market-data", .{
    .term_length = 1024 * 1024,
});
const count = channel.poll(handler_fn, 10);

L'intero percorso da publish() all'invocazione di handler_fn nel subscriber — 30 nanosecondi per un messaggio di 64 byte.

Affidabilità Basata su NAK per UDP

Per il trasporto di rete, ZigBolt usa la ritrasmissione guidata dal ricevitore. Il ricevitore traccia i gap nei numeri di sequenza tramite bitmap e invia NAK (negative acknowledgement) al mittente. Più il controllo della congestione AIMD — slow start stile TCP e congestion avoidance — per evitare di saturare la rete.

Cluster Raft: Quando Hai Bisogno di Consistenza

Per i casi in cui perdere un messaggio è inaccettabile (ad es. un matching engine), ZigBolt include il consenso Raft completo:

  • Elezione del leader con timeout configurabile (150-300 ms)
  • Replicazione del log — il leader replica ogni messaggio ai follower
  • Write-ahead log con validazione CRC32 e crash recovery
  • Snapshot — in modo che il WAL non cresca all'infinito

Archivio: Registra e Riproduci

Tutti i messaggi possono essere registrati in un archivio su disco segmentato. Poi — riproduzione da qualsiasi posizione per tempo o numero di sequenza. Compressione integrata stile LZ4 senza dipendenze esterne. Indice sparso per una ricerca rapida all'interno dei segmenti.

Sequencer a Ordine Totale

Per il market making su più venue, è fondamentale che tutti gli eventi abbiano un ordine globale. Il sequencer prende N stream di input e li fonde in uno solo, assegnando numeri di sequenza monotonicamente crescenti. Ogni partecipante vede la stessa sequenza di eventi.


Perché Zig e Non Rust/C/C++?

Abbiamo scelto tra quattro candidati. Ecco un confronto onesto:

Criterio Zig C/C++ Rust Java (Aeron)
GC / overhead di runtime Nessuno Nessuno Nessuno Safepoint JVM, GC
Generazione di codice comptime Nativa Macro/template proc macro Nessuna
Interop C (DPDK, io_uring) Triviale @cImport Nativo FFI/bindgen Overhead JNI
SIMD @Vector, built-in Intrinsics packed_simd (instabile) Hint di vettorizzazione
Cross-compilazione Built-in CMake inferno cargo target N/A
Tempo di build Secondi Minuti (C++) Minuti Secondi + avvio JVM
Flusso di controllo nascosto Nessuno Eccezioni, cast impliciti Panic in unwrap Eccezioni

Zig ci ha dato una combinazione unica: performance a livello C + sicurezza durante lo sviluppo + metaprogrammazione comptime (codec, lookup table, macchine a stati del protocollo — tutto generato a compile time) + integrazione banale con DPDK, liburing, ef_vi tramite @cImport.

E un binario Zig pesa ~100 KB. Contro i 20+ MB di una soluzione basata su JVM.


Binding: Lavora nel Tuo Linguaggio

ZigBolt compila in una shared library con C-ABI, e abbiamo binding pronti per cinque linguaggi:

TypeScript / Node.js

import { IpcChannel } from "@zigbolt/node";

const channel = IpcChannel.create({
  name: "/my-market-data",
  termLength: 1024 * 1024,
});

const msg = Buffer.from("BTC/USDT 42000.50", "utf-8");
channel.publish(msg, 1);

Rust

use zigbolt::IpcChannel;

let ch = IpcChannel::create("/my-channel", 64 * 1024).unwrap();
ch.publish(b"hello", 1).unwrap();

let sub = IpcChannel::open("/my-channel", 64 * 1024).unwrap();
sub.poll(|data, msg_type_id| {
    println!("got {} bytes, type={}", data.len(), msg_type_id);
}, 10);

Python

from zigbolt import IpcChannel

ch = IpcChannel.create("/market-data", term_length=1024*1024)
ch.publish(b"tick data here", msg_type_id=1)

Più Go e C standard. Lo stesso canale di memoria condivisa è accessibile da tutti i linguaggi simultaneamente — publisher in Zig, subscriber in Python, monitoraggio in Go. Leggono tutti la stessa regione mmap.


Codec SBE: Messaggi Compatibili FIX

Per i protocolli finanziari, ZigBolt include un codec SBE (Simple Binary Encoding) completo con schemi a compile time. Tipi di messaggi built-in:

  • NewOrderSingle — invio ordine
  • ExecutionReport — rapporto di esecuzione
  • MarketDataIncrementalRefresh — aggiornamento incrementale dei dati di mercato
  • MassQuote — quotazione di massa
  • Heartbeat — verifica di connettività
  • Logon — autenticazione

Nessun generatore di codice esterno, nessun XML. Tutto è descritto con struct Zig e validato a compile time.


Protocollo Wire: Compatibilità con Aeron

ZigBolt implementa i flyweight del protocollo wire compatibili con Aeron:

  • DataHeaderFlyweight — frame di dati
  • StatusMessage — controllo di flusso
  • NAK — negative acknowledgement
  • Setup, RTT, Error — frame di servizio

Questo significa che ZigBolt può coesistere con l'infrastruttura Aeron esistente. La migrazione non deve essere un big bang.


Cosa C'è Dopo

ZigBolt è attualmente alla versione 0.2.1. Il core è stabile, i benchmark sono riproducibili, i binding funzionano. In arrivo:

  • Backend io_uring — trasporto di rete zero-copy su Linux 6.0+ (IORING_OP_SEND_ZC)
  • DPDK / AF_XDP — kernel bypass per quando ogni microsecondo conta
  • Multi-Raft — sharding per strumento/strategia
  • Archivio colonnare — integrazione Apache Arrow/Parquet per l'analisi
  • Supporto hugepage — hugepage 2MB/1GB pre-faulted per minimizzare i TLB miss

Provalo

Compila dai sorgenti (zig build), esegui i benchmark (zig build bench), connetti tramite FFI da qualsiasi linguaggio. Se hai Zig 0.15.1 e un paio di minuti — prova il benchmark ping-pong e confronta con la tua soluzione attuale.


Link:


Citazione

@software{soloviov2026zigbolt,
  author = {Soloviov, Eugen},
  title = {ZigBolt: Why We Built Our Own Aeron in Zig and Hit 20 Nanoseconds Per Message},
  year = {2026},
  url = {https://marketmaker.cc/it/blog/post/zigbolt-zig-messaging-hft},
  version = {0.2.1},
  description = {Come e perché abbiamo costruito da zero un sistema di messaggistica a latenza ultra-bassa per HFT in Zig. Niente JVM, niente GC, niente sorprese.}
}
Disclaimer: le informazioni fornite in questo articolo hanno solo scopo didattico e informativo e non costituiscono consulenza finanziaria, di investimento o di trading. Il trading di criptovalute comporta un rischio significativo di perdita.

Autori

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

Resta un Passo Avanti al Mercato

Iscriviti alla nostra newsletter per approfondimenti esclusivi sul trading con IA, analisi di mercato e aggiornamenti sulla piattaforma.

Rispettiamo la tua privacy. Annulla l'iscrizione in qualsiasi momento.