Cukai IPC: Letakkan Enjin Backtest di Sebalik Soket dan Rugi 13% — Hampir Tiada Kaitan dengan Soket Itu
Sebahagian daripada siri "Backtest Tanpa Ilusi".
📄 Artikel ini berkembang menjadi kertas kajian. Satu kernel backtest yang bergantung pada laluan dialihkan (port) baris demi baris daripada numba ke Rust dan dipanggil merentasi sempadan proses/bahasa dalam empat cara, dengan get kesetaraan yang mengesahkan PnL per-kombo yang identik — ditambah pengukuran terasing bagi keluk latensi IPC tulen, cukai serialisasi, dan kos spawn. Baca kertas kajian dalam talian (versi interaktif + PDF) di ipc-tax.marketmaker.cc, kod dan data di github.com/suenot/ipc-tax.
Setiap enjin backtest yang menjadi pantas akhirnya mencetuskan perbualan yang sama. Enjin kami tiba pada jadualnya. Tangga kelajuan baru sahaja membawa sapuan parameter 80-kombo daripada 69.9 saat pandas turun kepada kira-kira 2 saat numba berbenang tunggal, dan gatal semula jadi yang seterusnya ialah: mengapa berhenti pada JIT Python? Tulis semula kernel dalam Rust. Jadikan ia perkhidmatan enjin yang betul — satu binari yang disusun di sebalik soket, boleh dipanggil daripada setiap skrip penyelidikan, setiap bahasa, dan peniaga langsung juga. Satu kernel, satu kebenaran, tiada logik pendua.
Dan kemudian hujah balas tiba, juga pada jadualnya: saat anda meninggalkan proses, IPC memakan anda. Data mesti disirikan (serialized), dihantar merentasi sempadan, dinyahsirikan; setiap panggilan membayar syscall dan pertukaran konteks; kernel Rust anda yang indah akan menghabiskan hidupnya menunggu pada paip. Kekal dalam-proses. Semua orang tahu ini.
Artikel ini mengukur perkara yang semua orang tahu, dan pengukuran itu lebih menarik daripada mana-mana pihak dalam hujah tersebut. Kepercayaan rakyat — "enjin merentas-bahasa yang lebih pantas kalah kepada numba dalam-proses kerana IPC membunuh anda" — ternyata salah secara umum dan betul hanya di bawah syarat tertentu. Merentasi sempadan sekali, dalam bait mentah, berkos kira-kira 2 milisaat pada kerja dua saat: ralat pembundaran. Cukai itu bukan terletak pada sempadan. Ia terletak pada cara anda merentasinya — dan tiga cara perkhidmatan enjin biasanya digunakan di alam liar (API JSON, panggilan setiap unit kerja, spawn proses setiap panggilan) masing-masing, secara terukur, adalah sekeping daripada bencana yang diramalkan oleh cerita rakyat.
Berikut ialah keseluruhan eksperimen di hadapan. Segala-galanya di bawah adalah anatomi setiap baris.
| Seni Bina | Apa yang merentasi sempadan setiap sapuan | Masa dinding | berbanding dalam-proses |
|---|---|---|---|
| numba dalam-proses | tiada apa-apa — panggilan langsung | 2.010 s | 1.00x |
| Pelayan Rust, batched (soket Unix) | satu round-trip: keseluruhan siri + kesemua 80 set parameter | 2.276 s | 1.13x |
Pelayan Rust, batched, kernel get_unchecked |
round-trip tunggal yang sama — varian kernel bebas semakan-sempadan (lihat keputusan muktamad) | 2.337 s | 1.16x |
| Pelayan Rust, chatty (soket Unix) | 80 round-trip: siri dihantar semula setiap kombo | 2.383 s | 1.19x |
| Rust spawn (stdin/stdout) | spawn proses + satu permintaan dipaipkan | 2.300 s | 1.14x |
Apple M2 Max, Python 3.14.6, numpy 2.4.3, numba 0.64.0, rustc 1.94.0 (build release, sifar crate luaran). 150,000 bar × 80 kombo, yuran round-trip 0.09%, seed 42; siri close adalah 1,200,000 bait (1.2 MB) di atas wayar. Median 10 larian setiap seni bina; sebaran min–max kekal dalam ~2%. Kesemua lima menjalankan sapuan stop-and-reverse HMA/HMA3 yang sama, dan get kesetaraan mengesahkan bahawa keputusan per-kombo (PnL, bilangan dagangan) kedua-dua varian kernel Rust sepadan tepat dengan numba — cap jari PnL −5165.58 merentasi 57,029 dagangan, identik bait-demi-bait dengan kernel numba kajian speed-ladder pada seed yang sama. Kami membandingkan sempadan, bukan pelaksanaan.
Baca baris batched dengan teliti, kerana ia membawa keseluruhan tesis. Seni bina Rust-atas-soket adalah 1.13x lebih perlahan daripada numba dalam-proses — 266 ms ketinggalan pada sapuan penuh (terbitan: 2.276 − 2.010). Cerita rakyat berkata milisaat tersebut adalah IPC. Ia tidak. Kira-kira 2 ms daripada jurang itu adalah sempadan — keseluruhan siri close 1.2 MB dihantar masuk, keputusan dihantar balik, diukur secara langsung. ~264 ms yang selebihnya ialah kernel Rust naif kami hanya mengira sapuan itu kira-kira 13% lebih perlahan daripada kernel numba (terbitan: 2.276 s tolak ~2 ms sempadan ≈ 2.274 s pengiraan Rust, berbanding 2.010 s untuk numba). Rust-bahasa itu tidak kalah kepada Python-bahasa itu; satu gelung skalar yang disusun-LLVM kalah dalam perlumbaan codegen kepada yang lain — dan kami tidak dapat mengaitkan kekalahan itu pada suspek yang jelas: binaan get_unchecked bebas semakan-sempadan bagi kernel yang sama ternyata tidak lebih pantas (2.337 s; bahagian keputusan muktamad membedah ini). Soket hampir tiada kaitan dengan mana-mana daripadanya.
Pegang kedua-dua separuh ayat itu. Sempadan itu hampir percuma apabila direntasi dengan betul — dan "tulis semula dalam Rust" membeli anda sempadan penggunaan (deployment), bukan kemenangan pengiraan automatik. Kedua-dua fakta ini bertentangan dengan naluri popular, dan kedua-duanya ada dalam jadual.
Satu kernel, dua bahasa, empat sempadan
Beban kerja itu sengaja sama seperti yang dipastikan oleh tangga kelajuan, supaya kedua-dua kajian berlabuh antara satu sama lain. Kernel itu adalah silang HMA/HMA3 — sistem stop-and-reverse pada dua purata bergerak gaya Hull, tujuh laluan purata-bergerak-berpemberat setiap kombinasi parameter ditambah gelung peristiwa bar-demi-bar yang berstatus yang membawa kedudukan, mencatat PnL tolak yuran round-trip 0.09% pada setiap silang, dan berbalik. Data adalah 150,000 bar gerakan Brown geometri sintetik yang disemai (seed=42); grid adalah 80 panjang HMA yang tersebar merentasi . Rujukan dalam-proses adalah anak tangga numba berbenang-tunggal daripada tangga kelajuan, diukur semula untuk kajian ini: 1.98 s di sana, 2.010 s di sini — kernel yang sama, mesin yang sama, membosankan secara meyakinkan.
Enjin merentas-bahasa itu adalah port baris-demi-baris kernel numba tersebut ke Rust — gelung yang sama, pengendalian NaN yang sama, aritmetik yuran yang sama — disusun dalam mod release tanpa crate luaran, supaya keseluruhan eksperimen kekal bebas-kebergantungan dan boleh dihasilkan semula. Ia bertutur protokol binari yang sengaja minimum: satu bingkai berawalan-panjang setiap arah, semuanya little-endian.
request: [u32 body_len][body]
body: [u8 opcode][u32 n_bars][u32 n_combos]
[n_bars × f64 close][n_combos × 6 × i64 params]
opcode 0 = sweep : reply = [n_combos × f64 pnl][n_combos × i64 trades]
opcode 1 = echo : reply = the close array, verbatim
Opcode echo adalah pisau bedah kajian ini: satu round-trip bersaiz terkawal yang tidak mengira apa-apa, supaya kos sempadan tulen boleh diukur secara terasing — serialisasi, syscall, transit soket, deserialisasi, dan tiada apa-apa lagi.
Lima seni bina terukur — empat corak sempadan ditambah satu varian kernel:
- in_process — panggil kernel numba secara langsung. Tiada sempadan. Rujukan.
- rust_batch_unix — pelayan Rust berterusan pada soket domain Unix. Satu round-trip menghantar keseluruhan siri close ditambah kesemua 80 set parameter; Rust mengira setiap kombo; satu balasan kembali. Panggilan chunky.
- rust_batch_unchecked — sempadan batched yang sama, tetapi kernel mengindeks dengan
get_unchecked(tiada semakan sempadan dalam laluan panas). Ia wujud untuk menguji hipotesis khusus tentang jurang pengiraan; bahagian keputusan muktamad membelanjakannya. - rust_chatty_unix — pelayan yang sama, tetapi satu round-trip setiap kombo, siri 1.2 MB dihantar semula setiap kali. Seni bina RPC-setiap-unit-kerja yang naif.
- rust_spawn_stdin — spawn binari setiap sapuan dan paipkan permintaan melalui stdin. Corak "shell out kepada enjin CLI"; membayar penciptaan proses.
Dan get kesetaraan, yang tanpanya semua ini tidak bermakna apa-apa: selepas pemasaan, vektor per-kombo (PnL, bilangan dagangan) setiap varian Rust dibandingkan dengan numba — bilangan dagangan tepat, PnL kepada mutlak . Larian yang dikomit melaporkan all_ok: true untuk kedua-dua binaan pengindeksan-selamat dan get_unchecked. Cap jari kombo-pertama — PnL −5165.58 mata peratusan merentasi 57,029 dagangan — sepadan digit demi digit dengan kernel numba kajian tangga kelajuan, yang mengaitkan kedua-dua kertas kajian kepada kernel yang sama pada seed yang sama. Port merentas-bahasa adalah tepat tempat percanggahan senyap suka bersarang (yuran dikenakan sebelum bukannya selepas penukaran peratus, perbandingan NaN yang bercabang secara berbeza, off-by-one dalam tetingkap — spesies pepijat yang sama yang ditunjukkan oleh taksonomi look-ahead kami boleh menghasilkan Sharpe 15 daripada bunyi hingar). Penanda aras dua enjin yang mengira perkara berbeza bukan penanda aras; ia adalah dua program tidak berkaitan yang berlumba.
Dengan kesetaraan ditetapkan, setiap perbezaan dalam jadual di atas adalah sempadan dan pengiraan — tiada apa-apa lagi.
Apa yang sebenarnya dikos oleh lintasan: keluk echo

Mulakan dengan pisau bedah. Operasi echo membuat round-trip payload float melalui pelayan Rust — Python membina bingkai, pelayan menghurai kesemua float, mengekodnya semula, dan menghantarnya balik. Kedua-dua arah membayar serialisasi, syscall, dan transit soket. Berikut adalah keluk terukur (median merentasi 10 larian):
| Payload (float) | Bait setiap arah | Round-trip |
|---|---|---|
| 1 | 8 | 14.1 µs |
| 100 | 800 | 16.4 µs |
| 1,000 | 8,000 | 18.1 µs |
| 10,000 | 80,000 | 192.5 µs |
| 100,000 | 800,000 | 1,367.3 µs |
| 150,000 | 1,200,000 | 2,043.4 µs |
Dua fakta struktur bersarang dalam jadual ini.
Pertama, lantai (floor). Satu round-trip yang membawa pada asasnya tiada apa-apa — 8 bait — berkos 14 µs. Itulah harga yang tidak boleh dikurangkan bagi membuat panggilan sama sekali melalui pengangkutan ini: dua syscall write, dua syscall read, jentera soket kernel, kebangkitan penjadual (scheduler wake-up). Perhatikan betapa ratanya keluk itu di sebelah kiri: daripada 1 float kepada 1,000 float, kos itu hampir tidak bergerak (14.1 → 18.1 µs). Di bawah kira-kira 8 KB anda membayar untuk panggilan itu, bukan bait itu. Nombor ini — lantai latensi — adalah pemalar tunggal paling penting dalam keseluruhan kajian, dan kami akan membina aritmetik pulang modal di atasnya di bawah.
Kedua, kecerunan. Selepas ~10,000 float, keluk itu menjadi terikat-lebar-jalur (bandwidth-bound) dan lebih kurang linear. Siri penuh 1.2 MB — 2.4 MB dipindahkan secara keseluruhan, keluar dan balik, termasuk penghuraian dan pengekodan semula penuh 150,000 float di pihak Rust — berkos 2,043.4 µs. Ini bersamaan dengan ~1.2 GB/s efektif merentasi keseluruhan tindanan naif itu (terbitan: 2.4 MB / 2.04 ms) — soket domain Unix dengan bingkai berawalan-panjang dan penghurai float bait-demi-bait, tiada helah zero-copy, tiada memori kongsi, tiada apa-apa yang bijak.
Model munasabah bagi satu lintasan, dengan kedua-dua pemalar diukur:
Sekarang letakkan nombor tajuk itu dalam konteks. Sapuan penuh mengambil masa 2.010 s dalam-proses. Menghantar keseluruhan set datanya merentasi sempadan dan balik berkos ~2.0 ms — kira-kira 0.1% daripada kerja tersebut (terbitan: 2.0434 ms / 2.010 s). Jika anda merentasi sekali, dalam bait mentah, sempadan itu adalah ralat pembundaran. Itulah separuh kepercayaan rakyat yang mati dahulu: ketakutan itu tidak pernah tentang sesuatu yang semurah ini.
Pihak Rust bagi lintasan itu hampir setidak-glamor kod sistem boleh jadi — disesuaikan daripada engine/src/main.rs:
fn read_frame<R: Read>(r: &mut R) -> Option<Vec<u8>> {
let mut len_buf = [0u8; 4];
r.read_exact(&mut len_buf).ok()?;
let len = u32::from_le_bytes(len_buf) as usize;
let mut body = vec![0u8; len];
r.read_exact(&mut body).ok()?;
Some(body)
}
fn write_frame<W: Write>(w: &mut W, body: &[u8]) {
w.write_all(&(body.len() as u32).to_le_bytes()).unwrap();
w.write_all(body).unwrap();
w.flush().unwrap();
}
// the server is a loop: read frame -> compute -> write frame
for stream in listener.incoming() {
serve_stream(stream.unwrap());
}
Satu nota skop yang jujur sebelum meneruskan: kesemua nombor sempadan dalam kajian ini adalah soket domain Unix pada satu hos. Enjin itu juga bertutur TCP (dengan TCP_NODELAY), tetapi kami tidak mengukurnya; TCP loopback duduk agak di atas lantai-lantai ini, dan lompatan rangkaian sebenar adalah rejim yang sama sekali berbeza — milisaat lantai, bukan mikrosaat. Segala-galanya di sini oleh itu adalah kes terbaik-hampir untuk merentasi sempadan dengan cara ini. Ini menjadikan cukai yang diukur seterusnya lebih mengutuk lagi: itulah yang anda bayar di atas itu, secara pilihan.
Cukai serialisasi: 1348x kerana memilih JSON

Inilah tempat kepercayaan rakyat tentang "overhead IPC" ternyata satu salah label. Kami mengukur kos mengekod siri close 150,000-float yang sama dalam tiga cara — payload tepat yang dihantar oleh setiap seni bina di atas:
| Pengekodan | Masa untuk mengekod 1.2 MB float | berbanding mentah |
|---|---|---|
bait mentah (.tobytes()) |
49.1 µs | 1.0x |
| pickle | 29.8 µs | 0.6x |
JSON (json.dumps(close.tolist())) |
66,243 µs | 1348x |
Laluan mentah adalah memcpy yang memakai panggilan fungsi:
def build_request(opcode, close, params):
body = bytes([opcode]) + struct.pack("<II", len(close), len(params))
body += close.astype("<f8").tobytes() # 150,000 floats -> 1.2 MB in 49 µs
body += np.asarray(params, dtype="<i8").reshape(-1).tobytes()
return struct.pack("<I", len(body)) + body # length-prefixed frame
(Pickle mendarat sedikit lebih murah lagi daripada laluan mentah kami kerana astype membayar salinan penukaran-dtype walaupun dtype sudah sepadan; kedua-duanya adalah kelas-memcpy dan kedua-duanya adalah ralat pembundaran. Keluarga binari secara keseluruhan hidup tiga peringkat magnitud di bawah keluarga teks.)
Dan laluan teks adalah apa yang sebenarnya dihantar oleh hampir setiap penggunaan "mari jadikan enjin ini microservice":
body = json.dumps({"op": "sweep", "close": close.tolist(), "params": params})
Enam puluh enam milisaat. Untuk mengekod. json.dumps(close.tolist()) mengotakkan setiap float ke dalam objek Python, kemudian merender setiap satu sebagai teks perpuluhan — 150,000 peruntukan heap dan 150,000 penukaran float-ke-string di mana laluan mentah hanya melakukan satu salinan blok. Dan payload wayar turut mengembang (satu float64 berkos 8 bait dalam binari dan kira-kira dua hingga tiga kali ganda itu sebagai teks perpuluhan — kami tidak pun mengenakan caj untuk transit tambahan itu).
Sekarang skalakannya seperti cara penggunaan sebenar melakukannya. 66 ms itu adalah satu pengekodan, satu pihak, satu panggilan. Perkhidmatan JSON membayar pengekodan dan penyahkodan, pada kedua-dua pihak sempadan, pada setiap panggilan. Satu panggilan batched tunggal melalui JSON akan membakar ~3.3% daripada keseluruhan bajet pengiraan sapuan hanya pada pengekodan pihak-klien sahaja (terbitan: 66 ms / 2.010 s). Letakkan JSON di bawah seni bina chatty — satu panggilan setiap kombo, corak di bawah — dan pengekodan pihak-klien sahaja berkos 80 × 66 ms = 5.3 s: lebih daripada dua setengah kali ganda keseluruhan kerja berguna (terbitan), sebelum satu bait pun bergerak dan sebelum pelayan menghurai apa-apa.
Inilah "cukai IPC" sebenar yang telah diukur oleh kebanyakan pasukan dalam pengeluaran tanpa mereka sedari. Ia bukan pernah komunikasi antara-proses. Ia adalah serialisasi teks bagi tatasusunan berangka — 1348x yang ditimbulkan sendiri pada komponen paling murah sempadan itu. Dunia berlajur (columnar) mempelajari pengajaran ini bertahun-tahun dahulu, dan ia adalah pengajaran yang sama yang terus ditemui oleh kajian Polars vs pandas kami daripada pihak saluran paip data: format seperti Arrow wujud tepat supaya data tatasusunan boleh merentasi sempadan proses dan bahasa sebagai bait berlajur mentah, bukan sebagai teks. Jika perkhidmatan enjin anda bertutur JSON untuk tatasusunan harga, tiada penalaan soket akan menyelamatkan anda — protokol itu adalah kesesakan (bottleneck).
Chatty vs chunky: hukum Fowler, terukur

Hukum Pertama Reka Bentuk Objek Teragih Martin Fowler — "jangan agihkan objek anda" — datang dengan satu akibat logik yang dinyatakannya pada nafas yang sama: jika anda mesti merentasi sempadan, antara muka itu perlu berbutir kasar, kerana panggilan jauh berkos beberapa peringkat magnitud lebih daripada panggilan tempatan. Setiap veteran sistem teragih mengangguk bersetuju. Hampir tiada seorang pun mempunyai nombor untuk beban kerja mereka sendiri. Berikut adalah nombor kami.
Seni bina chunky dan chatty menjalankan pelayan yang sama, protokol yang sama, data yang sama — hanya granulariti panggilan yang berbeza:
srv.call(0, close, params)
[srv.call(0, close, [params[k]]) for k in range(n)]
Chunky: 2.276 s (1.13x). Chatty: 2.383 s (1.19x) — 107 ms lebih perlahan (terbitan: 2.383 − 2.276). Untuk tepat tentang apa delta itu dan apa yang bukan: keluk echo memberikan ramalan naif untuknya — 79 penghantaran tambahan siri penuh pada lebih kurang separuh daripada round-trip payload-penuh 2,043 µs setiap satu, kira-kira 81 ms — yang mendarat kira-kira 25% di bawah 107 ms yang terukur; bakinya adalah pembinaan permintaan dan pembingkaian setiap-panggilan pada pihak Python, yang tidak disertakan oleh ramalan echo. Walau bagaimanapun ia menjadi ~1.4 ms setiap lintasan tambahan (terbitan: 107 / 79); balasan tidak ketara — 16 bait setiap kombo.
Dua bacaan bagi 107 ms itu, dan kedua-duanya penting.
Bacaan lunak: ia hanya ~4.5% daripada dinding masa, bukan bencana. Benar — dan berbaloi memahami mengapa bencana cerita rakyat gagal termanifestasi di sini. Setiap panggilan chatty masih membawa 25,130 µs pengiraan sebenar (nilai satu kombo — kos per-kombo dalam-proses yang terukur), jadi overhead sempadan setiap-panggilan sebanyak ~1.4 ms kekal satu peringkat magnitud di bawah kerja setiap-panggilan. Seni bina chatty tidak membawa maut apabila setiap panggilan benar-benar berat. Ia menjadi membawa maut apabila granulariti mengecil — yang menjadi keseluruhan subjek bahagian pulang modal.
Bacaan yang mengutuk: cukai ini sepenuhnya sukarela, dan ia berskala dengan bilangan panggilan × payload. Corak chatty menghantar semula set data pada setiap panggilan atas satu sebab sahaja: perkhidmatan itu tanpa status (stateless), jadi setiap permintaan mesti membawa keseluruhan konteks. Itulah bentuk lalai bagi "endpoint sapuan" yang naif — dan pada dasarnya setiap microservice REST yang pernah dilakar di papan putih. Pelayan berstatus (stateful) — muatkan siri sekali, kemudian hantar bingkai parameter 48-bait — akan meletakkan setiap panggilan per-kombo berhampiran hujung payload-kecil keluk echo: kira-kira 16 µs setiap panggilan, lebih kurang 1.3 ms untuk kesemua 80 (terbitan daripada lantai echo; analitik, tidak diukur secara berasingan). Penalti chatty tidak akan mengecil; ia akan lenyap. Pengajarannya tepat: masalahnya bukan membuat banyak panggilan — ia adalah menghantar semula status kerana protokol itu berpura-pura setiap panggilan adalah yang pertama.
Pramuatkan data. Hantar parameter. Rentasi sempadan dengan niat, bukan dengan seluruh dunia dalam beg pakaian anda setiap kali.
Kos spawn: menyewa enjin mengikut panggilan

Corak penggunaan ketiga adalah yang paling lama: tiada pelayan langsung. Spawn binari enjin, paipkan satu permintaan melalui stdin, baca balasan daripada stdout, biarkan ia mati. Naluri setiap penskrip shell, setiap integrasi "panggil sahaja CLI daripada Python", setiap rangka kerja hiperparameter yang dikonfigurasikan untuk melancarkan satu binari setiap percubaan.
Terukur: 2.300 s (1.14x) — kira-kira 24 ms melebihi batch pelayan-berterusan (terbitan: 2.300 − 2.276). 24 milisaat itu membeli satu fork/exec, loader dinamik, persediaan paip, dan pembongkaran proses. Dan perhatikan bahawa apa yang diukur di sini hampir dengan lantai untuk corak ini: satu binari asli kecil bebas-kebergantungan, panas dalam cache halaman. Men-spawn apa-apa dengan runtime — JVM, jurubahasa Python dengan import — berkos jauh lebih banyak; kami tidak mengukur itu di sini, tetapi arahnya tidak diragui.
Struktur cukai ini adalah apa yang penting: ia tetap setiap panggilan, tidak kisah berapa banyak kerja yang dibawa oleh panggilan itu. Diamortisasikan merentasi sapuan 80-kombo penuh, 24 ms adalah kira-kira 1% — hingar. Spawn semula setiap kombo dan pemalar yang sama menjadi 80 × ~24 ms ≈ 1.9 s — pada dasarnya keseluruhan kerja berguna dibakar pada penciptaan proses (terbitan; analitik). Spawn semula setiap bar dan aritmetiknya tidak sanggup dituliskan.
Kos tetap, granulariti halus: pilih satu. Corak yang membayar satu spawn hanya waras apabila spawn itu jarang dan payload di belakangnya besar — tepat seperti pengukuran satu-spawn-setiap-sapuan kami, dan tepat tidak seperti cara seni bina subproses-setiap-simbol berakhir digunakan sebaik sahaja bilangan simbol berkembang.
Aritmetik pulang modal: lantai adalah kadar halangan

Segala-galanya yang diukur setakat ini termampat menjadi satu peraturan reka bentuk, dan peraturan itu adalah aritmetik, bukan pendapat.
Setiap lintasan sempadan berkos sekurang-kurangnya lantai latensi — 14 µs di sini, round-trip echo payload-kecil, dan hampir dengan yang terbaik yang ditawarkan oleh pengangkutan ini. Lantai itu adalah kadar halangan (hurdle rate): satu panggilan merentasi sempadan hanya berbaloi dibuat jika pengiraan yang dibawanya melepasi halangan itu dengan gandaan yang selesa. Takrifkan nisbah granulariti
dan bahagian sempadan daripada masa dinding anda adalah lebih kurang — dengan transit payload di atasnya jika panggilan itu turut membawa data.
Sekarang jalankan nombor sapuan melaluinya. Kos dalam-proses terukur bagi satu kombo adalah 25,130 µs. Pada granulariti per-kombo:
Panggilan per-kombo duduk ~1,795x di atas lantai — sempadan itu menuntut jauh di bawah sepersepuluh peratus setiap panggilan. Inilah sebabnya walaupun seni bina chatty hanya kehilangan 107 ms: pada granulariti beban kerja ini, setiap corak lintasan yang tidak menghantar semula data atau bertutur teks diamortisasikan dengan selamat. Panggilan peringkat-kombo, peringkat-lipatan, peringkat-sapuan semuanya jauh dalam zon murah.
Sekarang beralih kepada ekstrem bertentangan. Ini adalah ekstrapolasi merentas-beban-kerja yang ilustratif — bukan varian sapuan kami, tetapi bentuk beban kerja yang benar-benar wujud di alam liar: enjin dirujuk setiap bar. Perkhidmatan enjin gaya-langsung setiap-tick; strim isyarat gRPC-setiap-bar; "pelayan strategi" yang ditinjau sekali untuk setiap satu daripada 150,000 bar. Pengiraan berguna setiap bar dalam kernel ini adalah 25,130 µs / 150,000 ≈ 0.17 µs (terbitan) — setiap panggilan akan membawa kira-kira 1/84 kos sempadannya sendiri dalam kerja berguna (terbitan: lantai 14.05 µs berbanding 0.168 µs pengiraan). Jumlahnya lebih teruk daripada bunyi nisbah itu:
— lebih daripada keseluruhan kerja dalam-proses 2.010 s, dibelanjakan sebelum enjin jauh itu mengira satu nombor pun, dan ia akan kekal 2.1 s walaupun enjin di sebelah sana adalah infiniti pantas (terbitan: 150,000 × 14 µs). Tiada kelebihan pengiraan yang terselamat daripada granulariti sehalus itu. Dan ingat kembali bahawa lantai ini adalah soket Unix pada satu hos; buat panggilan setiap-bar itu kepada perkhidmatan merentasi rangkaian dan lantai itu berkembang sebanyak dua hingga tiga peringkat magnitud, pada 150,000 panggilan.

Satu lagi penentukuran yang jujur, kerana 14 µs juga bukan hukum fizik — ia adalah harga pengangkutan kami: satu klien Python, satu soket kernel, syscall dalam kedua-dua arah. Pengangkutan mesin-sama yang dibina-khusus pergi jauh lebih rendah. ZigBolt — bas pemesejan Zig sumber-terbuka kami untuk beban kerja HFT, ditanda aras secara asli pada mesin yang sama ini — melakukan round-trip gelang memori-kongsi dalam kira-kira 39 ns min (p50 sehala 10/20/30 ns pada mesej 64/256/1024-bait). Itu lebih kurang 360x di bawah lantai soket kami (terbitan: 14.05 µs / 39 ns). Perbandingan ini sengaja epal-dengan-oren, dan kami menandakannya sedemikian: 14 µs kami adalah round-trip soket klien-Python, 39 ns ZigBolt adalah Zig asli melalui memori kongsi, jadi jurang itu mencampurkan pengangkutan dan runtime. Bacalah ia bukan sebagai perlumbaan antara kedua-duanya tetapi sebagai julat yang boleh diduduki oleh lantai mesin-sama: kira-kira tiga peringkat magnitud, dipilih mengikut pelaksanaan. Ini adalah pengajaran RPC Ringan lama (Bershad et al., 1990) dalam pakaian moden — lintasan mesin-sama dikuasai oleh jentera protokol, dan ia runtuh apabila pengangkutan dibina untuk kes mesin-sama. Aritmetik pulang modal di atas tidak berubah bentuk; halangan itu hanya bergerak. Pada lantai 39 ns, walaupun granulariti setiap-bar akan melepasinya (150,000 × 39 ns ≈ 5.9 ms, terbitan) — yang tepat menerangkan bagaimana sistem HFT mampu memiliki sempadan yang tidak mampu dimiliki oleh perkhidmatan REST.
Inilah keseluruhan kisah pulang modal dalam satu ayat: sempadan tidak peduli betapa pantas enjin anda; ia mengenakan caj setiap lintasan, jadi pemboleh ubah yang anda kawal ialah berapa banyak kerja yang dibawa oleh setiap lintasan — dan apa lintasan itu diperbuat daripada. Batch setiap sapuan dan melebihi seratus ribu. Batch setiap kombo, — masih baik. Panggil setiap bar melalui soket, — seni bina itu mati sebelum pengoptimuman pertama, dan tiada penulisan semula enjin, dalam Rust atau apa-apa sahaja, boleh menghidupkannya semula.
Di mana 1.13x itu sebenarnya bersarang — dan keputusan muktamad

Masa untuk membedah jurang tajuk itu dengan jujur, kerana ia membawa penemuan paling counterintuitive kajian ini.
Seni bina Rust batched ketinggalan daripada numba dalam-proses sebanyak 266 ms (terbitan: 2.276 − 2.010). Komponen sempadan terukur: satu round trip payload-penuh pada ~2.0 ms, serialisasi mentah pada 49 µs, header bingkai pada segelintir bait — panggil keseluruhan bil sempadan itu ~2 ms. Oleh itu lebih 99% jurang itu bukan sempadan langsung. Ia adalah pengiraan: ditanggalkan daripada IPC, pelayan Rust membelanjakan ~2.274 s melakukan sapuan yang dilakukan numba dalam 2.010 s — kernel Rust naif itu kira-kira 13% lebih perlahan pada pengiraan mentah (terbitan).
Itu layak mendapat satu perenggan yang tidak berkelip, kerana "tulis semula dalam Rust dan ia akan menjadi lebih pantas" adalah sama banyaknya kepercayaan rakyat seperti "IPC akan membunuh anda." Kedua-dua kernel berakhir di LLVM — numba menurunkan bytecode Python melaluinya, rustc menurunkan MIR melaluinya — dan kedua-duanya berkemungkinan besar berjalan sebagai gelung skalar: jumlah dalaman WMA adalah pengurangan titik-terapung, yang tidak akan divektorkan-auto oleh LLVM tanpa lesen penyusunan-semula fast-math yang tidak diberikan oleh lalai @njit numba dan tidak diminta oleh port kami. Jadi ~13% itu adalah jurang codegen terukur antara dua gelung skalar yang disusun-LLVM — dan bukannya menegaskan satu sebab, kami menguji yang paling jelas. Suspek semula jadi adalah pengindeksan selamat Rust: gelung WMA panas menyemak-sempadan setiap capaian tatasusunan, di mana @njit numba disusun dengan semakan sempadan dimatikan. Jadi kami membina varian kernel yang sama yang disahkan-kesetaraan pada get_unchecked — tiada semakan sempadan di mana-mana dalam laluan panas — dan memasakannya sebagai seni bina kelima. Ia tidak menutup jurang itu: 2.337 s (1.16x), sedikit lebih perlahan daripada 2.276 s binaan bersemak-sempadan. Hipotesis diuji, hipotesis ditolak. Keadaan pengetahuan yang jujur: ~13% itu adalah nyata dan boleh dihasilkan semula (median merentasi 10 larian, sebaran dalam ~2%), dan buat masa ini tidak dapat dikaitkan — sedikit perbezaan dalam kelakuan peruntukan, struktur gelung, atau penjadualan arahan yang hanya boleh diselesaikan oleh pemprofilan peringkat-himpunan. Pengajaran itu kekal utuh: Rust naif tidak automatik lebih pantas daripada numba yang baik, dan sempadan bahasa yang dibeli atas andaian kemenangan pengiraan percuma boleh tiba dengan kerugian pengiraan yang tersangkut padanya. Kernel Rust yang ditala — buffer prapremuatan, SIMD eksplisit, benang merentasi kombo — masih boleh membalikkan tanda itu. Tetapi itu adalah soalan pengiraan, untuk diselesaikan oleh pemprofilan dan kerja kernel, dan soalan kajian ini adalah sempadan. Jawapan sempadan: direntasi sekali, dalam bait, ia berkos ~0.1%.
Jadi susunlah keputusan muktamad penuh, setiap klausanya diukur di atas.
Perkhidmatan enjin merentas-bahasa menang apabila kesemua ini berlaku:
- Kelebihan pengiraan itu nyata — diukur pada kernel anda, bukan diandaikan daripada reputasi bahasa itu. (Kelebihan kami adalah −13% sehingga dibuktikan sebaliknya — dan penjelasan "jelas" pertama untuk defisit itu mati dalam pengujian.)
- Anda merentasi secara kasar — satu panggilan setiap sapuan atau setiap lipatan, beribu-ribu gandaan di atas lantai 14 µs, seperti yang ditunjukkan oleh jumlah 1.13x seni bina batch (~0.1% sempadan).
- Anda bertutur binari — tatasusunan mentah berawalan-panjang, Arrow, apa-apa sahaja kelas-memcpy pada 49 µs setiap 1.2 MB; tidak pernah teks pada 66,243 µs.
- Data itu dipramuatkan — pelayan berstatus menerima panggilan parameter-sahaja pada hujung ~16 µs keluk echo dan bukannya menghantar semula megabait.
Ia kalah apabila digunakan dengan cara perkhidmatan enjin biasanya digunakan:
- Microservice JSON/REST — membayar cukai serialisasi 1348x pada setiap panggilan, kedua-dua arah; di bawah granulariti chatty itu adalah 5.3 s pengekodan pada kerja 2 s.
- RPC setiap unit kerja — setiap kombo ia berkos 107 ms di sini dan terselamat hanya kerana setiap panggilan membawa 25,130 µs pengiraan; setiap bar ia adalah ~2.1 s IPC tulen sebelum apa-apa kerja berlaku, pada kerja 2.0 s.
- Satu spawn setiap panggilan — ~24 ms kos tetap setiap kali, tidak berbahaya sekali setiap sapuan, hampir dua saat apabila dibayar setiap kombo.
Yang bermaksud: seni bina yang gagal itu tidak eksotik. Enjin JSON REST, subproses-setiap-simbol, gRPC-setiap-tick — itu adalah banci yang adil tentang bagaimana "mari faktorkan enjin backtest ini" sebenarnya dibina. Kepercayaan rakyat itu berasas secara empirikal sebagai penerangan amalan lazim dan salah secara empirikal sebagai hukum alam. Sempadan itu tidak pernah menjadi masalahnya. Cara lalai untuk merentasinya adalah masalahnya.
Satu hujah untuk sempadan itu layak mendapat ayatnya sendiri, kerana itulah sebab kami menjalankan kajian ini langsung. Satu kernel disusun tunggal di sebalik sempadan yang direka dengan baik boleh melayani sapuan penyelidikan dan gelung dagangan langsung — binari yang sama, aritmetik yang sama, bit demi bit. Kajian pariti backtest-live kami mengkatalogkan bagaimana enjin penyelidikan dan pengeluaran hanyut berasingan apabila mereka adalah dua pangkalan kod; perkhidmatan enjin adalah penawar struktur paling kuat untuk hanyutan itu, dan kajian ini menetapkan harga penawar itu dengan jujur: dilakukan dengan betul, kira-kira 0.1% masa dinding dan satu get kesetaraan untuk membuktikan tiada apa yang berubah dalam terjemahan. Perdagangan itu — satu sempadan proses khusus sebagai pertukaran untuk pariti satu-kernel — adalah, pada nombor-nombor ini, satu tawaran baik. Dilakukan dengan salah, idea yang sama menghantar cukai serialisasi 1348x kepada pengeluaran dengan PnL anda menunggang di atasnya.
Kesimpulan Utama
- Sempadan itu hampir percuma; kepercayaan rakyat gagal pengukuran. Membuat round-trip keseluruhan siri close 1.2 MB melalui soket Unix — penghuraian dan pengekodan semula penuh termasuk — berkos 2,043.4 µs, kira-kira 0.1% daripada kerja 2.010 s (terbitan). Seni bina Rust-atas-soket batched mendarat pada jumlah 1.13x, dan ~99% daripada jurang itu pun bukan IPC.
- "Tulis semula dalam Rust" adalah dakwaan pengiraan — sahkan sebelum membeli sempadan itu. Port Rust baris-demi-baris kami mengira ~13% lebih perlahan daripada kernel numba (terbitan: 2.274 s berbanding 2.010 s) — jurang codegen boleh-dihasilkan-semula antara dua gelung skalar yang disusun-LLVM yang kekal tidak dapat dikaitkan: kami menguji suspek yang jelas dan menolaknya, kerana binaan
get_uncheckedyang disahkan-kesetaraan tanpa semakan sempadan ternyata tidak lebih pantas (2.337 s berbanding 2.276 s). Rust naif tidak automatik lebih pantas; kernel yang ditala mungkin sahaja lebih pantas — ukur, kemudian putuskan. - Cukai sebenar adalah teks. Mengekod 150,000 float sebagai JSON berkos 66,243 µs berbanding 49.1 µs mentah — 1348x, dibayar setiap arah, setiap panggilan, pada kedua-dua pihak. Penggunaan JSON chatty membakar 5.3 s pengekodan pada kerja 2 s (terbitan). Bertutur binari merentasi sempadan: bingkai mentah, Arrow — jangan sekali-kali
json.dumpspada tatasusunan harga. - Chatty vs chunky boleh diukur, dan tanpa-status (statelessness) adalah puncanya. Panggilan per-kombo yang menghantar semula data: 1.19x berbanding 1.13x batch (+107 ms, terbitan; ramalan sehala keluk echo sebanyak ~81 ms mendarat ~25% di bawahnya, bakinya adalah pembingkaian setiap-panggilan). Pelayan berstatus yang dipramuatkan akan mengambil kesemua 80 panggilan yang sama pada ~16 µs setiap satu — kira-kira 1.3 ms jumlahnya (terbitan daripada lantai echo). Hantar parameter, bukan set data.
- Hormati lantai — dan ketahui bahawa lantai itu adalah pilihan. Lintasan Python-atas-soket-Unix kami berlantai pada 14 µs; granulariti per-kombo melepasinya ~1,795x ganda (25,130 µs pengiraan setiap panggilan) — selamat. Corak setiap-bar (ekstrem merentas-beban-kerja yang ilustratif: enjin langsung setiap-tick, bukan sapuan ini) akan membayar 150,000 × 14 µs ≈ 2.1 s IPC tulen pada kerja 2.0 s (terbitan) — mati semasa ketibaan walaupun dengan enjin infiniti pantas. Men-spawn setiap panggilan menambah kos tetap ~24 ms (terbitan). Dan pengangkutan memori-kongsi yang dibina-khusus seperti ZigBolt membuat round-trip dalam ~39 ns secara asli pada mesin ini — ~360x di bawah lantai soket kami (terbitan; Zig asli berbanding klien Python, jadi bacalah sebagai julat yang boleh diduduki oleh lantai, bukan perlumbaan).
- Rentasi sekali, dalam bait, dengan data sudah tersedia di sana — dan sempadan itu membeli anda pariti untuk ~0.1%. Satu kernel melayani penyelidikan dan langsung, di-get oleh pemeriksaan kesetaraan (PnL −5165.58, 57,029 dagangan, identik merentasi bahasa dan merentasi kedua-dua binaan Rust), adalah kes jujur untuk perkhidmatan enjin. Kes-kes tidak jujur — JSON, chatty, spawn-setiap-panggilan — adalah yang memberikan IPC reputasinya.
Eksperimen penuh — enjin Rust, protokol wayar, harness echo dan serialisasi, get kesetaraan, dan setiap nombor dalam artikel ini boleh dijana semula daripada satu skrip deterministik — berada dalam kertas kajian rakan di ipc-tax.marketmaker.cc, dengan kod dan data di github.com/suenot/ipc-tax.
Soket itu tidak pernah menjadi masalah. Dua milisaat untuk keseluruhan set data, round trip — cerita rakyat itu tersasar tiga peringkat magnitud, dan dalam kedua-dua arah serentak: terlalu pesimistik tentang bait, terlalu pemaaf terhadap teks. Rentasi sempadan itu seolah-olah ia berkos sesuatu, dan ia tidak akan.
Pengarang
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.