← Maqolalarga qaytish
June 30, 2026
5 daqiqa o'qish

IPC solig'i: backtest dvigatelini soket ortiga qo'ying va 13% yo'qoting — bu deyarli hech narsasi soketga tegishli emas

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

"Illyuziyalarsiz backtestlar" seriyasining bir qismi.

📄 Bu maqola tadqiqot maqolasiga aylandi. Yo'lga bog'liq backtest yadrosi numba dan Rust tiliga qatorma-qator ko'chirildi va jarayon/til chegarasi orqali to'rt xil usulda chaqirildi, kombinatsiya boshiga bir xil PnL ni tasdiqlovchi ekvivalentlik darvozasi bilan — shuningdek sof IPC kechikish egri chizig'i, serializatsiya solig'i va spawn narxining alohida o'lchovlari bilan. Maqolani onlayn o'qing (interaktiv versiya + PDF) ipc-tax.marketmaker.cc saytida, kod va ma'lumotlar github.com/suenot/ipc-tax manzilida.

Tez bo'lib qolgan har qanday backtest dvigateli oxir-oqibat bir xil suhbatni keltirib chiqaradi. Bizniki o'z vaqtida keldi. Tezlik zinapoyasi endigina 80 kombinatsiyali parametr skanerlashni pandas ning 69,9 soniyasidan bir oqimli numba ning taxminan 2 soniyasigacha tushirgan edi, va keyingi tabiiy istak: nega Python JIT da to'xtash kerak? Yadroni Rust da qayta yozish. Uni to'liq qiymatli dvigatel xizmatiga aylantirish — soket ortida turgan kompilyatsiya qilingan ikkilik fayl, har bir tadqiqot skriptidan, har bir tildan va hatto jonli treyderdan ham chaqiriladigan. Bitta yadro, bitta haqiqat, dublikat mantiq yo'q.

Va keyin qarshi dalil ham o'z vaqtida keladi: jarayondan chiqqan zahoti IPC sizni yeb qo'yadi. Ma'lumotlar serializatsiya qilinishi kerak, chegara orqali yuborilishi kerak, deserializatsiya qilinishi kerak; har bir chaqiruv syscall va kontekst almashtirishlar uchun to'laydi; sizning chiroyli Rust yadroingiz butun umrini quvur kutish bilan o'tkazadi. Jarayonda qoling. Buni hamma biladi.

Bu maqola hamma bilaman deb o'ylagan narsani o'lchaydi, va o'lchov bahsning har ikki tomonidan ham qiziqarliroq. Xalq e'tiqodi — "tezroq tillar aro dvigatel jarayon ichidagi numba dan yutqazadi, chunki IPC sizni o'ldiradi" — umuman noto'g'ri va faqat maxsus shartlarda to'g'ri ekanligi ma'lum bo'ldi. Chegarani bir marta, xom baytlarda kesib o'tish ikki soniyalik vazifada taxminan 2 millisekund turadi: yaxlitlash xatosi. Soliq chegarada emas. U — uni qanday kesib o'tishingizda — va dvigatel xizmatlarining tabiiy sharoitda odatda joylashtiriladigan uch usuli (JSON API, ish birligi boshiga chaqiruv, chaqiruv boshiga jarayon spawn) har biri, o'lchanadigan tarzda, folklor bashorat qiladigan falokatning bir qismi.

Mana butun tajriba oldindan. Quyidagilarning barchasi har bir qatorning anatomiyasi.

Arxitektura Skanerlash boshiga chegara orqali nima o'tadi Devor vaqti jarayon ichidagiga nisbatan
jarayon ichidagi numba hech narsa — to'g'ridan-to'g'ri chaqiruv 2,010 s 1,00x
Rust serveri, batched (Unix soketi) bitta borish-kelish: butun seriya + barcha 80 parametr to'plami 2,276 s 1,13x
Rust serveri, batched, get_unchecked yadrosi xuddi shu yagona borish-kelish — chegara tekshiruvisiz yadro varianti (bahoni ko'ring) 2,337 s 1,16x
Rust serveri, gaplashuvchi (Unix soketi) 80 borish-kelish: seriya kombinatsiya boshiga qayta yuboriladi 2,383 s 1,19x
Rust spawn (stdin/stdout) jarayon spawn + bitta quvur orqali yuborilgan so'rov 2,300 s 1,14x

Apple M2 Max, Python 3.14.6, numpy 2.4.3, numba 0.64.0, rustc 1.94.0 (relizga qurish, tashqi crate yo'q). 150,000 bar × 80 kombinatsiya, 0,09% borish-kelish to'lovi, seed 42; yopilish seriyasi simda 1,200,000 bayt (1,2 MB) tashkil qiladi. Arxitektura boshiga 10 ishga tushirishning medianasi; min-maks diapazonlari ~2% chegarasida qoladi. Barcha beshtasi bir xil HMA/HMA3 stop-and-reverse skanerlashni bajaradi, va ekvivalentlik darvozasi ikkala Rust yadro variantining ham kombinatsiya boshiga (PnL, savdo soni) natijalari numba ga aniq mos kelishini tasdiqlaydi — barmoq izi PnL −5165,58, 57,029 savdo bo'yicha, tezlik zinapoyasi tadqiqotining numba yadrosi bilan xuddi shu seed bo'yicha bayt-baytga bir xil. Biz chegaralarni taqqoslayapmiz, amalga oshirishlarni emas.

Batched qatorini diqqat bilan o'qing, chunki u butun tezisni ko'taradi. Rust-soket-orqali arxitekturasi jarayon ichidagi numba dan 1,13x sekin — to'liq skanerlashda 266 ms orqada qoladi (hosilaviy: 2,276 − 2,010). Xalq hikoyasi bu millisekundlar IPC ekanligini aytadi. Bunday emas. O'sha farqning taxminan 2 ms chegara hisoblanadi — butun 1,2 MB yopilish seriyasi yuborilib, natijalar qaytarilib, to'g'ridan-to'g'ri o'lchangan. Qolgan ~264 ms bizning oddiy Rust yadromiz skanerlashni numba yadrosidan shunchaki taxminan 13% sekin hisoblashi bilan bog'liq (hosilaviy: 2,276 s dan ~2 ms chegarani ayirib ≈ 2,274 s Rust hisoblash, numba uchun 2,010 s bilan solishtirganda). Til sifatida Rust til sifatida Python dan yutqazmadi; bitta skalyar LLVM-kompilyatsiya qilingan sikl boshqasiga qarshi kod generatsiyasi poygasida yutqazdi — va biz yo'qotishni aniq gumon qilinuvchiga ham bog'liy olmadik: xuddi shu yadroning chegara tekshiruvisiz get_unchecked qurilishi tezroq bo'lib chiqmadi (2,337 s; baho bo'limi buni tahlil qiladi). Soketning bularning barchasiga aloqasi deyarli yo'q edi.

Ushbu gapning ikkala yarmini ham ushlab turing. Chegara to'g'ri kesib o'tilganda deyarli bepul — va "buni Rust da qayta yoz" sizga joylashtirish chegarasini sotib oladi, avtomatik hisoblash g'alabasini emas. Ikkala fakt ham ommabop intuitsiyaga zid keladi, va ikkalasi ham jadvalda.

Bitta yadro, ikki til, to'rtta chegara

Ish yuklamasi ataylab tezlik zinapoyasi belgilagan bilan bir xil, shunday qilib ikkita tadqiqot bir-biriga bog'lanadi. Yadro — HMA/HMA3 kesishishi — ikkita Hull uslubidagi harakatlanuvchi o'rtachalar bo'yicha stop-and-reverse tizimi, parametr kombinatsiyasi boshiga yettita og'irlikli-harakatlanuvchi-o'rtacha o'tishi, shuningdek pozitsiyani olib yuruvchi, har bir kesishishda 0,09% borish-kelish to'lovini ayirib PnL yozadigan va teskari buriladigan holatli bar-bar hodisa siklidan iborat. Ma'lumotlar — 150,000 bar seed orqali yaratilgan sintetik geometrik broun harakati (seed=42); to'r — [6,200][6, 200] oralig'iga tarqalgan 80 HMA uzunligi. Jarayon ichidagi etalon — zinapoyaning bir oqimli numba pog'onasi, ushbu tadqiqot uchun qayta o'lchangan: u yerda 1,98 s, bu yerda 2,010 s — xuddi shu yadro, xuddi shu mashina, ishonarli darajada zerikarli.

Tillar aro dvigatel o'sha numba yadrosining Rust tiliga qatorma-qator ko'chirilishi — xuddi shu sikllar, xuddi shu NaN ishlov berish, xuddi shu to'lov arifmetikasi — tashqi crate lar yo'q holda reliz rejimida kompilyatsiya qilingan, shuning uchun butun tajriba bog'liqliksiz va qayta yaratiluvchi bo'lib qoladi. U ataylab minimal ikkilik protokolda gaplashadi: har bir yo'nalishda uzunlik prefiksli freym, hammasi 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

echo opkodi tadqiqotning skalpeli: hech narsa hisoblamaydigan, boshqariluvchi hajmdagi borish-kelish, shunday qilib sof chegara narxini alohida o'lchash mumkin — serializatsiya, syscall, soket tranziti, deserializatsiya, va boshqa hech narsa.

Beshta o'lchangan arxitektura — to'rtta chegara namunasi va bitta yadro varianti:

  • in_process — numba yadrosini to'g'ridan-to'g'ri chaqirish. Chegara yo'q. Etalon.
  • rust_batch_unix — Unix domen soketidagi doimiy Rust serveri. Bitta borish-kelish butun yopilish seriyasini va barcha 80 parametr to'plamini yuboradi; Rust har bir kombinatsiyani hisoblaydi; bitta javob qaytadi. Og'ir chaqiruv.
  • rust_batch_unchecked — xuddi shu batched chegara, lekin yadro get_unchecked orqali indekslaydi (issiq yo'lda chegara tekshiruvi yo'q). Hisoblash farqi haqida ma'lum bir gipotezani tekshirish uchun mavjud; baho bo'limi buni sarflaydi.
  • rust_chatty_unix — xuddi shu server, lekin kombinatsiya boshiga bitta borish-kelish, 1,2 MB seriya har safar qayta yuboriladi. Oddiy RPC-ish-birligi-boshiga arxitekturasi.
  • rust_spawn_stdin — ikkilik faylni skanerlash boshiga spawn qilish va so'rovni stdin orqali yuborish. "CLI dvigatelini tashqariga chaqirish" namunasi; jarayon yaratish narxini to'laydi.

Va ekvivalentlik darvozasi, busiz bularning hech biri ma'noga ega bo'lmaydi: vaqt o'lchagandan keyin, har bir Rust variantining kombinatsiya boshiga (PnL, savdo soni) vektori numba bilan solishtiriladi — savdo soni aniq, PnL mutlaq 10610^{-6} gacha. Topshirilgan ishga tushirish ikkala qurilish uchun ham — xavfsiz indekslash va get_uncheckedall_ok: true xabar beradi. Birinchi kombinatsiyaning barmoq izi — PnL −5165,58 foiz punkt, 57,029 savdo bo'yicha — tezlik zinapoyasi tadqiqotining numba yadrosi bilan raqam-raqamga mos keladi, bu ikkala maqolani ham xuddi shu seed bo'yicha bitta yadroga bog'laydi. Tillar aro portlar aynan jimgina farq yashashni yaxshi ko'radigan joy (foizga aylantirishdan oldin emas, keyin qo'llaniladigan to'lov, boshqacha tarmoqlanadigan NaN solishtirish, oynada bitta-bitta xato — bizning look-ahead noto'g'ri xulosalar taksonomiyasi shovqindan 15 Sharpe yasay olishini ko'rsatgan xuddi shu turdagi xato). Turli narsalarni hisoblaydigan ikkita dvigatelning benchmark testi benchmark test emas; bu ikkita bog'liq bo'lmagan dasturning poygasidir.

Ekvivalentlik o'rnatilgandan so'ng, yuqoridagi jadvaldagi har bir farq — chegara va hisoblash — boshqa hech narsa emas.

Kesib o'tish haqiqatan qancha turadi: echo egri chizig'i

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

Skalpeldan boshlaylik. Echo operatsiyasi nn floatdan iborat yukni Rust serveri orqali borish-kelishga yuboradi — Python freymni quradi, server barcha nn floatni tahlil qiladi, ularni qayta kodlaydi va qaytarib yuboradi. Ikkala yo'nalish ham serializatsiya, syscall va soket tranzitiga to'laydi. Mana o'lchangan egri chiziq (10 ishga tushirishning medianasi):

Yuk (float) Yo'nalish boshiga bayt Borish-kelish
1 8 14,1 mks
100 800 16,4 mks
1,000 8,000 18,1 mks
10,000 80,000 192,5 mks
100,000 800,000 1,367,3 mks
150,000 1,200,000 2,043,4 mks

Bu jadvalda ikkita tarkibiy fakt yashaydi.

Birinchidan, tag. Mohiyatan hech narsa olib yurmaydigan borish-kelish — 8 bayt — 14 mks turadi. Bu ushbu transport orqali umuman chaqiruv qilishning mustaqil narxi: ikkita write syscall, ikkita read syscall, yadro soket mexanizmi, rejalashtiruvchining uyg'onishi. Egri chiziqning chapda qanchalik tekis ekaniga e'tibor bering: 1 floatdan 1,000 floatgacha narx deyarli qimirlamaydi (14,1 → 18,1 mks). Taxminan 8 KB dan past siz chaqiruv uchun to'laysiz, baytlar uchun emas. Bu raqam — kechikish tagi — butun tadqiqotning eng muhim doimiysi, va biz quyida muvozanat nuqtasi arifmetikasini shunga asoslaymiz.

Ikkinchidan, qiya. ~10,000 floatdan keyin egri chiziq o'tkazuvchanlik kengligiga bog'liq va taxminan chiziqli bo'lib qoladi. To'liq 1,2 MB seriya — jami 2,4 MB harakatlantirildi, borib-kelib, shu jumladan Rust tomonida 150,000 floatning to'liq tahlili va qayta kodlanishi — 2,043,4 mks turadi. Bu butun oddiy stek orqali samarali ~1,2 GB/s beradi (hosilaviy: 2,4 MB / 2,04 ms) — uzunlik prefiksli freymlar va bayt-baytlik float tahlilchisi bo'lgan Unix domen soketi, hech qanday zero-copy hiylalari yo'q, umumiy xotira yo'q, hech qanday ayyorlik yo'q.

Ikkala o'lchangan doimiy bilan bitta kesib o'tishning oqilona modeli:

Tcall(b)    14 μstag  +  2b1,2 GB/syuk, ikkala yo’nalishT_{\text{call}}(b) \;\approx\; \underbrace{14\ \mu\text{s}}_{\text{tag}} \;+\; \underbrace{\frac{2b}{1,2\ \text{GB/s}}}_{\text{yuk, ikkala yo'nalish}}

Endi bosh sarlavha raqamini kontekstga qo'yaylik. To'liq skanerlash jarayon ichida 2,010 s oladi. Uning butun ma'lumotlar to'plamini chegara orqali va qaytadan yuborish ~2,0 ms turadi — vazifaning taxminan 0,1% (hosilaviy: 2,0434 ms / 2,010 s). Agar siz bir marta, xom baytlarda kesib o'tsangiz, chegara yaxlitlash xatosi hisoblanadi. Bu xalq e'tiqodining birinchi o'ladigan yarmi: qo'rquv hech qachon shunday arzon narsaga tegishli bo'lmagan edi.

O'sha kesib o'tishning Rust tomoni tizim kodi kabi yorqin emas — engine/src/main.rs dan moslashtirilgan:

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());
}

Davom etishdan oldin doira haqida halol eslatma: bu tadqiqotdagi barcha chegara raqamlari bitta xostdagi Unix domen soketidan. Dvigatel shuningdek TCP da ham gaplashadi (TCP_NODELAY bilan), lekin biz buni o'lchamadik; loopback TCP bu taglardan biroz yuqorida joylashadi, va haqiqiy tarmoq sakrashi butunlay boshqa rejim — millisekund tag, mikrosekund emas. Shuning uchun bu yerdagi hammasi chegarani shu tarzda kesib o'tishning eng yaxshi mumkin bo'lgan holati. Bu keyin o'lchanadigan soliqlarni yanada ayblanuvchi qiladi: ular siz shu ustiga tanlov bo'yicha to'laydigan narsa.

Serializatsiya solig'i: JSON tanlaganingiz uchun 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

Mana, "IPC yuklamasi" haqidagi xalq e'tiqodi noto'g'ri yorliqlash bo'lib chiqadigan joy. Biz xuddi shu 150,000-float yopilish seriyasini kodlash narxini uch usulda o'lchadik — aynan yuqoridagi har bir arxitektura yuboradigan yuk:

Kodlash 1,2 MB floatni kodlash vaqti xomga nisbatan
xom baytlar (.tobytes()) 49,1 mks 1,0x
pickle 29,8 mks 0,6x
JSON (json.dumps(close.tolist())) 66,243 mks 1348x

Xom yo'l — funksiya chaqiruvi kiyingan 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 hatto bizning xom yo'limizdan biroz arzonroq chiqadi, chunki astype dtype allaqachon mos kelsa ham dtype aylantirish nusxasini to'laydi; ikkalasi ham memcpy sinfiga kiradi va ikkalasi ham yaxlitlash xatolari. Ikkilik oila umuman matn oilasidan uch tartib pastroqda yashaydi.)

Matn yo'li esa deyarli har bir "dvigatelni mikroservisga aylantiraylik" joylashtirishi haqiqatda yuboradigan narsa:

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

Oltmish olti millisekund. Faqat kodlash uchun. json.dumps(close.tolist()) har bir floatni Python obyektiga o'raydi, keyin har birini o'nlik matn sifatida chiqaradi — xom yo'l bitta blok nusxasini qilgan joyda 150,000 to'p ajratish va 150,000 float-satrga aylantirish. Va simdagi yuk ham shishadi (bitta float64 ikkilikda 8 bayt turadi va o'nlik matn sifatida taxminan ikki-uch barobar — biz hatto qo'shimcha tranzit uchun ham hisoblamadik).

Endi buni haqiqiy joylashtirish qilgandek masshtablaylik. O'sha 66 ms — bitta kodlash, bitta tomon, bitta chaqiruv. JSON xizmati kodlash va dekodlashning ikkalasini ham, chegaraning ikkala tomonida, har bir chaqiruvda to'laydi. JSON orqali bitta batched chaqiruv butun skanerlashning hisoblash byudjetining ~3,3% ini faqat mijoz tomonidagi kodlashga sarflaydi (hosilaviy: 66 ms / 2,010 s). JSON ni gaplashuvchi arxitektura ostiga qo'ying — kombinatsiya boshiga bitta chaqiruv, quyidagi namuna — va mijoz tomonidagi kodlash yolg'iz 80 × 66 ms = 5,3 s turadi: bitta bayt ham qimirlamasdan va server hech narsani tahlil qilmasdan oldin butun foydali ishning ikki yarim barobaridan ko'prog'i (hosilaviy).

Bu ko'plab jamoalar bilmasdan ishlab chiqarishda o'lchagan haqiqiy "IPC solig'i". Bu hech qachon jarayonlararo aloqa bo'lmagan. Bu raqamli massivlarning matn serializatsiyasi edi — chegaraning eng arzon tarkibiy qismiga o'z-o'ziga qilingan 1348x. Ustunli dunyo bu darsni yillar oldin o'rgangan, va bu bizning Polars va pandas tadqiqoti ma'lumotlar quvuri tomonidan doimo duch kelgan xuddi shu dars. Arrow kabi formatlar aynan shu sababdan mavjud — massiv ma'lumotlari jarayon va til chegaralarini matn sifatida emas, xom ustunli baytlar sifatida kesib o'ta olishi uchun. Agar sizning dvigatel xizmatingiz narx massivlari uchun JSON gaplashsa, hech qanday soket sozlash sizni qutqara olmaydi — protokolning o'zi tor joy.

Gaplashuvchi va ommaviy: Faulerning qonuni, o'lchangan

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

Martin Faulerning Taqsimlangan obyekt loyihalashning birinchi qonuni — "obyektlaringizni tarqatmang" — u xuddi shu nafasda aytgan xulosa bilan keladi: agar chegarani kesib o'tishingiz kerak bo'lsa, interfeys yirik donador bo'lishi kerak, chunki masofaviy chaqiruv mahalliy chaqiruvdan tartib darajasida qimmatroq turadi. Har bir taqsimlangan tizimlar faxriysi rozi bo'lib bosh irg'aydi. Deyarli hech kimda o'z ish yuklamasi uchun raqam yo'q. Mana bizniki.

Ommaviy va gaplashuvchi arxitekturalar xuddi shu server, xuddi shu protokol, xuddi shu ma'lumotlarda ishlaydi — faqat chaqiruv donadorligi farq qiladi:

srv.call(0, close, params)

[srv.call(0, close, [params[k]]) for k in range(n)]

Ommaviy: 2,276 s (1,13x). Gaplashuvchi: 2,383 s (1,19x) — 107 ms sekinroq (hosilaviy: 2,383 − 2,276). Bu delta nima ekanligi va nima emasligi haqida aniq bo'lish uchun: echo egri chizig'i unga oddiy bashorat beradi — to'liq seriyani har biri taxminan 2,043 mks to'liq-yuk borish-kelishining yarmida qo'shimcha 79 marta yuborish, taxminan 81 ms — bu o'lchangan 107 ms dan taxminan 25% past tushadi; qolgani Python tomonidagi chaqiruv boshiga so'rov qurish va freymlash, buni echo bashorati o'z ichiga olmaydi. Har holda, bu qo'shimcha kesib o'tish boshiga ~1,4 ms ga to'g'ri keladi (hosilaviy: 107 / 79); javoblar ahamiyatsiz — kombinatsiya boshiga 16 bayt.

O'sha 107 ms ning ikkita talqini bor, va ikkalasi ham muhim.

Yumshoq talqin: bu devor vaqtining faqat ~4,5% i, falokat emas. To'g'ri — va folklorning falokati bu yerda nega ro'y bermaganini tushunish muhim. Har bir gaplashuvchi chaqiruv hali ham 25,130 mks haqiqiy hisoblash olib yuradi (bitta kombinatsiyaning qiymati — jarayon ichidagi kombinatsiya boshiga o'lchangan qiymat), shuning uchun chaqiruv boshiga ~1,4 ms chegara yuklamasi chaqiruv boshiga ishdan bir tartib past qoladi. Gaplashuvchi arxitekturalar har bir chaqiruv haqiqatan og'ir bo'lganda halokatli emas. Ular donadorlik kichraygan sari halokatli bo'ladi — bu muvozanat nuqtasi bo'limining butun mavzusi.

Ayblovchi talqin: bu soliq butunlay ixtiyoriy edi, va u chaqiruv soni × yuk bilan masshtablanadi. Gaplashuvchi namuna ma'lumotlar to'plamini har bir chaqiruvda faqat bitta sababga ko'ra qayta yuboradi: xizmat holatsiz, shuning uchun har bir so'rov butun kontekstni olib yurishi kerak. Bu oddiy "skanerlash so'nggi nuqtasi"ning standart shakli — va deyarli har bir oq taxtada eskiz qilingan REST mikroservisining. Holatli server — seriyani bir marta yuklash, keyin 48 baytli parametr freymlarini yuborish — har bir kombinatsiya boshiga chaqiruvni echo egri chizig'ining kichik-yuk uchiga yaqin joylashtirar edi: chaqiruv boshiga taxminan 16 mks, barcha 80 tasi uchun taxminan 1,3 ms (echo tagidan hosilaviy; analitik, alohida o'lchanmagan). Gaplashuvchi jarima kichraymaydi; u yo'qoladi. Dars aniq: muammo ko'p chaqiruv qilishda emas — u protokol har bir chaqiruvni birinchisi deb ko'rsatgani uchun holatni qayta yuborishda.

Ma'lumotlarni oldindan yuklang. Parametrlarni yuboring. Chegarani har safar chamadoningizda butun dunyo bilan emas, niyat bilan kesib o'ting.

Spawn narxi: dvigatelni chaqiruv boshiga ijaraga olish

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

Uchinchi joylashtirish namunasi eng eskisi: umuman server yo'q. Dvigatel ikkilik faylini spawn qilish, so'rovni stdin orqali yuborish, javobni stdout dan o'qish, uni o'lishga qoldirish. Har bir shell skriptchisining instinkti, har bir "Python dan CLI ni shunchaki chaqiring" integratsiyasi, har bir sinov boshiga ikkilik fayl ishga tushirishga sozlangan har bir giperparametr freymvorki.

O'lchangan: 2,300 s (1,14x) — doimiy-server to'plamidan taxminan 24 ms yuqori (hosilaviy: 2,300 − 2,276). O'sha 24 millisekund fork/exec, dinamik yuklovchi, quvur sozlash va jarayonni buzishni sotib oladi. Va bu nimani o'lchashi namuna uchun tagga yaqin ekanligiga e'tibor bering: sahifa keshida issiq, bog'liqliksiz kichik mahalliy ikkilik fayl. Ish vaqti bilan biror narsani spawn qilish — JVM, importlari bo'lgan Python tarjimoni — ancha qimmatga tushadi; biz buni bu yerda o'lchamadik, lekin yo'nalish shubhasiz.

Bu solig'ning tuzilishi muhim: u chaqiruv boshiga doimiy, chaqiruv qancha ish olib yurishidan qat'i nazar. Butun 80-kombinatsiyali skanerlashga taqsimlanganda, 24 ms taxminan 1% — shovqin. Kombinatsiya boshiga qayta spawn qiling va xuddi shu doimiy 80 × ~24 ms ≈ 1,9 s bo'ladi — asosan butun foydali ish jarayon yaratishga sarflanadi (hosilaviy; analitik). Bar boshiga qayta spawn qiling va arifmetika yozishga ham arzimaydi.

Doimiy narx, mayda donadorlik: birini tanlang. Spawn to'laydigan namuna faqat spawn kamdan-kam bo'lganda va uning ortidagi yuk juda katta bo'lganda oqilona — aynan bizning bitta-spawn-skanerlash-boshiga o'lchovimizdek, va aynan belgi soni oshgan sari belgi-boshiga ichki jarayon arxitekturalari haqiqatan qanday ishlatilishiga qarama-qarshi.

Muvozanat nuqtasi arifmetikasi: tag — minimal chegara stavkasi

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

Hozirgacha o'lchangan hamma narsa bitta loyihalash qoidasiga siqiladi, va qoida arifmetika, fikr emas.

Har bir chegara kesib o'tishi kamida kechikish tagini turadi — bu yerda 14 mks, kichik-yuk echo borish-kelishi, va bu transport taklif qila oladigan eng yaxshisiga yaqin. O'sha tag — minimal chegara stavkasi: chegara orqali chaqiruv faqat yuborilgan hisoblash o'sha chegarani qulay ko'paytmada oshib ketganda arziydi. Donadorlik nisbatini aniqlang

G  =  Tchaqiruv boshiga hisoblashTtagG \;=\; \frac{T_{\text{chaqiruv boshiga hisoblash}}}{T_{\text{tag}}}

va sizning devor vaqtingizdagi chegaraning ulushi taxminan 1/(1+G)1/(1+G) — agar chaqiruv ma'lumotlarni ham olib yursa, yuk tranziti ustiga qo'shiladi.

Endi skanerlash raqamlarini shu orqali o'tkazaylik. Bitta kombinatsiyaning o'lchangan jarayon ichidagi narxi 25,130 mks. Kombinatsiya boshiga donadorlikda:

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

Kombinatsiya boshiga chaqiruvlar tagdan ~1,795x yuqorida joylashadi — chegara chaqiruv boshiga foizning o'ndan bir qismidan ancha kamini talab qiladi. Shuning uchun gaplashuvchi arxitektura ham faqat 107 ms yo'qotdi: bu ish yuklamasining donadorligida, ma'lumotlarni qayta yubormaydigan yoki matn gaplashmaydigan har qanday kesib o'tish namunasi xavfsiz taqsimlanadi. Kombinatsiya darajasidagi, fold darajasidagi, skanerlash darajasidagi chaqiruvlarning barchasi arzon zonada chuqur.

Endi qarama-qarshi cheksizlikka o'taylik. Bu — illyustrativ ish-yuklamalar-aro ekstrapolyatsiya — bizning skanerlashimizning varianti emas, balki tabiiy sharoitda haqiqatan mavjud bo'lgan ish yuklamasi shakli: dvigatelga bar boshiga murojaat qilinadi. Jonli uslubdagi tik-boshiga dvigatel xizmati; gRPC-bar-boshiga signal oqimi; 150,000 barning har biri uchun bir marta so'raladigan "strategiya serveri". Bu yadrodagi bar boshiga foydali hisoblash 25,130 mks / 150,000 ≈ 0,17 mks (hosilaviy) — har bir chaqiruv foydali ishda o'z chegara narxining taxminan 1/84 qismini olib yurar edi (hosilaviy: 0,168 mks hisoblash ustidagi 14,05 mks tag). Jami eshitilganidan ham yomonroq:

150,000 chaqiruv×14 μs    2,1 s sof IPC150{,}000 \ \text{chaqiruv} \times 14\ \mu\text{s} \;\approx\; \mathbf{2,1\ s\ sof\ IPC}

butun 2,010 s jarayon ichidagi vazifadan ham ko'p, masofaviy dvigatel bitta raqamni hisoblamasdan oldin sarflandi, va masofaviy dvigatel cheksiz tez bo'lsa ham 2,1 s bo'lib qolar edi (hosilaviy: 150,000 × 14 mks). Bunday mayda donadorlikda hech qanday hisoblash afzalligi omon qolmaydi. Va esda tuting, bu tag bitta xostdagi Unix soketi; o'sha bar-boshiga chaqiruvni tarmoq orqali xizmatga qiling, tag 150,000 chaqiruvda ikki-uch tartib darajasiga o'sadi.

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

Yana bitta halol kalibrlash, chunki 14 mks ham fizika qonuni emas — bu bizning transportimizning narxi: Python mijozi, yadro soketi, ikkala yo'nalishda syscall. Bitta mashina uchun maxsus qurilgan transport ancha pastga tushadi. ZigBolt — HFT ish yuklamalari uchun bizning ochiq manbali Zig xabar avtobusi, aynan shu mashinada mahalliy tarzda benchmark qilingan — umumiy xotira uzugi borish-kelishini o'rtacha hisobda taxminan 39 ns da bajaradi (64/256/1024 baytli xabarlarda 10/20/30 ns bir yo'nalishli p50). Bu bizning soket tagimizdan taxminan 360x past (hosilaviy: 14,05 mks / 39 ns). Solishtirish ataylab olma-bilan-apelsin, va biz buni shunday belgilaymiz: bizning 14 mks — Python-mijoz soket borish-kelishi, ZigBolt ning 39 ns — umumiy xotira orqali mahalliy Zig, shuning uchun farq transport va ish vaqtini aralashtiradi. Buni ikkovi o'rtasidagi poyga sifatida emas, balki bitta mashina tagi egallashi mumkin bo'lgan diapazon: taxminan uch tartib darajasi, amalga oshirish bo'yicha tanlangan sifatida o'qing. Bu zamonaviy kiyimdagi eski Lightweight RPC darsi (Bershad va boshq., 1990) — bitta mashinadagi kesib o'tishlarga protokol mexanizmi hukmronlik qiladi, va ular transport bitta mashina holati uchun qurilganda quladi. Yuqoridagi muvozanat nuqtasi arifmetikasi shaklini o'zgartirmaydi; chegara faqat siljiydi. 39 ns tagda, bar-boshiga donadorlik ham uni oshib ketar edi (150,000 × 39 ns ≈ 5,9 ms, hosilaviy) — bu aynan HFT tizimlari REST xizmati ko'tara olmaydigan chegaralarni qanday ko'tara olishini ko'rsatadi.

Bu butun muvozanat nuqtasi hikoyasi bitta gapda: chegara sizning dvigatelingiz qanchalik tez ekanligiga ahamiyat bermaydi; u kesib o'tish boshiga to'lov oladi, shuning uchun siz nazorat qiladigan o'zgaruvchilar har bir kesib o'tish qancha ish olib yurishi va kesib o'tish nimadan qilinganidir. Skanerlash boshiga to'plang va GG yuz mingdan yuqori. Kombinatsiya boshiga to'plang, G1795G \approx 1795 — hali ham yaxshi. Soket orqali bar boshiga chaqiring, G<1G < 1 — arxitektura birinchi optimallashtirishdan oldin o'lgan, va dvigatelni qayta yozish, Rust da yoki boshqa narsada, uni qayta tirilta olmaydi.

1,13x haqiqatan qayerda yashaydi — va baho

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

Bosh sarlavha farqini halol tahlil qilish vaqti keldi, chunki u tadqiqotning eng intuitsiyaga zid natijasini olib yuradi.

Batched Rust arxitekturasi jarayon ichidagi numba dan 266 ms orqada qoladi (hosilaviy: 2,276 − 2,010). O'lchangan chegara tarkibiy qismlari: ~2,0 ms da to'liq-yuk borish-kelishi, 49 mks da xom serializatsiya, bir necha baytda freym sarlavhalari — butun chegara hisobini ~2 ms deylik. Shuning uchun farqning 99% dan ortig'i chegara umuman emas. Bu hisoblash: IPC dan tozalangan, Rust serveri numba 2,010 s da bajaradigan skanerlashni bajarishga ~2,274 s sarflaydi — oddiy Rust yadrosi xom hisoblashda taxminan 13% sekin (hosilaviy).

Bu och gapga loyiq, chunki "buni Rust da qayta yoz, u tezroq bo'ladi" "IPC sizni o'ldiradi" kabi xalq e'tiqodidir. Ikkala yadro ham oxir-oqibat LLVM ga tushadi — numba Python bytecode ni shu orqali pasaytiradi, rustc MIR ni shu orqali pasaytiradi — va ikkalasi ham, eng ehtimol, skalyar sikllar sifatida ishlaydi: WMA ning ichki yig'indisi — suzuvchi nuqtali kamaytirish, uni LLVM numba ning @njit standartlari bermaydigan va bizning portimiz so'ramaydigan fast-math qayta bog'lash litsenziyasisiz avtomatik ravishda vektorlashtira olmaydi. Shuning uchun ~13% ikkita skalyar LLVM-kompilyatsiya qilingan sikl o'rtasidagi o'lchangan kod generatsiyasi farqi — va sababni da'vo qilish o'rniga, biz aniq gumon qilinuvchini tekshirdik. Tabiiy gumon qilinuvchi — Rust ning xavfsiz indekslashi: issiq WMA sikli har bir massiv kirishida chegarani tekshiradi, numba ning @njit esa chegara tekshiruvi o'chirilgan holda kompilyatsiya qilinadi. Shuning uchun biz xuddi shu yadroning get_unchecked dan foydalanadigan ekvivalentlik tekshirilgan variantini qurdik — issiq yo'lda hech qayerda chegara tekshiruvi yo'q — va uni beshinchi arxitektura sifatida o'lchadik. U farqni yopmadi: 2,337 s (1,16x), chegara-tekshirilgan qurilishning 2,276 s dan bir oz sekinroq. Gipoteza tekshirildi, gipoteza rad etildi. Bilimning halol holati: ~13% haqiqiy va qayta yaratiluvchi (10 ishga tushirishning medianasi, ~2% chegarasidagi diapazonlar), va hozirgi vaqtda tayinlanmagan — faqat assembler darajasidagi profillash hal qila oladigan ajratish xatti-harakatidagi, sikl tuzilishidagi yoki ko'rsatma rejalashtirishdagi qandaydir farq. Dars butun qoladi: oddiy Rust avtomatik ravishda yaxshi numba dan tez emas, va bepul hisoblash g'alabasi taxmini bilan sotib olingan til chegarasi hisoblash yo'qotishi bilan kelishi mumkin. Sozlangan Rust yadrosi — oldindan ajratilgan buferlar, aniq SIMD, kombinatsiyalar bo'ylab oqimlar — hali ham belgini o'zgartira olishi mumkin. Lekin bu profillash va yadro ishi bilan hal qilinadigan hisoblash savoli, va bu tadqiqotning savoli — chegara. Chegaraning javobi: bir marta kesib o'tilganda, baytlarda, u ~0,1% turadi.

Shunday qilib, to'liq bahoni yig'aylik, yuqorida o'lchangan har bir band.

Tillar aro dvigatel xizmati quyidagilarning barchasi bajarilganda g'alaba qozonadi:

  • Hisoblash afzalligi haqiqiy — tilning obro'sidan taxmin qilinmasdan, o'z yadroingizda o'lchangan. (Bizniki isbotlanguncha −13% edi — va o'sha kamchilikning birinchi "aniq" tushuntirishi sinovda o'ldi.)
  • Siz yirik donador kesib o'tasiz — skanerlash boshiga yoki fold boshiga bitta chaqiruv, 14 mks tagdan minglab marta yuqori, to'plam arxitekturasining umumiy 1,13x i (~0,1% chegara) ko'rsatganidek.
  • Siz ikkilik tilda gaplashasiz — uzunlik prefiksli xom massivlar, Arrow, 1,2 MB da 49 mks turadigan memcpy sinfidagi har qanday narsa; 66,243 mks turadigan matn hech qachon emas.
  • Ma'lumotlar oldindan yuklangan — holatli server megabaytlarni qayta yuborish o'rniga echo egri chizig'ining ~16 mks uchida faqat-parametr chaqiruvlarini qabul qiladi.

U dvigatel xizmatlari odatda joylashtirilgandek joylashtirilganda yutqazadi:

  • JSON/REST mikroservisi — har bir chaqiruvda, ikkala yo'nalishda ham 1348x serializatsiya solig'ini to'laydi; gaplashuvchi donadorlikda bu 2 s vazifada 5,3 s kodlash.
  • Ish birligi boshiga RPC — kombinatsiya boshiga bu yerda 107 ms turadi va faqat har bir chaqiruv 25,130 mks hisoblashni olib yurishi tufayli omon qoladi; bar boshiga bu har qanday ish bo'lmasdan oldin ~2,1 s sof IPC, 2,0 s vazifada.
  • Chaqiruv boshiga spawn — har safar ~24 ms doimiy narx, skanerlash boshiga bir marta zararsiz, kombinatsiya boshiga to'langanda ikki soniyaga yaqin.

Ya'ni: muvaffaqiyatsizlikka uchraydigan arxitekturalar ekzotik emas. JSON REST dvigateli, belgi-boshiga ichki jarayon, gRPC-tik-boshiga — bu "backtest dvigatelini ajratib chiqaraylik" haqiqatda qanday qurilishining adolatli hisobi. Xalq e'tiqodi umumiy amaliyot tavsifi sifatida empirik ravishda yaxshi asoslangan va tabiat qonuni sifatida empirik ravishda noto'g'ri. Chegara hech qachon muammo bo'lmagan. Uni kesib o'tishning standart usullari muammo.

Chegara foydasiga bir dalil o'z gapiga loyiq, chunki bu biz bu tadqiqotni umuman o'tkazgan sababdir. Yaxshi loyihalashtirilgan chegara ortidagi bitta kompilyatsiya qilingan yadro ham tadqiqot skanerlashi, ham jonli savdo siklini xizmat qila oladi — xuddi shu ikkilik fayl, xuddi shu arifmetika, bit-bitga. Bizning backtest-live paritet tadqiqoti tadqiqot va ishlab chiqarish dvigatellari ikkita kod bazasi bo'lganda qanday uzoqlashishini katalogladi; dvigatel xizmati o'sha chetlanishning eng kuchli tarkibiy davosi, va bu tadqiqot davoning narxini halol belgilaydi: to'g'ri qilinganda, devor vaqtining taxminan 0,1% i va tarjimada hech narsa o'zgarmaganini isbotlaydigan ekvivalentlik darvozasi. O'sha almashuv — maxsus jarayon chegarasi bitta-yadro paritetiga almashtiriladi — bu raqamlar bo'yicha foydali kelishuv. Noto'g'ri qilinsa, xuddi shu g'oya PnL ingiz ustida o'tirib ishlab chiqarishga 1348x serializatsiya solig'ini yuboradi.

Asosiy xulosalar

  1. Chegara deyarli bepul; xalq e'tiqodi o'lchovda muvaffaqiyatsizlikka uchraydi. Butun 1,2 MB yopilish seriyasini Unix soketi orqali borib-kelib yuborish — to'liq tahlil va qayta kodlashni qo'shgan holda — 2,043,4 mks turadi, 2,010 s vazifaning taxminan 0,1% i (hosilaviy). Batched Rust-soket-orqali arxitekturasi umumiy 1,13x ga yetadi, va hatto o'sha farqning ~99% i IPC emas.
  2. "Buni Rust da qayta yoz" — hisoblash da'vosi — chegarani sotib olishdan oldin uni tekshiring. Bizning qatorma-qator Rust portimiz numba yadrosidan ~13% sekin hisoblaydi (hosilaviy: 2,274 s ga qarshi 2,010 s) — tayinlanmasdan qoladigan, ikkita skalyar LLVM-kompilyatsiya qilingan sikl o'rtasidagi qayta yaratiluvchi kod generatsiyasi farqi: biz aniq gumon qilinuvchini tekshirdik va rad etdik, chunki chegara tekshiruvisiz ekvivalentlik tekshirilgan get_unchecked qurilishi tezroq bo'lib chiqmadi (2,337 s ga qarshi 2,276 s). Oddiy Rust avtomatik ravishda tez emas; sozlangan yadro tez bo'lishi mumkin — o'lchang, keyin qaror qiling.
  3. Haqiqiy soliq — matn. 150,000 floatni JSON sifatida kodlash 66,243 mks turadi, xomda 49,1 mks bilan solishtirganda — 1348x, yo'nalish boshiga, chaqiruv boshiga, ikkala tomonda ham to'langan. Gaplashuvchi JSON joylashtirish 2 s vazifada 5,3 s kodlashni yoqadi (hosilaviy). Chegaralar orqali ikkilik tilda gaplashing: xom freymlar, Arrow — narx massivida hech qachon json.dumps emas.
  4. Gaplashuvchi va ommaviy o'lchanadi, va holatsizlik aybdor. Ma'lumotlarni qayta yuboradigan kombinatsiya boshiga chaqiruvlar: to'plamning 1,13x iga qarshi 1,19x (+107 ms, hosilaviy; echo egri chizig'ining ~81 ms bir yo'nalishli bashorati bundan ~25% past tushadi, qolgani chaqiruv boshiga freymlash). Oldindan yuklangan holatli server xuddi shu 80 chaqiruvni har biri ~16 mks da qilar edi — jami taxminan 1,3 ms (echo tagidan hosilaviy). Parametrlarni yuboring, ma'lumotlar to'plamini emas.
  5. Tagni hurmat qiling — va tagning tanlov ekanligini biling. Bizning Python-Unix-soket-orqali kesib o'tishimiz 14 mks da tagga yetadi; kombinatsiya boshiga donadorlik uni ~1,795x oshib ketadi (chaqiruv boshiga 25,130 mks hisoblash) — xavfsiz. Bar-boshiga namuna (illyustrativ ish-yuklamalar-aro ekstremal: jonli tik-boshiga dvigatel, bu skanerlash emas) 2,0 s vazifada 150,000 × 14 mks ≈ 2,1 s sof IPC to'lar edi (hosilaviy) — cheksiz tez dvigatel bilan ham kelganda o'lik. Chaqiruv boshiga spawn qilish doimiy ~24 ms qo'shadi (hosilaviy). Va ZigBolt kabi maxsus qurilgan umumiy xotira transporti bu mashinada mahalliy tarzda ~39 ns da borish-kelish qiladi — bizning soket tagimizdan ~360x past (hosilaviy; mahalliy Zig Python mijoziga qarshi, shuning uchun buni tag egallashi mumkin bo'lgan diapazon sifatida o'qing, poyga sifatida emas).
  6. Bir marta, baytlarda, ma'lumotlar allaqachon mavjud bo'lganda kesib o'ting — va chegara sizga ~0,1% ga paritet sotib oladi. Tadqiqot va jonlini xizmat qiladigan, ekvivalentlik tekshiruvi bilan himoyalangan (PnL −5165,58, 57,029 savdo, tillar orasida va ikkala Rust qurilishida ham bir xil) bitta yadro — dvigatel xizmati uchun halol holat. Nohalol holatlar — JSON, gaplashuvchi, chaqiruv-boshiga-spawn — IPC ga uning obro'sini bergan holatlardir.

To'liq tajriba — Rust dvigateli, sim protokoli, echo va serializatsiya tekshirish uskunalari, ekvivalentlik darvozasi, va bu maqoladagi bitta deterministik skriptdan qayta yaratiluvchi har bir raqam — ipc-tax.marketmaker.cc saytidagi hamrohlik maqolasida, kod va ma'lumotlar github.com/suenot/ipc-tax manzilida.

Soket hech qachon muammo bo'lmagan. Butun ma'lumotlar to'plami uchun ikki millisekund, borib-kelib — folklor uch tartib darajasiga adashdi, va bir vaqtning o'zida ikkala yo'nalishda ham: baytlar haqida juda pessimistik, matn haqida juda kechirimli. Chegarani u biror narsa turadigandek kesib o'ting, va u turmaydi.

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

Bozordan bir qadam oldinda bo'ling

Sun'iy intellekt savdo tahlillari, bozor tahlili va platforma yangiliklari uchun bizning xabarnomaga obuna bo'ling.

Biz sizning maxfiyligingizni hurmat qilamiz. Istalgan vaqtda obunadan chiqishingiz mumkin.