← Макалаларга кайтуу
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 кечигүү ийриинин, сериализация салыгынын жана spawn наркынын өзүнчө өлчөөлөрү менен. Макаланы онлайн окуңуз (интерактивдүү версия + PDF) ipc-tax.marketmaker.cc сайтында, код жана маалыматтар github.com/suenot/ipc-tax дарегинде.

Тез боло турган ар бир бэктест кыймылдаткычы акырында бир эле маегин жаратат. Биздики убагында келди. Ылдамдык баскычы жаңы гана 80-комбинациялуу параметр сынагын pandas'тын 69.9 секундунан бир агымдуу numba'нын болжолдуу 2 секундуна чейин түшүргөн эле, жана кийинки табигый кызыгуу: эмне үчүн Python JIT'те токтоо керек? Ядрону Rust'ко кайра жазуу. Аны толук кандуу кыймылдаткыч кызматына айландыруу — сокеттин артында турган компиляцияланган бинардык файл, ар бир изилдөө скриптинен, ар бир тилден жана атүгүл лайв-трейдерден чакырылуучу. Бир ядро, бир чындык, кайталанган логика жок.

Андан кийин каршы аргумент да убагында келет: процесстен чыккан заматта IPC сизди жутуп алат. Маалыматтар сериализацияланышы керек, чек ара аркылуу жөнөтүлүшү керек, десериализацияланышы керек; ар бир чакыруу syscall жана контекст алмаштыруу үчүн акы төлөйт; сиздин сулуу Rust ядроңуз бүт өмүрүн түтүктү күтүү менен өткөрөт. Процессте калыңыз. Муну баары билет.

Бул макала баары билет деп ойлогон нерсени өлчөйт, жана өлчөө талаш-тартыштын эки жагынан тең кызыктуу. Элдик ишеним — "тезирээк тилдер аралык кыймылдаткыч процесс ичиндеги numba'дан жеңилет, себеби IPC сизди өлтүрөт" — жалпысынан туура эмес жана атайын шарттарда гана туура экени аныкталды. Чек араны бир жолу, чийки байттар менен кесип өтүү эки секунддук тапшырмада болжолдуу 2 миллисекунд турат: тегеректөө катасы. Салык чек арада эмес. Ал — аны кантип кесип өтүүдө — жана кыймылдаткыч кызматтарынын жаратылышта көбүнчө жайгаштырылуучу үч жолу (JSON API, иш бирдиги сайын чакыруу, чакыруу сайын процесс spawn'у) ар бири, өлчөнөрлүк түрдө, фольклор алдын ала айткан кырсыктын бир бөлүгү.

Бул жерде бүт эксперимент алдын ала. Төмөндөгүнүн баары ар бир саптын анатомиясы.

Архитектура Сынак сайын чек ара аркылуу эмне өтөт Реалдуу убакыт процесс ичиндегиге салыштырмалуу
процесс ичиндеги numba эч нерсе — түз чакыруу 2.010 с 1.00x
Rust сервери, batched (Unix сокети) бир рейс: бүт серия + бардык 80 параметр топтому 2.276 с 1.13x
Rust сервери, batched, get_unchecked ядросу ошол эле бир рейс — чек ара текшерүүсүз ядро версиясы (бүтүмдү караңыз) 2.337 с 1.16x
Rust сервери, chatty (Unix сокети) 80 рейс: серия комбинация сайын кайра жиберилет 2.383 с 1.19x
Rust spawn (stdin/stdout) процесс spawn'у + бир түтүк аркылуу жиберилген суроо-талап 2.300 с 1.14x

Apple M2 Max, Python 3.14.6, numpy 2.4.3, numba 0.64.0, rustc 1.94.0 (релиз куруу, тышкы crate жок). 150,000 бар × 80 комбинация, 0.09% рейс акысы, seed 42; жабуу сериясы зымда 1,200,000 байт (1.2 МБ) түзөт. Архитектура сайын 10 жүгүрүүнүн медианасы; мин-макс диапазондору ~2% чегинде калат. Бардык бешөө тең бирдей HMA/HMA3 stop-and-reverse сынагын жүргүзөт, жана эквиваленттик дарбаза эки Rust ядро версиясынын тең комбинация сайынкы (PnL, бүтүм саны) натыйжаларынын numba'га так дал келерин ырастайт — манжа изи PnL −5165.58, 57,029 бүтүм боюнча, ылдамдык баскычы изилдөөсүнүн numba ядросу менен ошол эле seed боюнча байт-байтка бирдей. Биз чек араларды салыштырабыз, ишке ашырууларды эмес.

Batched сабын кылдаттык менен окуңуз, себеби ал бүт тезисти көтөрөт. Rust-сокет-аркылуу архитектурасы процесс ичиндеги numba'дан 1.13x жай — толук сынакта 266 мс артта калат (туунду: 2.276 − 2.010). Элдик аңгеме ал миллисекунддар IPC дейт. Андай эмес. Ошол айырманын болжолдуу 2 мс чек ара болуп саналат — бүт 1.2 МБ жабуу сериясы жиберилип, натыйжалар кайтарылып, түз өлчөнгөн. Калган ~264 мс биздин жөнөкөй Rust ядробуздун сынакты numba ядросунан болжолдуу 13% жай эсептешинен улам (туунду: 2.276 с минус ~2 мс чек ара ≈ 2.274 с Rust эсептөө, numba үчүн 2.010 с менен салыштырганда). Тил катары Rust тил катары Python'дон жеңилген жок; бир скалярдык LLVM-компиляцияланган цикл экинчисине карата код генерациялоо жарышын жеңилди — жана биз жоготууну ачык шектүүгө да таандык кыла алган жокпуз: ошол эле ядронун чек ара текшерүүсүз get_unchecked куруусу тезирээк болгон жок (2.337 с; бүтүм бөлүмү муну ажыратат). Сокеттин мунун баарына тиешеси дээрлик болгон жок.

Ошол сүйлөмдүн эки бөлүгүн тең кармап туруңуз. Чек ара туура кесип өткөндө дээрлик акысыз — жана "аны Rust'та кайра жаз" сизге жайгаштыруу чек арасын сатып алат, автоматтык эсептөө жеңишин эмес. Эки факт тең элдик интуицияга каршы келет, жана экөө тең таблицада.

Бир ядро, эки тил, төрт чек ара

Иш жүгү атайылап ылдамдык баскычы белгилегендей эле, ошондуктан эки изилдөө бири-бирине байланышат. Ядро — HMA/HMA3 кесилиши — эки Hull стилиндеги жылдырма орточолордогу stop-and-reverse системасы, параметр комбинациясы сайын жети салмактуу-жылдырма-орточо өтүшү, кошумча позицияны алып жүргөн, ар бир кесилиште 0.09% рейс акысын алып салып PnL жазган жана артка бурулган абалдуу бар-бар окуя циклы. Маалыматтар — 150,000 бар seed аркылуу жасалган синтетикалык геометриялык браундук кыймыл (seed=42); тор — [6,200][6, 200] аралыгына таралган 80 HMA узундугу. Процесс ичиндеги эталон — баскычтын бир агымдуу numba тепкичи, бул изилдөө үчүн кайра өлчөнгөн: анда 1.98 с, бул жерде 2.010 с — ошол эле ядро, ошол эле машина, ишенимдүү жадатма.

Тилдер аралык кыймылдаткыч ошол numba ядросунун Rust тилине сап-сабы менен котормосу — ошол эле циклдер, ошол эле NaN иштетүү, ошол эле акы арифметикасы — тышкы crate жок релиз режиминде компиляцияланган, ошондуктан бүт эксперимент көз карандылыксыз жана кайра жаратылуучу бойдон калат. Ал атайылап минималдуу бинардык протоколдо сүйлөйт: ар бир багытта узундук префикстелген фрейм, баары 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 opcode'у изилдөөнүн скальпели: эч нерсе эсептебеген, башкарылуучу өлчөмдөгү рейс, ошондуктан таза чек ара наркын өзүнчө өлчөөгө болот — сериализация, syscall, сокет транзити, десериализация, жана башка эч нерсе.

Беш өлчөнгөн архитектура — төрт чек ара үлгүсү жана бир ядро версиясы:

  • in_process — numba ядросун түз чакыруу. Чек ара жок. Эталон.
  • rust_batch_unix — Unix домен сокетиндеги туруктуу Rust сервери. Бир рейс бүт жабуу сериясын жана бардык 80 параметр топтомун жиберет; Rust ар бир комбинацияны эсептейт; бир жооп кайтат. Оор чакыруу.
  • rust_batch_unchecked — ошол эле batched чек ара, бирок ядро get_unchecked аркылуу индекстейт (ысык жолдо чек ара текшерүүсү жок). Эсептөө айырмасы боюнча белгилүү бир гипотезаны текшерүү үчүн бар; бүтүм бөлүмү аны жумшайт.
  • rust_chatty_unix — ошол эле сервер, бирок комбинация сайын бир рейс, 1.2 МБ серия ар жолу кайра жиберилет. Жөнөкөй RPC-иш-бирдиги-сайын архитектурасы.
  • rust_spawn_stdin — бинардык файлды сынак сайын spawn кылуу жана суроо-талапты stdin аркылуу жиберүү. "CLI кыймылдаткычын тышка чакыруу" үлгүсү; процесс түзүү наркын төлөйт.

Жана эквиваленттик дарбаза, аны болбосо мунун баары маанисиз болмок: убакыт өлчөгөндөн кийин, ар бир Rust версиясынын комбинация сайынкы (PnL, бүтүм саны) вектору numba менен салыштырылат — бүтүм саны так, PnL абсолюттук 10610^{-6} чейин. Тапшырылган жүгүрүү эки курулушка тең — коопсуз индекстөө жана get_uncheckedall_ok: true билдирет. Биринчи комбинациянын манжа изи — PnL −5165.58 пайыздык пункт, 57,029 бүтүм боюнча — ылдамдык баскычы изилдөөсүнүн numba ядросу менен сан-сандан дал келет, бул эки макаланы тең ошол seed боюнча бир ядрого бекитет. Тилдер аралык котормолор так унчукпаган айырмачылык жашагысы келген жер (пайызга айландыруудан мурун эмес, кийин колдонулган акы, ар башка тармакталган NaN салыштыруусу, терезедеги бир-бир ката — биздин look-ahead жаңылыш таксономиясы шаңдан 15 Sharpe жасай аларын көрсөткөн ошол эле ката түрү). Ар кандай нерсе эсептеген эки кыймылдаткычтын эталон сыноосу эталон сыноо эмес; бул эки байланышсыз программанын жарышы.

Эквиваленттик орнотулгандан кийин, жогорудагы таблицадагы ар бир айырма — чек ара жана эсептөө — башка эч нерсе эмес.

Кесип өтүү чындыгында эмне турат: echo ийриси

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 флоатты талдайт, аларды кайра коддойт жана кайра жиберет. Эки багыт тең сериализация, syscall жана сокет транзитине акы төлөйт. Мына, өлчөнгөн ийри (10 жүгүрүүнүн медианасы):

Жүктөм (флоат) Багыт сайын байт Рейс
1 8 14.1 мкс
100 800 16.4 мкс
1,000 8,000 18.1 мкс
10,000 80,000 192.5 мкс
100,000 800,000 1,367.3 мкс
150,000 1,200,000 2,043.4 мкс

Бул таблицада эки структуралык факт жашайт.

Биринчиден, түп. Маани жагынан эч нерсе алып жүрбөгөн рейс — 8 байт — 14 мкс турат. Бул ушул ташымал аркылуу чакыруу жасоонун дербес баасы: эки write syscall, эки read syscall, ядро сокет механизми, план түзүүчүнүн ойгонушу. Ийринин сол жакта канчалык жалпак экенине көңүл буруңуз: 1 флоаттан 1,000 флоатка чейин нарк араң кыймылдайт (14.1 → 18.1 мкс). Болжолдуу 8 КБ'дан төмөн сиз чакыруу үчүн акы төлөйсүз, байттар үчүн эмес. Бул сан — кечигүү түбү — бүт изилдөөнүн эң маанилүү туруктуусу, жана биз төмөндө теңдик чекитинин арифметикасын ушуга негиздейбиз.

Экинчиден, ийкем. ~10,000 флоаттан кийин ийри өткөрүү жөндөмдүүлүгүнө көз каранды жана болжолдуу сызыктуу болот. Толук 1.2 МБ серия — жалпы 2.4 МБ жылдырылды, барып-келип, ошондой эле Rust жагында 150,000 флоаттын толук талдоосу жана кайра коддолушу менен — 2,043.4 мкс турат. Бул бүт жөнөкөй стек аркылуу эффективдүү ~1.2 ГБ/с берет (туунду: 2.4 МБ / 2.04 мс) — узундук префикстелген фреймдер жана байт-байт флоат талдоочусу бар Unix домен сокети, эч кандай zero-copy амалдары жок, жалпы эс жок, эч кандай айлакерлик жок.

Эки өлчөнгөн туруктуу менен бир кесип өтүүнүн акылга сыярлык модели:

Tcall(b)    14 μsтүп  +  2b1.2 ГБ/сжүктөм, эки багытT_{\text{call}}(b) \;\approx\; \underbrace{14\ \mu\text{s}}_{\text{түп}} \;+\; \underbrace{\frac{2b}{1.2\ \text{ГБ/с}}}_{\text{жүктөм, эки багыт}}

Эми башкы санды контекстке коёлу. Толук сынак процесс ичинде 2.010 с алат. Анын бүт маалымат жыйнагын чек ара аркылуу барып-келип жиберүү ~2.0 мс турат — тапшырманын болжолдуу 0.1% (туунду: 2.0434 мс / 2.010 с). Эгер сиз бир жолу, чийки байттар менен кесип өтсөңүз, чек ара тегеректөө катасы болуп саналат. Бул элдик ишенимдин биринчи өлгөн жарымы: коркунуч эч качан ушундай арзан нерсеге байланыштуу болгон эмес.

Ошол кесип өтүүнүн 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 менен), бирок биз аны өлчөгөн жокпуз; loopback 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 МБ флоатты коддоо убактысы чийкиге салыштырмалуу
чийки байттар (.tobytes()) 49.1 мкс 1.0x
pickle 29.8 мкс 0.6x
JSON (json.dumps(close.tolist())) 66,243 мкс 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 мс — бир коддоо, бир жак, бир чакыруу. JSON кызматы коддоону жана декоддоону экөөнү тең, чек аранын эки жагында, ар бир чакырууда төлөйт. JSON аркылуу бир batched чакыруу бүт сынактын эсептөө бюджетинин ~3.3% жалгыз кардар жагындагы коддоого жагат (туунду: 66 мс / 2.010 с). JSON'ду chatty архитектуранын алдына коюңуз — комбинация сайын бир чакыруу, төмөндөгү үлгү — жана жалгыз кардар жагындагы коддоо 80 × 66 мс = 5.3 с турат: бир байт да кыймылдабай жана сервер эч нерсе талдабай туруп бүт пайдалуу иштин эки жарым эседен ашыгы (туунду).

Бул көптөгөн командалар билбестен өндүрүштө өлчөгөн чыныгы "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 с (1.13x). Chatty: 2.383 с (1.19x) — 107 мс жайыраак (туунду: 2.383 − 2.276). Бул дельта эмнени билдирерин жана билдирбесин так айтуу үчүн: echo ийриси ага жөнөкөй божомол берет — толук серияны ар бир 2,043 мкс толук-жүктөм рейсинин болжолдуу жарымында кошумча 79 жолу жиберүү, болжолдуу 81 мс — бул өлчөнгөн 107 мс'тен болжолдуу 25% төмөн түшөт; калганы Python жагындагы чакыруу сайын суроо-талап куруу жана фрейминг, аны echo божомолу камтыбайт. Кандай болсо да, бул кошумча кесип өтүү сайын ~1.4 мс чыгат (туунду: 107 / 79); жооптор маанисиз — комбинация сайын 16 байт.

Ошол 107 мс'тин эки окулушу бар, жана экөө тең маанилүү.

Жумшак окулуш: бул жалпы убакыттын жөн гана ~4.5%, кырсык эмес. Туура — жана фольклордун кырсыгы бул жерде эмне үчүн ишке ашпаганын түшүнүү маанилүү. Ар бир chatty чакыруу дагы эле 25,130 мкс чыныгы эсептөөнү алып жүрөт (бир комбинациянын баасы — процесс ичиндеги комбинация сайынкы өлчөнгөн наркы), ошондуктан чакыруу сайынкы ~1.4 мс чек ара жүктөмү чакыруу сайынкы иштен бир ирет чоңдук деңгээлинде төмөн калат. Chatty архитектуралары ар бир чакыруу чындап оор болгондо өлүмгө алып келбейт. Алар түйүрчөктүүлүк кичирейген сайын өлүмгө алып келет — бул теңдик чекити бөлүмүнүн бүт темасы.

Айыптоочу окулуш: бул салык толугу менен ыктыярдуу болгон, жана ал чакыруу саны × жүктөм менен масштабдалат. Chatty үлгүсү маалымат жыйнагын ар бир чакырууда бир гана себептен кайра жиберет: кызмат абалсыз, ошондуктан ар бир суроо-талап бүт контекстти алып жүрүшү керек. Бул жөнөкөй "сынак чекитинин" демейки формасы — жана дээрлик ар бир ак тактага скетч жасалган REST микросервисинин. Абалдуу сервер — серияны бир жолу жүктөө, андан кийин 48 байт параметр фреймдерин жиберүү — ар бир комбинация сайынкы чакырууну echo ийриинин кичине-жүктөм четине жакын коёт: чакыруу сайын болжолдуу 16 мкс, бардык 80 үчүн болжолдуу 1.3 мс (echo түбүнөн туунду; аналитикалык, өзүнчө өлчөнгөн эмес). Chatty жазасы кичирейбейт; ал жоголот. Сабак так: көйгөй көп чакыруу жасоодо эмес — ал протокол ар бир чакырууну биринчи катары эсептегендиктен абалды кайра жиберүүдө.

Маалыматтарды алдын ала жүктөңүз. Параметрлерди жибериңиз. Чек араны ар жолу бүт дүйнөнү чемоданыңызда эмес, ниет менен кесип өтүңүз.

Spawn наркы: кыймылдаткычты чакыруу сайын ижарага алуу

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

Үчүнчү жайгаштыруу үлгүсү эң эскиси: сервер таптакыр жок. Кыймылдаткыч бинардык файлын spawn кылуу, суроо-талапты stdin аркылуу жиберүү, жоопту stdout'тон окуу, аны өлүмгө калтыруу. Ар бир shell скрипттөөчүнүн инстинкти, ар бир "Python'дон CLI'ды жөн эле чакыр" интеграциясы, сынак сайын бинардык файл иштетүүгө конфигурацияланган ар бир гиперпараметр фреймворку.

Өлчөнгөн: 2.300 с (1.14x) — туруктуу-сервер batch'тан болжолдуу 24 мс жогору (туунду: 2.300 − 2.276). Ошол 24 миллисекунд fork/exec, динамикалык жүктөөчү, түтүк орнотуу жана процесс бузууну сатып алат. Жана мунун эмнени өлчөрү үлгү үчүн түбүнө жакын экенин белгилеп кетиңиз: барак кэшинде жылуу, көз карандылыксыз кичинекей нативдик бинардык файл. Кандайдыр бир нерсени аткаруу чөйрөсү менен spawn кылуу — JVM, импорттору бар Python интерпретатору — алда канча кымбат турат; биз муну бул жерде өлчөгөн жокпуз, бирок багыт шектенбейт.

Бул салыктын түзүлүшү маанилүү: ал чакыруу сайын туруктуу, чакыруу канча иш алып жүрөрүнө карабастан. Толук 80-комбинациялуу сынакка бөлүштүрүлгөндө, 24 мс болжолдуу 1% — ызы-чуу. Комбинация сайын кайра spawn кылыңыз, ошол эле туруктуу 80 × ~24 мс ≈ 1.9 с болот — негизинен бүт пайдалуу иш процесс түзүүгө жагылат (туунду; аналитикалык). Бар сайын кайра spawn кылыңыз, арифметика жазууга да арзыбайт.

Туруктуу нарк, майда түйүрчөктүүлүк: бирин тандаңыз. Spawn төлөгөн үлгү жалгыз spawn сейрек болгондо жана анын артындагы жүктөм эбегейсиз чоң болгондо гана акылга сыярлык — так биздин бир-spawn-сынак-сайын өлчөөбүз сыяктуу, жана символ саны өскөн сайын символ-сайынкы ички процесс архитектуралары чын мааниде колдонулуучу жолго так карама-каршы.

Теңдик чекитинин арифметикасы: түп — минималдуу чектик деңгээл

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 мкс, кичине-жүктөм echo рейси, жана бул ташымал сунуш кыла турган эң жакшыга жакын. Ошол түп — минималдуу чектик деңгээл: чек ара аркылуу чакыруу жалгыз жиберилген эсептөө ошол чекти ыңгайлуу эселик менен өткөндө гана баалуу. Түйүрчөктүүлүк катышын аныктаңыз

G  =  Tчакыруу сайынкы эсептөөTтүпG \;=\; \frac{T_{\text{чакыруу сайынкы эсептөө}}}{T_{\text{түп}}}

жана сиздин жалпы убактыңыздагы чек аранын үлүшү болжолдуу 1/(1+G)1/(1+G) — эгер чакыруу маалыматтарды да алып жүрсө, жүктөм транзити үстүнө кошулат.

Эми сынактын сандарын ушул аркылуу жүргүзөлү. Бир комбинациянын өлчөнгөн процесс ичиндеги наркы 25,130 мкс. Комбинация сайынкы түйүрчөктүүлүктө:

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

Комбинация сайынкы чакыруулар түптөн ~1,795x жогору жайгашат — чек ара чакыруу сайын пайыздын ондон бир бөлүгүнөн алда канча аз талап кылат. Ошондуктан chatty архитектурасы да болжолдуу 107 мс гана жоготту: бул иш жүгүнүн түйүрчөктүүлүгүндө, маалыматтарды кайра жибербеген же текст сүйлөбөгөн ар бир кесип өтүү үлгүсү коопсуз бөлүштүрүлөт. Комбинация-деңгээлдеги, fold-деңгээлдеги, сынак-деңгээлдеги чакыруулардын баары арзан аймакта терең.

Эми карама-каршы четке өтөлү. Бул — иллюстративдик иш-жүктөр-аралык экстраполяция — биздин сынактын версиясы эмес, чынында жаратылышта бар иш жүгү формасы: кыймылдаткычка бар сайын кайрылат. Лайв стилиндеги тик-сайынкы кыймылдаткыч кызматы; gRPC-бар-сайын сигнал агымы; 150,000 бардын ар бири үчүн бир жолу суралуучу "стратегия сервери". Бул ядродогу бар сайынкы пайдалуу эсептөө 25,130 мкс / 150,000 ≈ 0.17 мкс (туунду) — ар бир чакыруу пайдалуу иште өз чек ара наркынын болжолдуу 1/84 бөлүгүн алып жүрөт (туунду: 0.168 мкс эсептөө үстүндөгү 14.05 мкс түп). Жалпы сан катыш угулгандан да начар:

150,000 чакыруу×14 μs    2.1 с таза IPC150{,}000 \ \text{чакыруу} \times 14\ \mu\text{s} \;\approx\; \mathbf{2.1\ с\ таза\ IPC}

бүт 2.010 с процесс ичиндеги тапшырмадан да көп, алыскы кыймылдаткыч бир санды эсептебей туруп жумшалды, жана алыскы кыймылдаткыч чексиз тез болсо да 2.1 с бойдон калмак (туунду: 150,000 × 14 мкс). Мындай майда түйүрчөктүүлүктө эч кандай эсептөө артыкчылыгы аман калбайт. Жана эсиңизде болсун, бул түп бир хосттогу 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 мкс да физика мыйзамы эмес — бул биздин ташымалдын баасы: Python кардары, ядро сокети, эки багытта syscall. Бир машинага атайын жасалган ташымал алда канча төмөн түшөт. ZigBolt — HFT иш жүктөрү үчүн биздин ачык булактуу Zig билдирүү шинасы, дал ошол машинада нативдик түрдө бенчмарк жасалган — жалпы эс шакегинин рейсин орточо эсеп менен болжолдуу 39 нс убакытта аткарат (64/256/1024 байт билдирүүлөрдө 10/20/30 нс бир багыттуу p50). Бул биздин сокет түбүнөн болжолдуу 360x төмөн (туунду: 14.05 мкс / 39 нс). Салыштыруу атайылап алма-менен-апельсин, жана биз муну ошентип белгилейбиз: биздин 14 мкс — Python-кардар сокет рейси, ZigBolt'тун 39 нс — жалпы эс аркылуу нативдик Zig, ошондуктан айырма ташымал жана аткаруу чөйрөсүн аралаштырат. Муну экөөнүн ортосундагы жарыш катары эмес, бир машина түбү ала турган диапазон: болжолдуу үч ирет чоңдук деңгээли, ишке ашыруу боюнча тандалган катары окуңуз. Бул азыркы кийимдеги эски Lightweight RPC сабагы (Bershad жана башк., 1990) — бир машинадагы кесип өтүүлөргө протокол механизми үстөмдүк кылат, жана алар ташымал бир машина учуру үчүн курулганда кулайт. Жогорудагы теңдик чекитинин арифметикасы формасын өзгөртпөйт; чек жөн гана жылат. 39 нс түбүндө, бар-сайынкы түйүрчөктүүлүк да аны өтмөк (150,000 × 39 нс ≈ 5.9 мс, туунду) — бул так HFT системалары REST кызматы көтөрө албаган чек араларды кантип көтөрө аларын көрсөтөт.

Бул бүт теңдик чекити окуясы бир сүйлөмдө: чек ара сиздин кыймылдаткычыңыздын канчалык тез экенине көңүл бурбайт; ал кесип өтүү сайын акы алат, ошондуктан сиз көзөмөлдөгөн өзгөрмөлөр — ар бир кесип өтүү канча иш алып жүрөт жана кесип өтүү эмнеден жасалган. Сынак сайын batch жасаңыз, GG жүз миңден ашат. Комбинация сайын batch жасаңыз, 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 мс артта калат (туунду: 2.276 − 2.010). Өлчөнгөн чек ара курамдаштары: ~2.0 мс'та толук-жүктөм рейси, 49 мкс'та чийки сериализация, бир нече байтта фрейм аталыштары — бүт чек ара эсебин ~2 мс дейли. Ошондуктан айырманын 99%'дан ашыгы чек ара таптакыр эмес. Бул эсептөө: IPC'ден тазаланган, Rust сервери numba 2.010 с'та жасаган сынакты жасоого ~2.274 с коротот — жөнөкөй Rust ядросу чийки эсептөөдө болжолдуу 13% жай (туунду).

Бул ачык абзацка татыктуу, себеби "аны Rust'та кайра жаз, ал тезирээк болот" "IPC сизди өлтүрөт" сыяктуу эле элдик ишеним. Эки ядро да акыр-аягы LLVM'ге түшөт — numba Python bytecode'ун ошонун аркылуу төмөндөтөт, rustc MIR'ды ошонун аркылуу төмөндөтөт — жана экөө тең, эң ыктымал, скалярдык циклдер катары иштейт: WMA'нын ички суммасы — калкыма чекиттүү редукция, аны LLVM numba'нын @njit демейкилери бербеген жана биздин порт сурабаган fast-math кайра байланыштыруу лицензиясысыз автоматтык түрдө векторлоо кыла албайт. Ошондуктан ~13% эки скалярдык LLVM-компиляцияланган цикл ортосундагы өлчөнгөн код генерациялоо айырмасы — жана себепти жарыялоонун ордуна, биз ачык шектүүнү текшердик. Табигый шектүү — Rust'тун коопсуз индекстөөсү: ысык WMA цикли ар бир массив мүмкүнчүлүгүндө чек араны текшерет, ал эми numba'нын @njit'и чек ара текшерүүсү өчүрүлгөн бойдон компиляцияланат. Ошондуктан биз ошол эле ядронун get_unchecked колдонгон эквиваленттик текшерилген версиясын түздүк — ысык жолдо эч кайда чек ара текшерүүсү жок — жана аны бешинчи архитектура катары өлчөдүк. Ал айырманы жаппады: 2.337 с (1.16x), чек ара-текшерилген куруунун 2.276 с'нан бир аз жайыраак. Гипотеза текшерилди, гипотеза четке кагылды. Билимдин ак ниет абалы: ~13% чыныгы жана кайра жаралуучу (10 жүгүрүүнүн медианасы, ~2% чегиндеги диапазондор), жана азыркы учурда дайындалбаган — тек ассемблер деңгээлиндеги профильдөө чече ала турган бөлүштүрүү жүрүм-турумундагы, цикл түзүлүшүндөгү же нускама пландаштыруудагы кандайдыр бир айырма. Сабак бүтүн калат: жөнөкөй Rust автоматтык түрдө жакшы numba'дан тез эмес, жана бекер эсептөө жеңиши болжолу менен сатылып алынган тил чек арасы эсептөө жоготуусу менен келиши мүмкүн. Туураланган Rust ядросу — алдын ала бөлүштүрүлгөн буферлер, ачык SIMD, комбинациялар боюнча агымдар — дагы эле белгини бурушу мүмкүн. Бирок бул профильдөө жана ядро иши менен чечиле турган эсептөө суроосу, ал эми бул изилдөөнүн суроосу — чек ара. Чек аранын жообу: бир жолу кесип өткөндө, байттарда, ал ~0.1% турат.

Ошентип, толук бүтүмдү чогулталы, жогоруда өлчөнгөн ар бир пункт.

Тилдер аралык кыймылдаткыч кызматы мунун баары аткарылганда жеңет:

  • Эсептөө артыкчылыгы чыныгы — тилдин беделинен эмес, өз ядрода өлчөнгөн. (Бизде далилденгенге чейин −13% болгон — жана ошол жетишпестиктин биринчи "ачык" түшүндүрмөсү сыноодо өлдү.)
  • Сиз ири түйүрчөктүү кесип өтөсүз — сынак сайын же fold сайын бир чакыруу, 14 мкс түбүнөн миңдеген эсе жогору, batch архитектуранын жалпы 1.13x'и (~0.1% чек ара) көрсөткөндөй.
  • Сиз бинардык тилде сүйлөйсүз — узундук префикстелген чийки массивдер, Arrow, 1.2 МБ'та 49 мкс турган memcpy классындагы кандайдыр бир нерсе; 66,243 мкс турган текст эч качан эмес.
  • Маалыматтар алдын ала жүктөлгөн — абалдуу сервер мегабайттарды кайра жиберүүнүн ордуна echo ийриинин ~16 мкс четинде жалгыз-параметр чакырууларын кабыл алат.

Ал кыймылдаткыч кызматтары адатта жайгаштырылгандай жайгаштырылганда жеңилет:

  • JSON/REST микросервиси — ар бир чакырууда, эки багытта тең 1348x сериализация салыгын төлөйт; chatty түйүрчөктүүлүктө бул 2 с тапшырмада 5.3 с коддоо.
  • Иш бирдиги сайынкы RPC — комбинация сайын бул жерде 107 мс турат жана жалгыз ар бир чакыруу 25,130 мкс эсептөөнү алып жүргөндүктөн аман калат; бар сайын бул кандайдыр бир иш болбой туруп ~2.1 с таза IPC, 2.0 с тапшырмада.
  • Чакыруу сайынкы spawn — ар жолу ~24 мс туруктуу нарк, сынак сайын бир жолу зыянсыз, комбинация сайын төлөнгөндө эки секундга жакын.

Башкача айтканда: ийгиликсиздикке учураган архитектуралар экзотикалык эмес. JSON REST кыймылдаткычы, символ-сайынкы ички процесс, gRPC-тик-сайын — бул "бэктест кыймылдаткычын бөлүп чыгаралы" чын мааниде кантип курулаарынын адилет саны. Элдик ишеним жалпы практиканын сүрөттөлүшү катары эмпирикалык түрдө жакшы негизделген жана табигат мыйзамы катары эмпирикалык түрдө туура эмес. Чек ара эч качан көйгөй болгон эмес. Аны кесип өтүүнүн демейки жолдору көйгөй.

Чек аранын пайдасына бир аргумент өз сүйлөмүнө татыктуу, себеби бул биз бул изилдөөнү таптакыр жүргүзгөн себеп. Жакшы долбоорлонгон чек аранын артындагы бир компиляцияланган ядро изилдөө сынагын жана лайв соода циклин тең тейлей алат — ошол эле бинардык файл, ошол эле арифметика, бит-биттен. Биздин бэктест-лайв паритет изилдөөсү изилдөө жана өндүрүш кыймылдаткычтары эки код базасы болгондо кантип алыстап кетерин каталогдоду; кыймылдаткыч кызматы ошол четтөө үчүн эң күчтүү структуралык дарылоо, жана бул изилдөө дарылоонун баасын ак ниет менен белгилейт: туура жасалганда, жалпы убакыттын болжолдуу 0.1% жана котормодо эч нерсе өзгөрбөгөнүн далилдеген эквиваленттик дарбаза. Ошол алмашуу — атайын процесс чек арасы бир-ядро паритетине алмаштырылат — бул сандар боюнча тийимдүү бүтүм. Туура эмес жасалса, ошол идея PnL'иңиз үстүндө отуруп өндүрүшкө 1348x сериализация салыгын жиберет.

Негизги жыйынтыктар

  1. Чек ара дээрлик акысыз; элдик ишеним өлчөөдө ийгиликсиздикке учурайт. Бүт 1.2 МБ жабуу сериясын Unix сокети аркылуу барып-келип жиберүү — толук талдоо жана кайра коддоону кошкондо — 2,043.4 мкс турат, 2.010 с тапшырманын болжолдуу 0.1% (туунду). Batched Rust-сокет-аркылуу архитектурасы жалпысынан 1.13x'ке жетет, жана ошол айырманын да ~99% IPC эмес.
  2. "Аны Rust'та кайра жаз" — эсептөө билдирүүсү — чек араны сатып алуудан мурун аны текшериңиз. Биздин сап-сабы менен Rust портубуз numba ядросунан ~13% жай эсептейт (туунду: 2.274 с каршы 2.010 с) — дайындалбай кала турган, эки скалярдык LLVM-компиляцияланган цикл ортосундагы кайра жаралуучу код генерациялоо айырмасы: биз ачык шектүүнү текшердик жана четке кактык, себеби чек ара текшерүүсүз эквиваленттик текшерилген get_unchecked куруусу тезирээк болгон жок (2.337 с каршы 2.276 с). Жөнөкөй Rust автоматтык түрдө тез эмес; туураланган ядро тез болушу мүмкүн — өлчөңүз, андан кийин чечим кабыл алыңыз.
  3. Чыныгы салык — текст. 150,000 флоатты JSON катары коддоо 66,243 мкс турат, чийкиде 49.1 мкс менен салыштырганда — 1348x, багыт сайын, чакыруу сайын, эки жакта тең төлөнгөн. Chatty JSON жайгаштыруусу 2 с тапшырмада 5.3 с коддоону жагат (туунду). Чек аралар аркылуу бинардык тилде сүйлөңүз: чийки фреймдер, Arrow — баа массивинде эч качан json.dumps эмес.
  4. Chatty жана chunky өлчөнөт, жана абалсыздык күнөөлүү. Маалыматтарды кайра жиберген комбинация сайынкы чакыруулар: batch'тын 1.13x'ине каршы 1.19x (+107 мс, туунду; echo ийриинин ~81 мс бир багыттуу божомолу андан ~25% төмөн түшөт, калганы чакыруу сайынкы фрейминг). Алдын ала жүктөлгөн абалдуу сервер ошол 80 чакырууну ар бирин ~16 мкс'та алмак — жалпы болжолдуу 1.3 мс (echo түбүнөн туунду). Параметрлерди жибериңиз, маалымат жыйнагын эмес.
  5. Түптү сыйлаңыз — жана түптүн тандоо экенин билиңиз. Биздин Python-Unix-сокет-аркылуу кесип өтүүбүз 14 мкс'та түбүнө жетет; комбинация сайынкы түйүрчөктүүлүк аны ~1,795x өтөт (чакыруу сайын 25,130 мкс эсептөө) — коопсуз. Бар-сайынкы үлгү (иллюстративдик иш-жүктөр-аралык чек: лайв тик-сайынкы кыймылдаткыч, бул сынак эмес) 2.0 с тапшырмада 150,000 × 14 мкс ≈ 2.1 с таза IPC төлөйт (туунду) — чексиз тез кыймылдаткыч менен да келгенде өлүк. Чакыруу сайын spawn кылуу туруктуу ~24 мс кошот (туунду). Жана ZigBolt сыяктуу атайын жасалган жалпы эс ташымалы бул машинада нативдик түрдө ~39 нс'та рейс жасайт — биздин сокет түбүнөн ~360x төмөн (туунду; нативдик Zig каршы Python кардары, ошондуктан муну түп ала турган диапазон катары окуңуз, жарыш катары эмес).
  6. Бир жолу, байттарда, маалыматтар мурунтан бар кезде кесип өтүңүз — жана чек ара сизге ~0.1%'ке паритет сатып алат. Изилдөө жана лайвди тең тейлеген, эквиваленттик текшерүү менен корголгон (PnL −5165.58, 57,029 бүтүм, тилдер арасында жана эки Rust курулушунда тең бирдей) бир ядро — кыймылдаткыч кызматынын ак ниет учуру. Ак ниет эмес учурлар — JSON, chatty, чакыруу-сайынкы spawn — IPC'ге анын беделин берген учурлар.

Толук эксперимент — Rust кыймылдаткычы, зым протоколу, echo жана сериализация текшерүү аппараттары, эквиваленттик дарбаза, жана бул макаладагы бир детерминисттик скрипттен кайра жаралуучу ар бир сан — 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 соода аналитикасы, рынок талдоолору жана платформа жаңылыктары үчүн биздин жаңылыктар бюллетенине жазылыңыз.

Биз сиздин купуялыгыңызды урматтайбыз. Каалаган убакта жазылымдан чыга аласыз.