← लेखों की सूची पर वापस जाएँ
June 30, 2026
5 मिनट का पठन

IPC टैक्स: बैकटेस्ट इंजन को सॉकेट के पीछे रखें और 13% गंवाएं — जिसमें से लगभग कुछ भी सॉकेट की वजह से नहीं

#algotrading
#backtest
#performance
#ipc
#rust
#architecture
Part 10 of 10 · Collection
High-Performance Backtest Engines

"बैकटेस्ट्स विदाउट इल्यूज़न्स" सीरीज़ का हिस्सा।

📄 यह लेख एक रिसर्च पेपर में बदल गया। एक पाथ-डिपेंडेंट बैकटेस्ट कर्नेल को numba से Rust में लाइन-दर-लाइन पोर्ट किया गया और एक प्रोसेस/भाषा बाउंड्री के पार चार तरीकों से कॉल किया गया, एक इक्विवैलेंस गेट के साथ जो प्रति-कॉम्बो समान PnL की पुष्टि करता है — साथ ही शुद्ध IPC लेटेंसी कर्व, सीरियलाइज़ेशन टैक्स, और स्पॉन कॉस्ट के अलग-अलग मापन। पेपर ऑनलाइन पढ़ें (इंटरैक्टिव वर्जन + PDF) ipc-tax.marketmaker.cc पर, कोड और डेटा github.com/suenot/ipc-tax पर।

हर बैकटेस्ट इंजन जो तेज़ हो जाता है, अंततः वही बातचीत पैदा करता है। हमारा भी समय पर आया। स्पीड लैडर ने अभी-अभी एक 80-कॉम्बो पैरामीटर स्वीप को pandas के 69.9 सेकंड से घटाकर सिंगल-थ्रेडेड numba के लगभग 2 सेकंड तक पहुँचाया था, और अगली स्वाभाविक इच्छा थी: Python JIT पर क्यों रुकें? कर्नेल को Rust में फिर से लिखें। इसे एक असली इंजन सर्विस बनाएं — एक कंपाइल की गई बाइनरी जो सॉकेट के पीछे हो, हर रिसर्च स्क्रिप्ट, हर भाषा, और लाइव ट्रेडर द्वारा भी कॉल की जा सके। एक कर्नेल, एक सत्य, कोई डुप्लिकेट लॉजिक नहीं।

और फिर काउंटर-आर्ग्युमेंट आता है, वह भी समय पर: जिस पल आप प्रोसेस से बाहर निकलते हैं, IPC आपको खा जाता है। डेटा को सीरियलाइज़ करना होगा, बाउंड्री के पार भेजना होगा, डिसीरियलाइज़ करना होगा; हर कॉल syscalls और context switches की कीमत चुकाता है; आपका खूबसूरत Rust कर्नेल अपनी ज़िंदगी एक पाइप पर इंतज़ार करते हुए बिताएगा। प्रोसेस में ही रहें। सब जानते हैं यह।

यह लेख वह चीज़ मापता है जो सब जानते हैं, और यह मापन बहस के दोनों पक्षों से ज़्यादा दिलचस्प है। लोक-मान्यता — "एक तेज़ क्रॉस-लैंग्वेज इंजन इन-प्रोसेस numba से हार जाता है क्योंकि IPC आपको मार डालता है" — साबित होती है सामान्यतः गलत और केवल विशिष्ट परिस्थितियों में सही। बाउंड्री को एक बार, रॉ बाइट्स में पार करने में लगभग 2 मिलीसेकंड लगते हैं दो-सेकंड के जॉब पर: एक राउंडिंग एरर। टैक्स बाउंड्री में नहीं है। यह इसमें है कि आप उसे कैसे पार करते हैं — और तीन तरीके जिनसे इंजन सर्विसेज़ आमतौर पर असल दुनिया में डिप्लॉय की जाती हैं (एक JSON API, प्रति-यूनिट-ऑफ़-वर्क एक कॉल, प्रति-कॉल एक प्रोसेस स्पॉन) उनमें से हर एक, मापने योग्य रूप से, उसी आपदा का एक टुकड़ा है जिसकी लोकमान्यता भविष्यवाणी करती है।

यहाँ पूरा प्रयोग पहले से है। नीचे की हर चीज़ हर पंक्ति की शारीरिक रचना है।

आर्किटेक्चर प्रति स्वीप बाउंड्री के पार क्या जाता है वॉल टाइम इन-प्रोसेस की तुलना में
इन-प्रोसेस numba कुछ नहीं — एक डायरेक्ट कॉल 2.010 s 1.00x
Rust सर्वर, batched (Unix सॉकेट) एक राउंड-ट्रिप: पूरी सीरीज़ + सभी 80 पैरामीटर सेट 2.276 s 1.13x
Rust सर्वर, batched, get_unchecked कर्नेल वही सिंगल राउंड-ट्रिप — एक बाउंड्स-चेक-फ्री कर्नेल वैरिएंट (देखें वर्डिक्ट) 2.337 s 1.16x
Rust सर्वर, chatty (Unix सॉकेट) 80 राउंड-ट्रिप्स: सीरीज़ प्रति कॉम्बो दोबारा भेजी जाती है 2.383 s 1.19x
Rust spawn (stdin/stdout) प्रोसेस स्पॉन + एक पाइप की गई रिक्वेस्ट 2.300 s 1.14x

Apple M2 Max, Python 3.14.6, numpy 2.4.3, numba 0.64.0, rustc 1.94.0 (रिलीज़ बिल्ड, ज़ीरो बाहरी क्रेट्स)। 150,000 बार्स × 80 कॉम्बोज़, 0.09% राउंड-ट्रिप फीस, सीड 42; क्लोज़ सीरीज़ वायर पर 1,200,000 बाइट्स (1.2 MB) है। प्रति आर्किटेक्चर 10 रन का मीडियन; मिन-मैक्स स्प्रेड ~2% के भीतर रहते हैं। सभी पाँच एक ही HMA/HMA3 स्टॉप-एंड-रिवर्स स्वीप चलाते हैं, और एक इक्विवैलेंस गेट पुष्टि करता है कि दोनों Rust कर्नेल वैरिएंट्स के प्रति-कॉम्बो (PnL, ट्रेड काउंट) परिणाम numba से बिल्कुल मेल खाते हैं — फिंगरप्रिंट PnL −5165.58, 57,029 ट्रेड्स में, स्पीड-लैडर स्टडी के numba कर्नेल से उसी सीड पर बाइट-दर-बाइट समान। हम बाउंड्रीज़ की तुलना कर रहे हैं, इम्प्लीमेंटेशंस की नहीं।

batched पंक्ति को ध्यान से पढ़ें, क्योंकि यह पूरी थीसिस को वहन करती है। Rust-ओवर-सॉकेट आर्किटेक्चर इन-प्रोसेस numba से 1.13x धीमा है — पूरे स्वीप पर 266 ms पीछे (व्युत्पन्न: 2.276 − 2.010)। लोक-कथा कहती है कि वे मिलीसेकंड IPC हैं। नहीं हैं। उस गैप के लगभग 2 ms बाउंड्री हैं — पूरी 1.2 MB क्लोज़ सीरीज़ भेजी गई, परिणाम वापस भेजे गए, सीधे मापा गया। बाकी ~264 ms इसलिए हैं क्योंकि हमारा नैव Rust कर्नेल स्वीप को numba कर्नेल से बस लगभग 13% धीमा कंप्यूट करता है (व्युत्पन्न: 2.276 s में से ~2 ms बाउंड्री घटाकर ≈ 2.274 s Rust कंप्यूट, बनाम numba के लिए 2.010 s)। भाषा के रूप में Rust, भाषा के रूप में Python से नहीं हारा; एक स्केलर LLVM-कंपाइल्ड लूप ने दूसरे के खिलाफ कोडजेन रेस हार दी — और हम उस नुकसान को स्पष्ट संदिग्ध पर भी नहीं थोप सके: उसी कर्नेल का एक बाउंड्स-चेक-फ्री get_unchecked बिल्ड तेज़ नहीं निकला (2.337 s; वर्डिक्ट सेक्शन इसे विच्छेदित करता है)। सॉकेट का इसमें से लगभग कुछ भी लेना-देना नहीं था।

उस वाक्य के दोनों हिस्सों को थामें रखें। बाउंड्री लगभग मुफ़्त है जब सही तरीके से पार की जाए — और "इसे Rust में फिर से लिखो" आपको एक डिप्लॉयमेंट बाउंड्री खरीदता है, कोई ऑटोमैटिक कंप्यूट जीत नहीं। दोनों तथ्य लोकप्रिय अंतर्ज्ञान के खिलाफ जाते हैं, और दोनों तालिका में हैं।

एक कर्नेल, दो भाषाएँ, चार बाउंड्रीज़

वर्कलोड जानबूझकर वही है जिसे स्पीड लैडर ने तय किया था, ताकि दोनों स्टडीज़ एक-दूसरे से जुड़ी रहें। कर्नेल एक HMA/HMA3 क्रॉस है — दो Hull-स्टाइल मूविंग एवरेजेज़ पर एक स्टॉप-एंड-रिवर्स सिस्टम, प्रति पैरामीटर कॉम्बिनेशन सात वेटेड-मूविंग-एवरेज पास प्लस एक स्टेटफुल बार-दर-बार इवेंट लूप जो एक पोज़िशन कैरी करता है, हर क्रॉस पर 0.09% राउंड-ट्रिप फीस माइनस PnL बुक करता है, और रिवर्स करता है। डेटा 150,000 बार्स का सीडेड सिंथेटिक ज्यॉमेट्रिक ब्राउनियन मोशन है (seed=42); ग्रिड [6,200][6, 200] पर फैले 80 HMA लंबाइयाँ हैं। इन-प्रोसेस रेफरेंस लैडर की सिंगल-थ्रेडेड numba रंग है, इस स्टडी के लिए फिर से मापी गई: वहाँ 1.98 s, यहाँ 2.010 s — वही कर्नेल, वही मशीन, आश्वस्त करने वाला उबाऊ।

क्रॉस-लैंग्वेज इंजन उस numba कर्नेल का Rust में लाइन-दर-लाइन पोर्ट है — वही लूप्स, वही NaN हैंडलिंग, वही फीस अरिथमेटिक — रिलीज़ मोड में बिना किसी बाहरी क्रेट के कंपाइल किया गया, ताकि पूरा प्रयोग डिपेंडेंसी-फ्री और रिप्रोड्यूसिबल रहे। यह जानबूझकर एक न्यूनतम बाइनरी प्रोटोकॉल बोलता है: हर दिशा में एक लेंथ-प्रीफिक्स्ड फ्रेम, सब कुछ लिटिल-एंडियन।

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

echo ऑपकोड स्टडी का स्केलपेल है: एक नियंत्रणीय आकार का राउंड-ट्रिप जो कुछ भी कंप्यूट नहीं करता, ताकि शुद्ध बाउंड्री कॉस्ट को अलग से मापा जा सके — सीरियलाइज़ेशन, syscalls, सॉकेट ट्रांज़िट, डिसीरियलाइज़ेशन, और कुछ नहीं।

पाँच मापे गए आर्किटेक्चर — चार बाउंड्री पैटर्न प्लस एक कर्नेल वैरिएंट:

  • in_process — numba कर्नेल को सीधे कॉल करें। कोई बाउंड्री नहीं। रेफरेंस।
  • rust_batch_unix — एक Unix डोमेन सॉकेट पर एक पर्सिस्टेंट Rust सर्वर। एक राउंड-ट्रिप पूरी क्लोज़ सीरीज़ प्लस सभी 80 पैरामीटर सेट भेजता है; Rust हर कॉम्बो कंप्यूट करता है; एक रिप्लाई वापस आता है। भारी-भरकम कॉल।
  • rust_batch_unchecked — वही batched बाउंड्री, लेकिन कर्नेल get_unchecked से इंडेक्स करता है (हॉट पाथ में कोई बाउंड्स चेक नहीं)। कंप्यूट गैप के बारे में एक विशिष्ट परिकल्पना को टेस्ट करने के लिए मौजूद है; वर्डिक्ट सेक्शन इसे खर्च करता है।
  • rust_chatty_unix — वही सर्वर, लेकिन प्रति कॉम्बो एक राउंड-ट्रिप, हर बार 1.2 MB सीरीज़ दोबारा भेजी जाती है। नैव RPC-प्रति-यूनिट-ऑफ़-वर्क आर्किटेक्चर।
  • rust_spawn_stdin — प्रति स्वीप बाइनरी को स्पॉन करें और रिक्वेस्ट को stdin पर पाइप करें। "CLI इंजन को शेल आउट करें" पैटर्न; प्रोसेस क्रिएशन की कीमत चुकाता है।

और इक्विवैलेंस गेट, जिसके बिना इसमें से कुछ भी मायने नहीं रखता: टाइमिंग के बाद, हर Rust वैरिएंट के प्रति-कॉम्बो (PnL, ट्रेड काउंट) वेक्टर की तुलना numba से की जाती है — ट्रेड काउंट बिल्कुल सटीक, PnL एक एब्सोल्यूट 10610^{-6} तक। कमिटेड रन दोनों बिल्ड्स — सेफ-इंडेक्सिंग और get_unchecked — के लिए all_ok: true रिपोर्ट करता है। पहला-कॉम्बो फिंगरप्रिंट — PnL −5165.58 प्रतिशत अंक, 57,029 ट्रेड्स में — स्पीड-लैडर स्टडी के numba कर्नेल से अंक-दर-अंक मेल खाता है, जो दोनों पेपर्स को उसी सीड पर उसी कर्नेल से जोड़ता है। क्रॉस-लैंग्वेज पोर्ट्स ठीक वहीं होते हैं जहाँ साइलेंट डाइवर्जेंस रहना पसंद करता है (एक फीस जो प्रतिशत रूपांतरण से पहले लगाई जाए बजाय बाद के, एक NaN तुलना जो अलग तरह से ब्रांच करती है, एक विंडो में ऑफ-बाय-वन — उसी प्रकार का बग जो हमारे लुक-अहेड बायस टैक्सोनॉमी ने दिखाया था कि शोर से 15 का Sharpe बना सकता है)। दो इंजनों का बेंचमार्क जो अलग चीज़ें कंप्यूट करते हैं, बेंचमार्क नहीं है; यह दो असंबंधित प्रोग्राम्स की दौड़ है।

इक्विवैलेंस स्थापित होने के साथ, ऊपर की तालिका में हर अंतर बाउंड्री और कंप्यूट है — कुछ और नहीं।

पार करने की वास्तविक लागत क्या है: इको कर्व

The measured cost of a boundary crossing: a latency curve flat at fourteen microseconds for tiny payloads, bending upward only past ten thousand floats, reaching two milliseconds for the full 1.2-megabyte series

स्केलपेल से शुरू करते हैं। echo ऑप nn फ्लोट्स की एक पेलोड को Rust सर्वर के जरिए राउंड-ट्रिप करता है — Python फ्रेम बनाता है, सर्वर सभी nn फ्लोट्स को पार्स करता है, उन्हें फिर से एनकोड करता है, और वापस भेजता है। दोनों दिशाएँ सीरियलाइज़ेशन, syscalls, और सॉकेट ट्रांज़िट की कीमत चुकाती हैं। यहाँ मापा गया कर्व है (10 रन का मीडियन):

पेलोड (फ्लोट्स) प्रति दिशा बाइट्स राउंड-ट्रिप
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

इस तालिका में दो संरचनात्मक तथ्य रहते हैं।

पहला, फ़्लोर। एक राउंड-ट्रिप जो अनिवार्य रूप से कुछ भी नहीं ले जाता — 8 बाइट्स — की कीमत 14 µs है। यह इस ट्रांसपोर्ट पर बिल्कुल एक कॉल करने की अपरिहार्य कीमत है: दो write syscalls, दो read syscalls, कर्नेल सॉकेट मशीनरी, शेड्यूलर वेक-अप्स। ध्यान दें बाईं ओर कर्व कितना समतल है: 1 फ्लोट से 1,000 फ्लोट्स तक, कॉस्ट मुश्किल से हिलती है (14.1 → 18.1 µs)। लगभग 8 KB से नीचे आप कॉल के लिए भुगतान कर रहे हैं, बाइट्स के लिए नहीं। यह संख्या — लेटेंसी फ़्लोर — पूरी स्टडी का सबसे महत्वपूर्ण कॉन्स्टेंट है, और हम नीचे इस पर ब्रेक-ईवन अरिथमेटिक बनाएंगे।

दूसरा, स्लोप। ~10,000 फ्लोट्स के बाद कर्व बैंडविड्थ-बाउंड हो जाता है और लगभग लीनियर। पूरी 1.2 MB सीरीज़ — कुल 2.4 MB मूव हुई, आगे और पीछे, जिसमें Rust साइड पर 150,000 फ्लोट्स का पूरा पार्स और री-एनकोड शामिल है — की कीमत 2,043.4 µs है। यह पूरे नैव स्टैक के जरिए प्रभावी रूप से ~1.2 GB/s निकालता है (व्युत्पन्न: 2.4 MB / 2.04 ms) — लेंथ-प्रीफिक्स्ड फ्रेम्स और बाइट-बाय-बाइट फ्लोट पार्सर वाला एक Unix डोमेन सॉकेट, कोई ज़ीरो-कॉपी ट्रिक्स नहीं, कोई शेयर्ड मेमोरी नहीं, कुछ भी चालाक नहीं।

दोनों मापे गए कॉन्स्टेंट्स के साथ, एक क्रॉसिंग का एक उचित मॉडल:

Tcall(b)    14 μsफ़्लोर  +  2b1.2 GB/sपेलोड, दोनों दिशाएँT_{\text{call}}(b) \;\approx\; \underbrace{14\ \mu\text{s}}_{\text{फ़्लोर}} \;+\; \underbrace{\frac{2b}{1.2\ \text{GB/s}}}_{\text{पेलोड, दोनों दिशाएँ}}

अब हेडलाइन नंबर को संदर्भ में रखते हैं। पूरा स्वीप इन-प्रोसेस 2.010 s लेता है। इसके पूरे डेटासेट को बाउंड्री के पार और वापस भेजने में ~2.0 ms लगते हैं — जॉब का लगभग 0.1% (व्युत्पन्न: 2.0434 ms / 2.010 s)। अगर आप एक बार, रॉ बाइट्स में पार करते हैं, तो बाउंड्री एक राउंडिंग एरर है। यह लोक-मान्यता का वह आधा हिस्सा है जो सबसे पहले मरता है: डर कभी भी इतनी सस्ती चीज़ के बारे में नहीं था।

उस क्रॉसिंग की Rust साइड सिस्टम कोड जितनी बेरौनक हो सकती है, उतनी है — 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());
}

आगे बढ़ने से पहले स्कोप पर एक ईमानदार नोट: इस स्टडी में सभी बाउंड्री नंबर एक होस्ट पर एक Unix डोमेन सॉकेट के हैं। इंजन TCP भी बोलता है (TCP_NODELAY के साथ), लेकिन हमने उसे मापा नहीं; लूपबैक TCP इन फ़्लोर्स से कुछ ऊपर बैठता है, और एक वास्तविक नेटवर्क हॉप बिल्कुल अलग रीजीम है — मिलीसेकंड का फ़्लोर, माइक्रोसेकंड का नहीं। यहाँ सब कुछ इसलिए इस तरह बाउंड्री पार करने का निकटतम-सर्वोत्तम केस है। जो आगे मापे गए टैक्सेज़ को और भी अधिक निंदनीय बनाता है: वे वह हैं जो आप इसके ऊपर चुनाव से चुकाते हैं।

सीरियलाइज़ेशन टैक्स: JSON चुनने के लिए 1348x

Two encodings of the same 150,000-float array side by side: a raw-bytes memcpy measured in microseconds against a JSON text encoding towering three orders of magnitude taller

यहाँ "IPC ओवरहेड" के बारे में लोक-मान्यता एक गलत लेबलिंग साबित होती है। हमने उसी 150,000-फ्लोट क्लोज़ सीरीज़ को एनकोड करने की कीमत तीन तरीकों से मापी — बिल्कुल वही पेलोड जो ऊपर हर आर्किटेक्चर भेजता है:

एनकोडिंग 1.2 MB फ्लोट्स एनकोड करने का समय रॉ की तुलना में
रॉ बाइट्स (.tobytes()) 49.1 µs 1.0x
pickle 29.8 µs 0.6x
JSON (json.dumps(close.tolist())) 66,243 µs 1348x

रॉ पाथ एक फंक्शन कॉल के भेस में एक memcpy है:

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 हमारे रॉ पाथ से भी थोड़ा सस्ता निकलता है क्योंकि astype एक dtype-कन्वर्ज़न कॉपी चुकाता है भले ही dtype पहले से मेल खाता हो; दोनों memcpy-क्लास हैं और दोनों राउंडिंग एरर हैं। बाइनरी फैमिली समग्र रूप से टेक्स्ट फैमिली से तीन ऑर्डर ऑफ़ मैग्नीट्यूड नीचे रहती है।)

और टेक्स्ट पाथ वह है जो लगभग हर "आइए इंजन को माइक्रोसर्विस बनाएं" डिप्लॉयमेंट वास्तव में भेजता है:

body = json.dumps({"op": "sweep", "close": close.tolist(), "params": params})

छियासठ मिलीसेकंड। सिर्फ एनकोड करने के लिए। json.dumps(close.tolist()) हर फ्लोट को एक Python ऑब्जेक्ट में बॉक्स करता है, फिर हर एक को डेसिमल टेक्स्ट के रूप में रेंडर करता है — 150,000 हीप एलोकेशन और 150,000 फ्लोट-टू-स्ट्रिंग कन्वर्ज़न जहाँ रॉ पाथ ने एक ब्लॉक कॉपी की थी। और वायर पेलोड भी फूल जाता है (एक float64 की कीमत बाइनरी में 8 बाइट्स है और डेसिमल टेक्स्ट के रूप में लगभग दो से तीन गुना — हमने अतिरिक्त ट्रांज़िट के लिए तो चार्ज भी नहीं किया)।

अब इसे उस तरह स्केल करते हैं जैसे एक असली डिप्लॉयमेंट करता है। वह 66 ms एक एनकोड, एक साइड, एक कॉल है। एक JSON सर्विस एनकोड और डिकोड दोनों चुकाती है, बाउंड्री के दोनों साइड्स पर, हर कॉल पर। JSON पर एक सिंगल batched कॉल पूरे स्वीप के कंप्यूट बजट का ~3.3% अकेले क्लाइंट-साइड एनकोडिंग पर जला देगी (व्युत्पन्न: 66 ms / 2.010 s)। JSON को chatty आर्किटेक्चर के तहत रखें — प्रति कॉम्बो एक कॉल, नीचे दिया गया पैटर्न — और अकेले क्लाइंट-साइड एनकोडिंग की कीमत 80 × 66 ms = 5.3 s होती है: पूरे उपयोगी जॉब के ढाई गुने से भी ज़्यादा (व्युत्पन्न), इससे पहले कि एक भी बाइट हिले और सर्वर कुछ भी पार्स करे।

यह वह असली "IPC टैक्स" है जो ज़्यादातर टीमों ने प्रोडक्शन में मापा है बिना जाने। यह कभी इंटर-प्रोसेस कम्युनिकेशन था ही नहीं। यह न्यूमेरिक अरेज़ का टेक्स्ट सीरियलाइज़ेशन था — बाउंड्री के सबसे सस्ते कंपोनेंट पर एक सेल्फ-इनफ्लिक्टेड 1348x। कॉलमनार दुनिया ने यह सबक सालों पहले सीखा था, और यह वही है जिसमें हमारी Polars बनाम pandas स्टडी डेटा-पाइपलाइन साइड से बार-बार टकराई। Arrow जैसे फॉर्मेट्स ठीक इसीलिए मौजूद हैं ताकि अरे डेटा प्रोसेस और भाषा की बाउंड्रीज़ को रॉ कॉलमनार बाइट्स के रूप में पार कर सके, टेक्स्ट के रूप में नहीं। अगर आपकी इंजन सर्विस प्राइस अरेज़ के लिए JSON बोलती है, तो कोई सॉकेट ट्यूनिंग आपको नहीं बचाएगी — प्रोटोकॉल ही बॉटलनेक है।

Chatty बनाम Chunky: फाउलर का नियम, मापा गया

A chunky architecture shipping one large framed payload across the boundary once, beside a chatty architecture making eighty small round-trips that each drag the full dataset along

मार्टिन फाउलर का डिस्ट्रिब्यूटेड ऑब्जेक्ट डिज़ाइन का पहला नियम — "अपने ऑब्जेक्ट्स को डिस्ट्रिब्यूट मत करो" — एक कोरोलरी के साथ आता है जिसे उन्होंने उसी सांस में स्पष्ट किया: अगर आपको बाउंड्री पार करनी ही है, तो इंटरफ़ेस कोर्स-ग्रेन्ड होना चाहिए, क्योंकि एक रिमोट कॉल एक लोकल कॉल से ऑर्डर्स ऑफ़ मैग्नीट्यूड ज़्यादा महंगा है। हर डिस्ट्रिब्यूटेड-सिस्टम्स वेटरन इस पर सहमति में सिर हिलाता है। लगभग किसी के पास अपने खुद के वर्कलोड के लिए एक नंबर नहीं है। यह रहा हमारा।

chunky और chatty आर्किटेक्चर वही सर्वर, वही प्रोटोकॉल, वही डेटा चलाते हैं — केवल कॉल ग्रैन्युलैरिटी अलग है:

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 धीमा (व्युत्पन्न: 2.383 − 2.276)। यह डेल्टा क्या है और क्या नहीं, इस बारे में सटीक होने के लिए: इको कर्व इसके लिए एक नैव भविष्यवाणी देता है — पूरी सीरीज़ के 79 अतिरिक्त शिपमेंट्स, प्रत्येक लगभग 2,043 µs के पूर्ण-पेलोड राउंड-ट्रिप के आधे पर, लगभग 81 ms — जो मापे गए 107 ms से लगभग 25% नीचे बैठता है; बाकी Python साइड पर प्रति कॉल रिक्वेस्ट बिल्डिंग और फ्रेमिंग है, जिसे इको भविष्यवाणी शामिल नहीं करती। किसी भी तरह, यह प्रति अतिरिक्त क्रॉसिंग ~1.4 ms पर आता है (व्युत्पन्न: 107 / 79); रिप्लाइज़ नगण्य हैं — प्रति कॉम्बो 16 बाइट्स।

उन 107 ms की दो रीडिंग्स हैं, और दोनों मायने रखती हैं।

नरम रीडिंग: यह वॉल का केवल ~4.5% है, कोई तबाही नहीं। सच — और यह समझने लायक है कि लोकमान्यता की तबाही यहाँ क्यों वास्तविक नहीं हुई। हर chatty कॉल अभी भी 25,130 µs असली कंप्यूट ले जाता है (एक कॉम्बो के बराबर — मापी गई इन-प्रोसेस प्रति-कॉम्बो कॉस्ट), जिससे ~1.4 ms का प्रति-कॉल बाउंड्री ओवरहेड प्रति-कॉल काम से एक ऑर्डर ऑफ़ मैग्नीट्यूड नीचे रहता है। Chatty आर्किटेक्चर तब घातक नहीं होते जब हर कॉल वास्तव में भारी हो। वे घातक तब बनते हैं जब ग्रैन्युलैरिटी सिकुड़ती है — जो ब्रेक-ईवन सेक्शन का पूरा विषय है।

निंदनीय रीडिंग: यह टैक्स पूरी तरह स्वैच्छिक था, और यह कॉल काउंट × पेलोड के साथ स्केल करता है। chatty पैटर्न हर कॉल पर डेटासेट को केवल एक कारण से दोबारा भेजता है: सर्विस स्टेटलेस है, इसलिए हर रिक्वेस्ट को पूरा कॉन्टेक्स्ट ले जाना होगा। यह एक नैव "स्वीप एंडपॉइंट" का डिफ़ॉल्ट रूप है — और वस्तुतः हर REST माइक्रोसर्विस का जो कभी व्हाइटबोर्ड पर स्केच किया गया। एक स्टेटफुल सर्वर — सीरीज़ को एक बार लोड करें, फिर 48-बाइट पैरामीटर फ्रेम्स भेजें — हर प्रति-कॉम्बो कॉल को इको कर्व के छोटे-पेलोड सिरे के पास रखेगा: प्रति कॉल लगभग 16 µs, सभी 80 के लिए लगभग 1.3 ms (इको फ़्लोर से व्युत्पन्न; एनालिटिकल, अलग से नहीं मापा गया)। chatty पेनल्टी सिकुड़ेगी नहीं; यह गायब हो जाएगी। सबक सटीक है: समस्या कई कॉल्स करना नहीं है — यह स्टेट को दोबारा भेजना है क्योंकि प्रोटोकॉल दिखावा करता है कि हर कॉल पहली है।

डेटा को प्रीलोड करें। पैरामीटर भेजें। बाउंड्री को इरादे के साथ पार करें, हर बार अपने सूटकेस में पूरी दुनिया के साथ नहीं।

स्पॉन कॉस्ट: प्रति कॉल इंजन किराए पर लेना

An engine binary being spawned from scratch for a single request: process creation, loader, and pipe setup stacked as a fixed toll booth in front of a short stretch of useful work

तीसरा डिप्लॉयमेंट पैटर्न सबसे पुराना है: कोई सर्वर ही नहीं। इंजन बाइनरी को स्पॉन करें, stdin पर एक रिक्वेस्ट पाइप करें, stdout से रिप्लाई पढ़ें, उसे मरने दें। हर शेल स्क्रिप्टर का इंस्टिंक्ट, हर "बस Python से CLI कॉल करें" इंटीग्रेशन, हर हाइपरपैरामीटर फ्रेमवर्क जो प्रति ट्रायल एक बाइनरी लॉन्च करने के लिए कॉन्फ़िगर किया गया है।

मापा गया: 2.300 s (1.14x) — पर्सिस्टेंट-सर्वर बैच के ऊपर लगभग 24 ms (व्युत्पन्न: 2.300 − 2.276)। वे 24 मिलीसेकंड एक fork/exec, डायनामिक लोडर, पाइप सेटअप, और प्रोसेस टियरडाउन खरीदते हैं। और ध्यान दें कि यह जो मापता है वह पैटर्न के लिए फ़्लोर के करीब है: एक छोटी डिपेंडेंसी-फ्री नेटिव बाइनरी, पेज कैश में वॉर्म। एक रनटाइम के साथ कुछ भी स्पॉन करना — एक JVM, इम्पोर्ट्स वाला एक Python इंटरप्रेटर — कहीं ज़्यादा महंगा है; हमने यहाँ उसे मापा नहीं, लेकिन दिशा संदेह में नहीं है।

इस टैक्स की संरचना ही मायने रखती है: यह प्रति कॉल फिक्स्ड है, इससे बेपरवाह कि कॉल कितना काम ले जाता है। पूरे 80-कॉम्बो स्वीप पर एमॉर्टाइज़्ड, 24 ms लगभग 1% है — शोर। प्रति कॉम्बो रीस्पॉन करें और वही कॉन्स्टेंट 80 × ~24 ms ≈ 1.9 s बन जाता है — मूलतः पूरा उपयोगी जॉब प्रोसेस क्रिएशन में जल गया (व्युत्पन्न; एनालिटिकल)। प्रति बार रीस्पॉन करें और अरिथमेटिक लिखने लायक भी नहीं रहती।

फिक्स्ड कॉस्ट, फाइन ग्रैन्युलैरिटी: एक चुनें। जो पैटर्न एक स्पॉन चुकाता है वह तभी समझदार है जब स्पॉन दुर्लभ हो और उसके पीछे की पेलोड विशाल हो — बिल्कुल हमारे एक-स्पॉन-प्रति-स्वीप मापन जैसा, और बिल्कुल उस तरीके के विपरीत जैसे प्रति-सिंबल-सबप्रोसेस आर्किटेक्चर अंततः इस्तेमाल होते हैं जब सिंबल काउंट बढ़ता है।

ब्रेक-ईवन अरिथमेटिक: एक फ़्लोर एक हर्डल रेट है

Break-even arithmetic on a balance: fourteen microseconds of boundary floor on one side weighed against the compute each call carries, with per-combo calls far above water and per-bar calls drowned

अब तक जो कुछ भी मापा गया है वह एक डिज़ाइन नियम में सिमट जाता है, और वह नियम अरिथमेटिक है, राय नहीं।

हर बाउंड्री क्रॉसिंग की कीमत कम से कम लेटेंसी फ़्लोर होती है — यहाँ 14 µs, छोटा-पेलोड इको राउंड-ट्रिप, और इस ट्रांसपोर्ट के सबसे अच्छे के करीब। वह फ़्लोर एक हर्डल रेट है: बाउंड्री के पार एक कॉल तभी करने लायक है जब वह जो कंप्यूट भेजता है वह उस हर्डल को एक आरामदायक गुणक से पार कर जाए। ग्रैन्युलैरिटी रेशियो को परिभाषित करें

G  =  Tप्रति कॉल कंप्यूटTफ़्लोरG \;=\; \frac{T_{\text{प्रति कॉल कंप्यूट}}}{T_{\text{फ़्लोर}}}

और आपके वॉल टाइम में बाउंड्री का हिस्सा लगभग 1/(1+G)1/(1+G) है — पेलोड ट्रांज़िट अतिरिक्त, अगर कॉल डेटा भी ले जाता है।

अब स्वीप के नंबरों को इसके माध्यम से चलाते हैं। एक कॉम्बो की मापी गई इन-प्रोसेस कॉस्ट 25,130 µs है। प्रति-कॉम्बो ग्रैन्युलैरिटी पर:

G  =  25,130 μs14 μs    1795G \;=\; \frac{25{,}130\ \mu\text{s}}{14\ \mu\text{s}} \;\approx\; 1795

प्रति-कॉम्बो कॉल्स फ़्लोर से ~1,795x ऊपर बैठती हैं — बाउंड्री प्रति कॉल एक प्रतिशत के दसवें हिस्से से बहुत कम दावा करती है। इसीलिए chatty आर्किटेक्चर ने भी केवल 107 ms खोए: इस वर्कलोड की ग्रैन्युलैरिटी पर, हर वह क्रॉसिंग पैटर्न जो डेटा दोबारा नहीं भेजता या टेक्स्ट नहीं बोलता, सुरक्षित रूप से एमॉर्टाइज़्ड है। कॉम्बो-लेवल, फोल्ड-लेवल, स्वीप-लेवल कॉल्स सभी सस्ते ज़ोन में गहरे हैं।

अब विपरीत छोर पर पलटते हैं। यह एक इलस्ट्रेटिव क्रॉस-वर्कलोड एक्सट्रापोलेशन है — हमारे स्वीप का वैरिएंट नहीं, बल्कि एक वर्कलोड फॉर्म जो असल दुनिया में वास्तव में मौजूद है: इंजन से प्रति बार परामर्श लिया जाता है। एक लाइव-स्टाइल प्रति-टिक इंजन सर्विस; एक gRPC-प्रति-बार सिग्नल स्ट्रीम; एक "स्ट्रैटेजी सर्वर" जिसे 150,000 बार्स में से हर एक के लिए एक बार पोल किया जाता है। इस कर्नेल में प्रति बार उपयोगी कंप्यूट 25,130 µs / 150,000 ≈ 0.17 µs है (व्युत्पन्न) — हर कॉल अपनी खुद की बाउंड्री कॉस्ट का लगभग 1/84 उपयोगी काम में ले जाएगी (व्युत्पन्न: 0.168 µs कंप्यूट के ऊपर 14.05 µs फ़्लोर)। कुल उतना ही खराब है जितना रेशियो सुनने में लगता है, बल्कि उससे भी बदतर:

150,000 कॉल्स×14 μs    2.1 s शुद्ध IPC150{,}000 \ \text{कॉल्स} \times 14\ \mu\text{s} \;\approx\; \mathbf{2.1\ s\ शुद्ध\ IPC}

पूरे 2.010 s इन-प्रोसेस जॉब से भी ज़्यादा, रिमोट इंजन के एक भी नंबर कंप्यूट करने से पहले खर्च हो गया, और यह 2.1 s रहता भले ही दूसरी तरफ का इंजन अनंत रूप से तेज़ हो (व्युत्पन्न: 150,000 × 14 µs)। इतनी फाइन ग्रैन्युलैरिटी पर कोई कंप्यूट एडवांटेज नहीं बचता। और याद रखें कि यह फ़्लोर एक होस्ट पर एक Unix सॉकेट है; उस प्रति-बार कॉल को नेटवर्क के पार एक सर्विस को करें, और फ़्लोर 150,000 कॉल्स पर दो से तीन ऑर्डर्स ऑफ़ मैग्नीट्यूड बढ़ जाता है।

The same-machine boundary floor as an implementation choice: a Python-over-Unix-socket round-trip at fourteen microseconds towering over a shared-memory ring crossing at thirty-nine nanoseconds, three orders of magnitude apart

एक और ईमानदार कैलिब्रेशन, क्योंकि 14 µs भी कोई भौतिकी का नियम नहीं है — यह हमारे ट्रांसपोर्ट की कीमत है: एक Python क्लाइंट, एक कर्नेल सॉकेट, दोनों दिशाओं में syscalls। एक विशेष रूप से बनाया गया सेम-मशीन ट्रांसपोर्ट बहुत नीचे जाता है। ZigBolt — HFT वर्कलोड्स के लिए हमारा ओपन-सोर्स Zig मैसेजिंग बस, इसी मशीन पर नेटिवली बेंचमार्क किया गया — लगभग 39 ns मीन में एक शेयर्ड-मेमोरी रिंग राउंड-ट्रिप करता है (64/256/1024-बाइट मैसेज पर 10/20/30 ns का वन-वे p50)। यह हमारे सॉकेट फ़्लोर से लगभग 360x नीचे है (व्युत्पन्न: 14.05 µs / 39 ns)। तुलना जानबूझकर सेब-बनाम-संतरा है, और हम इसे उसी तरह फ्लैग करते हैं: हमारा 14 µs एक Python-क्लाइंट सॉकेट राउंड-ट्रिप है, ZigBolt का 39 ns शेयर्ड मेमोरी पर नेटिव Zig है, इसलिए गैप ट्रांसपोर्ट और रनटाइम दोनों को मिलाता है। इसे दोनों के बीच दौड़ के रूप में नहीं, बल्कि वह रेंज जो सेम-मशीन फ़्लोर घेर सकता है: लगभग तीन ऑर्डर्स ऑफ़ मैग्नीट्यूड, इम्प्लीमेंटेशन द्वारा चुनी गई के रूप में पढ़ें। यह आधुनिक परिधान में पुराना Lightweight RPC सबक है (Bershad et al., 1990) — सेम-मशीन क्रॉसिंग्स प्रोटोकॉल मशीनरी द्वारा हावी होती हैं, और वे तब ढह जाती हैं जब ट्रांसपोर्ट सेम-मशीन केस के लिए बनाया गया हो। ऊपर की ब्रेक-ईवन अरिथमेटिक अपना आकार नहीं बदलती; हर्डल बस खिसक जाती है। 39 ns फ़्लोर पर, प्रति-बार ग्रैन्युलैरिटी भी इसे पार कर लेती (150,000 × 39 ns ≈ 5.9 ms, व्युत्पन्न) — जो बिल्कुल वैसा ही है जैसे HFT सिस्टम्स उन बाउंड्रीज़ को अफोर्ड कर सकते हैं जो एक REST सर्विस नहीं कर सकती।

यह पूरी ब्रेक-ईवन कहानी एक वाक्य में है: बाउंड्री इस बात की परवाह नहीं करती कि आपका इंजन कितना तेज़ है; यह प्रति क्रॉसिंग चार्ज करती है, इसलिए जो वेरिएबल्स आप नियंत्रित करते हैं वे हैं हर क्रॉसिंग कितना काम ले जाती है — और क्रॉसिंग किस चीज़ की बनी है। प्रति स्वीप बैच करें और GG एक लाख से ऊपर है। प्रति कॉम्बो बैच करें, G1795G \approx 1795 — फिर भी ठीक है। एक सॉकेट पर प्रति बार कॉल करें, G<1G < 1 — आर्किटेक्चर पहले ऑप्टिमाइज़ेशन से पहले ही मर चुका है, और इंजन की कोई भी दोबारा लिखाई, Rust में या किसी और चीज़ में, इसे पुनर्जीवित नहीं कर सकती।

1.13x वास्तव में कहाँ रहता है — और वर्डिक्ट

The 266-millisecond gap dissected: a sliver of two milliseconds labeled as the boundary next to a large slab of measured codegen difference between two scalar compiled kernels, with the folk belief crossed out

हेडलाइन गैप को ईमानदारी से विच्छेदित करने का समय है, क्योंकि यह स्टडी का सबसे काउंटरइंट्यूटिव फाइंडिंग ले जाता है।

batched Rust आर्किटेक्चर इन-प्रोसेस numba से 266 ms पीछे है (व्युत्पन्न: 2.276 − 2.010)। मापे गए बाउंड्री कंपोनेंट्स: ~2.0 ms पर एक फुल-पेलोड राउंड ट्रिप, 49 µs पर रॉ सीरियलाइज़ेशन, कुछ बाइट्स पर फ्रेम हेडर्स — पूरे बाउंड्री बिल को ~2 ms कहें। इसलिए गैप का 99% से ज़्यादा हिस्सा बाउंड्री बिल्कुल नहीं है। यह कंप्यूट है: IPC से अलग करके, Rust सर्वर वह स्वीप करने में ~2.274 s बिताता है जो numba 2.010 s में करता है — नैव Rust कर्नेल रॉ कंप्यूट में लगभग 13% धीमा है (व्युत्पन्न)।

यह एक बिना लाग-लपेट वाले पैराग्राफ का हकदार है, क्योंकि "इसे Rust में फिर से लिखो और यह तेज़ हो जाएगा" उतनी ही लोक-मान्यता है जितनी "IPC आपको मार डालेगा"। दोनों कर्नेल्स अंततः LLVM में जाते हैं — numba Python bytecode को उसके माध्यम से लोअर करता है, rustc MIR को उसके माध्यम से लोअर करता है — और दोनों संभवतः स्केलर लूप्स के रूप में चलते हैं: WMA का इनर सम एक फ्लोटिंग-पॉइंट रिडक्शन है, जिसे LLVM बिना fast-math रीएसोसिएशन लाइसेंस के ऑटो-वेक्टराइज़ नहीं करेगा जो numba के @njit डिफ़ॉल्ट्स नहीं देते और हमारा पोर्ट रिक्वेस्ट नहीं करता। तो ~13% दो स्केलर LLVM-कंपाइल्ड लूप्स के बीच एक मापा गया कोडजेन गैप है — और कारण दावा करने के बजाय, हमने स्पष्ट संदिग्ध को टेस्ट किया। स्वाभाविक संदिग्ध Rust की सुरक्षित इंडेक्सिंग है: हॉट WMA लूप हर array access पर बाउंड्स-चेक करता है, जबकि numba का @njit बाउंड्स चेकिंग बंद करके कंपाइल होता है। तो हमने उसी कर्नेल का एक इक्विवैलेंस-वेरिफाइड वैरिएंट get_unchecked का उपयोग करके बनाया — हॉट पाथ में कहीं भी कोई बाउंड्स चेक नहीं — और इसे पाँचवें आर्किटेक्चर के रूप में मापा। इसने गैप को बंद नहीं किया: 2.337 s (1.16x), बाउंड्स-चेक्ड बिल्ड के 2.276 s से मामूली रूप से धीमा। परिकल्पना टेस्ट की गई, परिकल्पना खारिज की गई। ज्ञान की ईमानदार स्थिति: ~13% वास्तविक और रिप्रोड्यूसिबल है (10 रन का मीडियन, ~2% के भीतर स्प्रेड), और वर्तमान में अनएट्रिब्यूटेड — एलोकेशन बिहेवियर, लूप स्ट्रक्चर, या इंस्ट्रक्शन शेड्यूलिंग में कोई अंतर जिसे केवल असेंबली-लेवल प्रोफाइलिंग ही सुलझा पाएगी। सबक बरकरार रहता है: नैव Rust ऑटोमैटिकली अच्छे numba से तेज़ नहीं है, और एक भाषा बाउंड्री जो एक मुफ़्त कंप्यूट जीत की धारणा पर खरीदी गई हो वह एक कंप्यूट नुकसान के साथ आ सकती है। एक ट्यून्ड Rust कर्नेल — प्रीअलोकेटेड बफर्स, एक्स्प्लिसिट SIMD, कॉम्बोज़ में थ्रेड्स — अभी भी साइन पलट सकता है। लेकिन वह एक कंप्यूट सवाल है, जिसे प्रोफाइलिंग और कर्नेल वर्क से सुलझाया जाना है, और इस स्टडी का सवाल बाउंड्री है। बाउंड्री का जवाब: एक बार पार की गई, बाइट्स में, इसकी कीमत ~0.1% है।

तो चलिए पूरा वर्डिक्ट असेंबल करते हैं, ऊपर मापी गई हर क्लॉज़।

एक क्रॉस-लैंग्वेज इंजन सर्विस जीतती है जब यह सब सच हो:

  • कंप्यूट एडवांटेज असली है — आपके कर्नेल पर मापा गया, भाषा की प्रतिष्ठा से नहीं मान लिया गया। (हमारा साबित होने तक −13% था — और उस कमी के लिए पहली "स्पष्ट" व्याख्या टेस्टिंग में मर गई।)
  • आप कोर्सली पार करते हैं — प्रति स्वीप या प्रति फोल्ड एक कॉल, 14 µs फ़्लोर के हज़ारों गुने ऊपर, जैसा batch आर्किटेक्चर का कुल 1.13x (~0.1% बाउंड्री) दिखाता है।
  • आप बाइनरी बोलते हैं — लेंथ-प्रीफिक्स्ड रॉ अरेज़, Arrow, memcpy-क्लास कुछ भी जो 1.2 MB में 49 µs पर हो; कभी टेक्स्ट नहीं जो 66,243 µs पर हो।
  • डेटा प्रीलोडेड है — एक स्टेटफुल सर्वर मेगाबाइट्स दोबारा भेजने के बजाय इको कर्व के ~16 µs सिरे पर सिर्फ-पैरामीटर कॉल्स लेता है।

यह हारती है जब उसे उस तरह डिप्लॉय किया जाए जैसे इंजन सर्विसेज़ आमतौर पर होती हैं:

  • एक JSON/REST माइक्रोसर्विस — हर कॉल पर 1348x सीरियलाइज़ेशन टैक्स चुकाती है, दोनों दिशाओं में; chatty ग्रैन्युलैरिटी के तहत यह 2 s के जॉब पर 5.3 s की एनकोडिंग है।
  • प्रति यूनिट-ऑफ़-वर्क RPC — प्रति कॉम्बो यहाँ 107 ms खर्च होते हैं और सिर्फ इसलिए बचता है क्योंकि हर कॉल 25,130 µs कंप्यूट ले जाता है; प्रति बार यह किसी भी काम से पहले ~2.1 s शुद्ध IPC है, 2.0 s के जॉब पर।
  • प्रति कॉल एक स्पॉन — हर बार ~24 ms फिक्स्ड कॉस्ट, प्रति स्वीप एक बार हानिरहित, प्रति कॉम्बो चुकाने पर लगभग दो सेकंड।

यानी: जो आर्किटेक्चर विफल होते हैं वे विदेशी नहीं हैं। JSON REST इंजन, प्रति-सिंबल सबप्रोसेस, gRPC-प्रति-टिक — यह इस बात की एक निष्पक्ष जनगणना है कि "आइए बैकटेस्ट इंजन को फैक्टर आउट करें" वास्तव में कैसे बनाया जाता है। लोक-मान्यता सामान्य प्रैक्टिस के विवरण के रूप में अनुभवजन्य रूप से अच्छी तरह स्थापित है और प्रकृति के नियम के रूप में अनुभवजन्य रूप से गलत है। बाउंड्री कभी समस्या नहीं थी। इसे पार करने के डिफ़ॉल्ट तरीके हैं।

बाउंड्री के पक्ष में एक तर्क अपने खुद के वाक्य का हकदार है, क्योंकि यही कारण है कि हमने यह स्टडी बिल्कुल की। एक अच्छी तरह से डिज़ाइन की गई बाउंड्री के पीछे एक सिंगल कंपाइल्ड कर्नेल रिसर्च स्वीप और लाइव ट्रेडिंग लूप दोनों की सेवा कर सकता है — वही बाइनरी, वही अरिथमेटिक, बिट-दर-बिट। हमारी बैकटेस्ट-लाइव पैरिटी स्टडी ने कैटलॉग किया कि कैसे रिसर्च और प्रोडक्शन इंजन अलग हो जाते हैं जब वे दो कोडबेस होते हैं; एक इंजन सर्विस उस ड्रिफ्ट के लिए सबसे मजबूत संरचनात्मक इलाज है, और यह स्टडी उस इलाज की कीमत ईमानदारी से लगाती है: सही ढंग से किया जाए तो, वॉल टाइम का लगभग 0.1% और एक इक्विवैलेंस गेट यह साबित करने के लिए कि ट्रांसलेशन में कुछ भी नहीं बदला। वह ट्रेड — एक डेडिकेटेड प्रोसेस बाउंड्री के बदले वन-कर्नेल पैरिटी — इन नंबरों पर, एक सौदा है। गलत ढंग से किया जाए, तो वही आइडिया प्रोडक्शन में एक 1348x सीरियलाइज़ेशन टैक्स भेजता है, जिसके ऊपर आपका PnL सवार होता है।

मुख्य बातें

  1. बाउंड्री लगभग मुफ़्त है; लोक-मान्यता मापन में विफल हो जाती है। पूरी 1.2 MB क्लोज़ सीरीज़ को एक Unix सॉकेट के जरिए राउंड-ट्रिप करने में — पूर्ण पार्स और री-एनकोड सहित — 2,043.4 µs लगते हैं, 2.010 s के जॉब का लगभग 0.1% (व्युत्पन्न)। batched Rust-ओवर-सॉकेट आर्किटेक्चर कुल मिलाकर 1.13x पर आती है, और उस गैप का भी ~99% IPC नहीं है।
  2. "इसे Rust में फिर से लिखो" एक कंप्यूट दावा है — बाउंड्री खरीदने से पहले इसे वेरिफाई करें। हमारा लाइन-दर-लाइन Rust पोर्ट numba कर्नेल से ~13% धीमा कंप्यूट करता है (व्युत्पन्न: 2.274 s बनाम 2.010 s) — दो स्केलर LLVM-कंपाइल्ड लूप्स के बीच एक रिप्रोड्यूसिबल कोडजेन गैप जो अनएट्रिब्यूटेड रहता है: हमने स्पष्ट संदिग्ध को टेस्ट किया और खारिज किया, क्योंकि बिना बाउंड्स चेक्स के एक इक्विवैलेंस-वेरिफाइड get_unchecked बिल्ड तेज़ नहीं निकला (2.337 s बनाम 2.276 s)। नैव Rust ऑटोमैटिकली तेज़ नहीं है; एक ट्यून्ड कर्नेल शायद हो — मापें, फिर तय करें।
  3. असली टैक्स टेक्स्ट है। 150,000 फ्लोट्स को JSON के रूप में एनकोड करने में 66,243 µs लगते हैं बनाम रॉ में 49.1 µs — 1348x, प्रति दिशा, प्रति कॉल, दोनों साइड्स पर चुकाया गया। एक chatty JSON डिप्लॉयमेंट 2 s के जॉब पर 5.3 s एनकोडिंग जलाता है (व्युत्पन्न)। बाउंड्रीज़ के आर-पार बाइनरी बोलें: रॉ फ्रेम्स, Arrow — प्राइस अरे पर कभी json.dumps नहीं।
  4. Chatty बनाम chunky मापने योग्य है, और स्टेटलेसनेस दोषी है। प्रति-कॉम्बो कॉल्स जो डेटा दोबारा भेजते हैं: batch के 1.13x के मुकाबले 1.19x (+107 ms, व्युत्पन्न; इको कर्व की ~81 ms की वन-वे भविष्यवाणी इससे ~25% नीचे बैठती है, बाकी प्रति-कॉल फ्रेमिंग है)। एक प्रीलोडेड स्टेटफुल सर्वर वही 80 कॉल्स ~16 µs प्रत्येक पर लेगा — कुल लगभग 1.3 ms (इको फ़्लोर से व्युत्पन्न)। पैरामीटर भेजें, डेटासेट नहीं।
  5. फ़्लोर का सम्मान करें — और जानें कि फ़्लोर एक चुनाव है। हमारी Python-ओवर-Unix-सॉकेट क्रॉसिंग 14 µs पर फ़्लोर करती है; प्रति-कॉम्बो ग्रैन्युलैरिटी इसे ~1,795x पार करती है (प्रति कॉल 25,130 µs कंप्यूट) — सुरक्षित। एक प्रति-बार पैटर्न (एक इलस्ट्रेटिव क्रॉस-वर्कलोड एक्सट्रीम: एक लाइव प्रति-टिक इंजन, यह स्वीप नहीं) 150,000 × 14 µs ≈ 2.1 s शुद्ध IPC चुकाएगा 2.0 s के जॉब पर (व्युत्पन्न) — एक अनंत रूप से तेज़ इंजन के साथ भी आगमन पर मृत। प्रति कॉल स्पॉन करना फिक्स्ड ~24 ms जोड़ता है (व्युत्पन्न)। और ZigBolt जैसा एक विशेष रूप से बनाया गया शेयर्ड-मेमोरी ट्रांसपोर्ट इस मशीन पर नेटिवली ~39 ns में राउंड-ट्रिप करता है — हमारे सॉकेट फ़्लोर से ~360x नीचे (व्युत्पन्न; नेटिव Zig बनाम एक Python क्लाइंट, इसलिए इसे उस रेंज के रूप में पढ़ें जो फ़्लोर घेर सकता है, दौड़ के रूप में नहीं)।
  6. एक बार पार करें, बाइट्स में, डेटा पहले से मौजूद होने के साथ — और बाउंड्री आपको ~0.1% के लिए पैरिटी खरीदती है। एक कर्नेल जो रिसर्च और लाइव दोनों की सेवा करता है, एक इक्विवैलेंस चेक द्वारा गेटेड (PnL −5165.58, 57,029 ट्रेड्स, भाषाओं में और दोनों Rust बिल्ड्स में समान), इंजन सर्विस के लिए ईमानदार केस है। बेईमान केस — JSON, chatty, स्पॉन-प्रति-कॉल — वे हैं जिन्होंने IPC को उसकी प्रतिष्ठा दी।

पूरा प्रयोग — Rust इंजन, वायर प्रोटोकॉल, इको और सीरियलाइज़ेशन हार्नेस, इक्विवैलेंस गेट, और इस लेख की हर संख्या जो एक डिटरमिनिस्टिक स्क्रिप्ट से फिर से बनाई जा सकती है — साथी पेपर में है ipc-tax.marketmaker.cc पर, कोड और डेटा के साथ github.com/suenot/ipc-tax पर।

सॉकेट कभी समस्या नहीं थी। पूरे डेटासेट के लिए दो मिलीसेकंड, राउंड ट्रिप — लोककथा तीन ऑर्डर्स ऑफ़ मैग्नीट्यूड से गलत थी, और एक साथ दोनों दिशाओं में: बाइट्स के बारे में बहुत निराशावादी, टेक्स्ट के बारे में बहुत उदार। बाउंड्री को इस तरह पार करें जैसे उसकी कोई कीमत हो, और वह नहीं होगी।

blog.disclaimer

Authors

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

बाज़ार से आगे रहें

AI ट्रेडिंग इनसाइट्स, मार्केट एनालिसिस और प्लेटफ़ॉर्म अपडेट के लिए हमारे न्यूज़लेटर को सब्सक्राइब करें।

हम आपकी गोपनीयता का सम्मान करते हैं। किसी भी समय अनसब्सक्राइब करें।