ZigBolt:なぜ我々はZigで独自のAeronを構築し、メッセージあたり20ナノ秒を達成したのか
ロックフリーリングバッファ、ゼロコピーコーデック、Raftクラスタ——すべて純粋なZigで、すべてオープンソース。
アルゴリズムトレーディングやマーケットメイキングに携わっているなら、1マイクロ秒の価値をご存知でしょう。たった1回の余分なコンテキストスイッチで、あなたの注文は2番目に到着します。JVMのガベージコレクションが一度停止するだけで、反対側のマーケットメイカーはすでにクォートを更新しています。ナノ秒単位で利益が決まる世界では、メッセージングインフラはサービス間の退屈なパイプラインではなく、競争優位そのものです。
我々はZigBoltを構築しました——Zig言語による高頻度取引向けメッセージングシステムです。ゼロから。JVMなし、ガベージコレクタなし、Media Driverなし、XML設定ファイルなし。そして、SPSCリングバッファでp50レイテンシ20ナノ秒、共有メモリIPCで30ナノ秒を達成しました。
本記事では、なぜこれが必要だったのか、内部の仕組み、そしてなぜZigを選んだのかについて解説します。
概要
- ZigBolt — 純粋なZigで書かれたHFT向けオープンソース(MIT)メッセージングシステム
- SPSC p50: 20ns、IPC p50: 30ns — Aeronの公称スペックを上回る
- ゼロコピーコーデックは0nsで動作(コンパイル時コード生成、ランタイムは単なるポインタキャスト)
- GCなし、JVMなし、Media Driverなし — ライブラリはアプリケーションに直接組み込み
- Raftクラスタ、アーカイブ、シーケンサ — すべて同梱
- FFIバインディング:Rust、Python、Go、TypeScript、C対応 — お好みの言語で
課題:Aeronは優れているが、十分ではない
Real LogicのAeronは、キャピタルマーケットにおける低レイテンシメッセージングのデファクトスタンダードです。数十のHFT企業が利用し、実戦で検証済みで、優れたアーキテクチャを持っています。しかしAeronには根本的な問題があります——JVMです。
JVM safepoints:見えない敵
すべてのデータを慎重にオフヒープメモリに配置し、GCエルゴノミクスを無効にしてGuaranteedSafepointInterval=300000を設定しても、JVMは依然として時折すべてのスレッドをsafepointで停止させます。これはバグではなく、アーキテクチャ上の設計です。JVMはデオプティマイゼーション、バイアスドロッキング、スタックウォーキングのためにsafepointsを必要とします。
実際にはこうなります:スレッドがp50 = 200nsでメッセージを送信しているのに、突然p99.9が50μsに跳ね上がる。明確な原因なしに。JVMのいずれかのスレッドが、safepointの時間だと判断したからです。
Media Driver:余分なホップ
Aeronは Media Driver——パブリッシャーとサブスクライバー間で共有メモリを介してメッセージをルーティングする独立プロセス(または組み込みJVM)——を通じて動作します。これにより美しいアイソレーションが実現されますが、最低1回の余分なホップが追加されます:
Aeron: App → shm → Media Driver → shm → socket → NIC
ZigBolt: App → ring buffer → io_uring → NIC
各ホップごとに、余分なナノ秒、余分なキャッシュミス、余分な不確定性が加わります。
SBE:別途のビルドステップ
Simple Binary Encodingは、金融メッセージの標準的なFIXコーデックです。Aeronエコシステムでは、XMLスキーマからコードを生成する独立したJavaユーティリティです。別途の依存関係、別途のビルドステップ、別途の問題群。
ソリューション:ZigBolt
我々は自問しました:Aeronの最良のアイデア——トリプルバッファログ、ロックフリーリングバッファ、Raftクラスタ——を、以下の特性を持つ言語で実装したらどうなるか:
- ランタイムオーバーヘッドなし(GCなし、safepointsなし)
- コンパイル時にコード生成が可能(comptime)
- Cライブラリ(DPDK、io_uring)との統合が容易
- バイナリサイズ約100KB
その言語とは——Zigです。
アーキテクチャ
┌─────────────────────────────────────────────────────────┐
│ Publisher/Subscriber API(型付きラッパー) │
├─────────────────────────────────────────────────────────┤
│ Transport Layer(チャネルファクトリ、ライフサイクル) │
├─────────────────────────────────────────────────────────┤
│ IPC Channel(共有メモリ) │ UDP Channel(ネットワーク) │
├─────────────────────────────────────────────────────────┤
│ WireCodec(comptime ゼロコピー)│ SBE Encoder/Decoder │
├─────────────────────────────────────────────────────────┤
│ Ring Buffers (SPSC/MPSC) │ LogBuffer(トリプルバッファ) │
├─────────────────────────────────────────────────────────┤
│ Archive(リプレイ)│ Sequencer(全順序)│ Raft(HA) │
└─────────────────────────────────────────────────────────┘
7層構造で、各層を独立して使用可能です。2つのプロセス間のIPCにSPSCリングバッファだけが必要?そのまま使えます。Raftコンセンサスとアーカイブを備えたフルクラスタが必要?それもあります。
ベンチマーク:言葉ではなく数字で

以下は1000万回のイテレーション(Apple Silicon / macOS)における実測結果です:
SPSCリングバッファ
| メッセージサイズ | p50 | p99 | p99.9 | スループット |
|---|---|---|---|---|
| 8バイト | 20ns | 30ns | 120ns | 42.8M msg/s |
| 32バイト | 30ns | 50ns | 150ns | 28.5M msg/s |
| 64バイト | 50ns | 60ns | 320ns | 17.6M msg/s |
| 256バイト | 30ns | 50ns | 50ns | 29.5M msg/s |
IPC Channel(共有メモリ)
| メッセージサイズ | p50 | p99 | p99.9 | スループット |
|---|---|---|---|---|
| 64バイト | 30ns | 40ns | 40ns | 35.7M msg/s |
| 256バイト | 40ns | 40ns | 170ns | 27.4M msg/s |
| 1024バイト | 90ns | 260ns | 900ns | 9.9M msg/s |
LogBuffer(Aeron方式トリプルバッファ)
| メッセージサイズ | p50 | p99 | p99.9 | スループット |
|---|---|---|---|---|
| 32バイト | 30ns | 40ns | 320ns | 33.6M msg/s |
| 64バイト | 30ns | 30ns | 160ns | 38.0M msg/s |
| 256バイト | 30ns | 40ns | 60ns | 31.1M msg/s |
WireCodec(comptimeゼロコピー)
| 操作 | レイテンシ | スループット |
|---|---|---|
| エンコード(32バイト) | 0ns | ∞(インラインmemcpy) |
| デコード(32バイト) | ~0.4ns | 27億 msg/s |
はい、読み間違いではありません:エンコードはゼロナノ秒です。なぜならWireCodec(T)はコンパイル時に構造体を検証し、encode/decodeを通常の@memcpyまたはポインタキャストに変換するからです。ランタイムオーバーヘッド = ゼロ。
比較のために:AeronはIPC RTT(ラウンドトリップ)約250nsと公表しています。我々のワンウェイレイテンシは30ns。ラウンドトリップで計算しても、4倍高速です。
内部の仕組み
ロックフリーSPSC:シンプルさという美徳

単一プロデューサー・単一コンシューマーのリングバッファは、最もシンプルかつ最速のロックフリーデータ構造です。ライターがheadを進め、リーダーがtailを進めます。CASは不要——acquire/releaseアトミックで十分です。
重要なトリックはキャッシュラインパディングです。headとtailが同一キャッシュラインに存在すると、一方のカウンタの更新がもう一方のコアのキャッシュを無効化します(フォールスシェアリング)。解決策:
// Head(書き込み位置)——独自のキャッシュライン
head: std.atomic.Value(usize) align(128) = .init(0),
// 128バイトパディング——アイソレーションを保証
_pad0: [128 - @sizeOf(std.atomic.Value(usize))]u8 = .{0} ** ...,
// Tail(読み取り位置)——独自のキャッシュライン
tail: std.atomic.Value(usize) align(128) = .init(0),
64バイトではなく128バイト——Apple Silicon(および多くのARM)では、ハードウェアプリフェッチャがキャッシュラインのペアで動作する可能性があるためです。安全策を取っています。
WireCodec:コード生成の代わりにcomptime

Java/C++の世界では、バイナリコーデックには別途のステップが必要です:スキーマ作成 → コードジェネレータ実行 → コード取得 → コンパイル。Zigではこれらすべてがコンパイル時に完了します:
const TickMsg = packed struct {
symbol_id: u32,
price: i64,
quantity: u32,
side: u8,
_reserved: [3]u8,
timestamp: u64,
};
const Codec = WireCodec(TickMsg);
// エンコード——32バイトのmemcpyに過ぎない。1-2命令にインライン化。
Codec.encode(&msg, buf[0..Codec.wire_size]);
// デコード——ポインタキャスト。コピーなし。
const tick = Codec.decode(buf[0..Codec.wire_size]);
Zigコンパイラはcomptimeフェーズで以下を検証:
- 構造体がpackedである(パディングホールなし)
- サイズが8バイトの倍数(SIMD向けアライメント)
- すべてのフィールドがプリミティブ型
問題があれば——コンパイルエラーになります。深夜3時の本番環境でのランタイム例外ではありません。
共有メモリによるIPC
2つのプロセスが/dev/shm内の同一ファイルをマップします。パブリッシャーがリングバッファに書き込み、サブスクライバーが読み取ります。ホットパス上にソケットもシステムコールもありません:
// Publisher
const channel = try IpcChannel.create("/market-data", .{
.term_length = 1024 * 1024, // 1 MB
});
channel.publish(&msg_bytes, msg_type_id);
// Subscriber(別プロセス)
const channel = try IpcChannel.open("/market-data", .{
.term_length = 1024 * 1024,
});
const count = channel.poll(handler_fn, 10);
publish()からサブスクライバーのhandler_fn呼び出しまでの全経路——64バイトメッセージで30ナノ秒。
NAKベースのUDP信頼性
ネットワークトランスポートにおいて、ZigBoltはレシーバー駆動の再送メカニズムを使用します。レシーバーがシーケンス番号のギャップをビットマップで追跡し、送信者にNAK(否定確認応答)を送信します。さらにAIMD輻輳制御——TCPライクなスロースタートと輻輳回避——によりネットワークの過負荷を防ぎます。
Raftクラスタ:整合性が必要な場合
メッセージの損失が許されないケース(例:マッチングエンジン)のために、ZigBoltは完全なRaftコンセンサスを搭載しています:
- リーダー選出:設定可能なタイムアウト(150-300ms)
- ログレプリケーション ——リーダーが各メッセージをフォロワーに複製
- WAL(先行書き込みログ):CRC32検証とクラッシュリカバリ対応
- スナップショット ——WALの無限成長を防止
アーカイブ:記録とリプレイ
すべてのメッセージをディスク上のセグメント化されたアーカイブに記録できます。その後、時間またはシーケンス番号で任意の位置からリプレイ可能。外部依存なしの組み込みLZ4スタイル圧縮。セグメント内の高速検索用スパースインデックス。
トータルオーダーシーケンサ
複数の取引所でのマーケットメイキングでは、すべてのイベントにグローバルな順序が必要です。シーケンサはN個の入力ストリームを受け取り、単調増加するシーケンス番号を割り当てて1つに統合します。すべての参加者が同一のイベントシーケンスを見ることになります。
なぜRust/C/C++ではなくZigなのか?
4つの候補言語で比較しました。以下が公正な比較表です:
| 基準 | Zig | C/C++ | Rust | Java (Aeron) |
|---|---|---|---|---|
| GC / ランタイムオーバーヘッド | なし | なし | なし | JVM safepoints, GC |
| Comptimeコード生成 | ネイティブ | マクロ/テンプレート | proc macros | なし |
| C相互運用(DPDK, io_uring) | 簡単な@cImport |
ネイティブ | FFI/bindgen | JNIオーバーヘッド |
| SIMD | @Vector、組み込み |
Intrinsics | packed_simd(不安定) | ベクトル化ヒント |
| クロスコンパイル | 組み込み | CMake地獄 | cargo target | N/A |
| ビルド時間 | 秒単位 | 分単位(C++) | 分単位 | 秒単位 + JVM起動 |
| 隠れた制御フロー | なし | 例外、暗黙の型変換 | unwrap内のパニック |
例外 |
Zigはユニークな組み合わせを提供しました: Cのパフォーマンス + 開発時の安全性 + comptimeメタプログラミング(コーデック、ルックアップテーブル、プロトコルステートマシン——すべてコンパイル時に生成)+ @cImportによるDPDK、liburing、ef_viとのシームレスな統合。
さらにZigバイナリは約100KB。JVMベースのソリューションの20MB以上と比較してください。エッジデプロイやコンテナにとって、これは重要です。
バインディング:お好みの言語で
ZigBoltはC-ABIの共有ライブラリにコンパイルされ、5つの言語の既製バインディングを提供しています:
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();
// Subscriber
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)
GoおよびピュアCバインディングもあります。同一の共有メモリチャネルに、すべての言語から同時にアクセス可能です——パブリッシャーはZig、サブスクライバーはPython、モニタリングはGo。全員が同じmmap領域を読み取ります。
SBEコーデック:FIX互換メッセージ
金融プロトコル向けに、ZigBoltはコンパイル時スキーマによる完全なSBE(Simple Binary Encoding)コーデックを搭載しています。組み込みメッセージタイプ:
- NewOrderSingle — 注文発注
- ExecutionReport — 約定報告
- MarketDataIncrementalRefresh — インクリメンタルマーケットデータ更新
- MassQuote — 一括クォート
- Heartbeat — 接続確認
- Logon — 認証
外部コードジェネレータ不要、XML不要。すべてZig構造体で記述し、コンパイル時に検証されます。
Wire Protocol:Aeronとの互換性
ZigBoltはAeron互換のwire protocol flyweightsを実装しています:
- DataHeaderFlyweight — データフレーム
- StatusMessage — フロー制御
- NAK — 否定確認応答
- Setup、RTT、Error — 制御フレーム
これは、ZigBoltが既存のAeronインフラストラクチャと共存できることを意味します。移行は一度にすべてを切り替える必要はありません。
今後の展望
ZigBoltは現在バージョン0.2.1です。コアは安定し、ベンチマークは再現可能、バインディングは動作しています。近い将来の計画:
- io_uringバックエンド — Linux 6.0+でのゼロコピーネットワークトランスポート(IORING_OP_SEND_ZC)
- DPDK / AF_XDP — 1マイクロ秒が勝負を分けるケースのカーネルバイパス
- Multi-Raft — 銘柄/戦略別シャーディング
- カラムナーアーカイブ — 分析のためのApache Arrow/Parquet統合
- Hugepageサポート — TLBミスを最小化するためのプリフォールト2MB/1GBヒュージページ
試してみる
- サイト:zigbolt-landing.vercel.app
- ドキュメント:zigbolt-landing.vercel.app/getting-started/introduction/
- ソースコード:github.com/suenot/zigbolt
- ライセンス:MIT
ソースからビルド(zig build)、ベンチマーク実行(zig build bench)、FFIで任意の言語から接続。Zig 0.15.1と数分の時間があれば——ping-pongベンチマークを試して、現在のソリューションと比較してみてください。
リンク:
- ZigBoltサイト:zigbolt-landing.vercel.app
- GitHub:github.com/suenot/zigbolt
- Aeron(比較用):github.com/real-logic/aeron | Aeronの概要記事
- Zig言語:ziglang.org
- Marketmaker.cc:marketmaker.cc
引用
@software{soloviov2026zigbolt,
author = {Soloviov, Eugen},
title = {ZigBolt: なぜ我々はZigで独自のAeronを構築し、メッセージあたり20ナノ秒を達成したのか},
year = {2026},
url = {https://marketmaker.cc/ja/blog/post/zigbolt-zig-messaging-hft},
version = {0.2.1},
description = {HFT向け超低レイテンシメッセージングシステムをZig言語でゼロから構築した経緯と理由。JVMなし、GCなし、サプライズなし。}
}
MarketMaker.cc Team
クオンツ・リサーチ&戦略