Розробка C++17 бібліотеки для динамічних мережевих повідомлень без використання кодогенерації
Привіт, DOU! Мене звати Сергій. Я хочу поділитися досвідом створення інструменту, який народився під час пошуку альтернативи класичним підходам до серіалізації.
Коли ми розробляємо системи телеметрії, моніторингу або розподіленого керування, індустріальним стандартом найчастіше стають Google Protocol Buffers або FlatBuffers. Вони швидкі й ефективні, але мають суттєве обмеження — жорстку прив’язку до етапу компіляції. Якщо структура вашого повідомлення має формуватись безпосередньо в рантаймі, або ви прагнете уникнути кроку генерації коду з .proto чи .fbs файлів у вашому CI/CD пайплайні, стандартні інструменти змушують писати багато додаткового обхідного коду.
Для уникнення цих проблем я розробив MessageFrame — легку бібліотеку на C++. Вона дозволяє формувати структуровані мережеві повідомлення «на льоту» без попереднього опису схем та кодогенерації, використовуючи бінарний формат MessagePack для серіалізації.
Чому саме C++17? Реалії Embedded-розробки
Вибір стандарту C++17 був цілком свідомим рішенням. У сучасній high-level розробці заведено гнатися за C++20 чи C++23, проте реальність вбудованих систем (embedded) диктує зовсім інші умови.
Мені доводилося розробляти софт під плати ADALM-Pluto (популярний SDR-модуль на базі Xilinx Zynq) та використовувати для крос-компіляції тулчейн від Linaro. Виробники подібного заліза та супутнього SDK вкрай рідко оновлюють базове програмне забезпечення, тому старі версії компіляторів там — звична справа. У таких екосистемах стабільна підтримка C++17 — це той максимум, на який розробник може гарантовано розраховувати.
MessageFrame проектувалася так, щоб принести сучасні зручності та оптимізації (на кшталт std::string_view чи std::optional) туди, де новіші стандарти фізично недоступні через обмеження компіляторів виробника обладнання.
Концепція: дворівнева адресація параметрів
Замість проектування окремої C++ структури під кожен тип пристрою чи повідомлення, MessageFrame пропонує універсальний підхід до організації даних. Повідомлення будується на основі чіткої ієрархічної формули:
device -> parameter -> value
Це дозволяє адресувати будь-яку сутність у системі за допомогою двох рядків — ідентифікатора пристрою (subsystem/device) та назви конкретного параметра.
На практиці це виглядає так:
msg.add("sdr_1", "tx_gain", msgframe::VALUE(10.0));
msg.add("sdr_1", "sample_rate", msgframe::VALUE(2'000'000.0));
msg.add("sensor_alpha", "voltage", msgframe::VALUE(12.6));
Така структура дозволяє абсолютно незалежним компонентам або пристроям додавати свої метрики в один і той самий мережевий пакет, навіть не знаючи про існування один одного. При цьому на стороні отримувача немає потреби підтримувати та оновлювати складні класи або структури десеріалізації.
Внутрішня архітектура та оптимізація пам’яті
Найбільший технічний виклик для бібліотек без попереднього опису схем — це забезпечення високої швидкості роботи з пам’яттю. Звичайні динамічні асоціативні контейнери (як-от std::map чи std::unordered_map) при частому додаванні елементів фрагментують пам’ять через постійні heap-алокації.
Щоб обійти це обмеження, сховище параметрів у MessageFrame (HybridMessageMap) реалізовано як прозорий гібридний контейнер:
- Векторний режим (до 128 параметрів): Поки кількість параметрів у повідомленні невелика (межа задається через ліміт
SMALL_CAPACITY), усі дані зберігаються у звичайномуstd::vector. Це мінімізує алокації у кучі. Оскільки всі елементи розташовані в пам’яті один за одним, процесор може максимально ефективно завантажувати їх безпосередньо у надшвидкий L1 кеш CPU без зайвих промахів. Пошук виконується за лінійний час O(N), проте для невеликої кількості елементів (до кількох десятків) вектор працює швидше за будь-яку хеш-таблицю за рахунок відсутності накладних витрат на обчислення хешу. - Режим хеш-мапи (понад 128 параметрів): Якщо ліміт перевищено, контейнер автоматично і непомітно для користувача мігрує у
tsl::robin_map. Це open-source реалізація хеш-таблиці на основі алгоритму Robin Hood hashing. Вона забезпечує стабільну швидкість пошуку O(1), ефективно використовуючи процесорний кеш.
Додаткові інженерні оптимізації
- Small String Optimization (SSO): std::string ключі
deviceтаparameterутримуються всередині об’єкта рядка без виділення динамічної пам’яті, якщо їх сумарна довжина разом із роздільником вкладається в SSO-буфер вашої стандартної бібліотеки (зазвичай це ~15...23 байти). Тобто якщо у вас два ключі типу"sdr_1", "tx_gain"то для застосування SSO їхня конкатенація «sdr_1.tx_gain»має бути менше ~15...23 байт. Методи з суфіксом _flat: функції на кшталтadd_flatабоset_flatприймають уже сформований конкатенований ключ. Це дозволяє уникнути зайвого створення тимчасових рядків на гарячих ділянках коду, якщо унікальний ключ уже зберігається в системі в готовому вигляді.
Вибір Wire Format та компроміси
Для перетворення внутрішнього стану повідомлення у послідовність байтів (wire format) було обрано MessagePack. Його інтеграція дає дві важливі переваги:
- Отримані бінарні пакети є дуже компактними.
- Формат стандартизований, тому згенеровані бібліотекою повідомлення можна легко розпарсити на будь-якій іншій мові програмування (Python, Go, JavaScript), де є підтримка MessagePack, навіть без використання самої бібліотеки MessageFrame.
Звісно, архітектурно це свідомий компроміс. На відміну від FlatBuffers, який працює за принципом zero-copy (читання безпосередньо з буфера), MessageFrame вимагає етапу явної десеріалізації (deserialize()), щоб розгорнути байти назад у гібридну мапу. Це плата за рантайм-гнучкість та відсутність статичних .proto файлів.
Структура кадру: поділ метаданих та важких даних
Окрім дрібних key-value параметрів, мережевим пакетам часто потрібно передавати важкі сирі масиви (наприклад, спектрограми, IQ-семпли або кадри з камер). Пропускати такі об’єми через загальну серіалізацію параметрів — неефективно.
Тому архітектура MessageFrame розділена на три ізольовані частини:
- Header — фіксований заголовок для швидкої маршрутизації пакетів без повного парсингу всього тіла.
- Parameters — згадана вище гібридна мапа типізованих параметрів.
- Attachments — сирі бінарні масиви (
std::vector<uint8_t>), які передаються черезstd::moveі серіалізуються в обхід key-value сховища, що виключає зайве копіювання даних.
std::vector<uint8_t> raw_iq_data = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 };
msg.add_attachment("raw_iq_stream", std::move(raw_iq_data)); // Швидка передача без копіювання
Результати бенчмарків
Тестування проводилося на конфігурації: Intel Core i7-4702MQ, Windows 10, MSVC (Release білд).
Важлива деталь: наведені цифри відображають повний наскрізний цикл роботи з повідомленням, який включає як серіалізацію об’єкта в MessagePack, так і його повну десеріалізацію назад у внутрішню мапу параметрів.
- Малий пакет (Header + 4 параметри, без аттачментів):
- Середній час обробки повідомлення: 2.43 us;
- Пропускна здатність: ~412,000 повідомлень/сек;
- Середній розмір у MessagePack: 82 байти.
- Великий пакет (Header + 150 параметрів — після переходу в режим хеш-мапи):
- Середній час обробки повідомлення: 65.86 us;
- Пропускна здатність: ~15,100 повідомлень/сек.
Запрошую до обговорення
Проект є повністю самодостатнім, не залежить від сторонніх системних пакетів чи Boost і поширюється під ліцензією MIT. Всі необхідні залежності (msgpack-c та tsl::robin_map) підключаються як git submodules.
Оскільки мої заміри швидкодії проводилися на досить старому ноутбуці, мені було б дуже цікаво побачити, яку швидкість видасть бібліотека на вашому залізі. У репозиторії є готові бенчмарки, які дуже легко запустити за допомогою вбудованого скрипта. Якщо у вас є кілька хвилин, підніміть тести на своїх сучасних конфігураціях та поділіться результатами циклу (pack + unpack) у коментарях!
Також цікаво почути думку колег:
- Як ви розв’язуєте проблему динамічної структури повідомлень у своїх C++ проектах?
- Чи доводилося вам проектувати подібні гібридні контейнери під специфічні обмеження пам’яті та старі тулчейни компіляторів?
Посилання на репозиторій: MessageFrame
Буду радий вашим відгукам, результатам тестів та пул-реквестам!
47 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарівтрошки вкину про швидкий меспак і повільний жсон
news.ycombinator.com/item?id=36911668
хорошо, что есть документация, плохо, что выглядит она как слоп, чем и является
Якщо є конкретні зауваження до розділів чи прикладів коду — підкажіть, що саме варто покращити.
ParameterValue — а ось це ось можна було зробити шаблоним класом, з спеціалізацією спеціфічних орерацій для конкретних типів, як std::string ?
Якщо зробити ParameterValue шаблоном ParameterValue, ми втратимо його головну властивість — можливість тримати різні типи даних в одному списку в рантаймі.
Для компілятора ParameterValue і ParameterValue — це два абсолютно різні типи. Через це їх неможливо буде покласти в один загальний словник параметрів кадру (наприклад, std::unordered_map) без віртуальних функцій чи std::any, які потягнуть за собою зайві алокації та vtable.
Поточна реалізація — це саме універсальний рантайм-контейнер, який дозволяє зберігати числа, рядки чи bool у єдиній мапі без виділення пам’яті в купі для примітивів.
всі там були, але я повернувся до жорстких спек
чи це погано?
гнучкість це зручно коли весь проект під контролем одного розробника, а якщо це група, чи взагалі різні компанії ? код обросте «if key exist?» чи find як у прикладі, чи треба буде додати якусь версію протоколу і далі брати параметри звідти. зʼявиться спека рядом у .md файлі де буде сказано, що от такі датчики дають отакі ключі, а от ці — оті, і оп, у нас вже протобаф, але замість .proto у нас .md
ну і wire у нас буває різний, якщо це canbus мегабітний на всіх, то один вайбкодер який накине туди усі ключі своїй телеметрії покладе всю шину :)
Коментарі стають все більш зрілі))
Ви абсолютно праві в двох речах:
1. MessageFrame категорично не підходить для CAN-bus чи шин з лімітом кадру в 8 байт.
2. Якщо над системою працюють 5 різних компаній, то Protobuf з його .proto файлом — це порятунок від хаосу.
Ваш посил абсолютно правильний: срібної кулі немає, і гнучкість завжди купується ціною дисципліни команди.
MessageFrame створений для просторніших каналів (Ethernet, ZeroMQ, Shared Memory) під Linux Embedded / SoС, коли треба за 5 хвилин прокинути новий параметр телеметрії разом із мегабайтним буфером кадру, не перезбираючи пів проекту через protoc і CMake.
поки не «продано» ) якщо ми вже почали використовувати мережу чи shm, то є вирогідність того, що у нас компоненти не тільки на ++, от у мене стандарт для такого це zenoh, внутри прилада оптичні сенсори паблішать у черги кадри та метадані, і різні контейнери задач типу фреймера, контролера гімбала чи opencv можуть їх звідти брати та збагачувати метадані. і поки там зараз протобафи, розробник що на плюсиках, що на расті, що на пайтоні отримує та паблишить щось усім зрозуміло. ще на початковому етапі мені довелося захищати protobuf на пагорбі від набігу команди розробки які хотіли гнучкості і аргументи були от майже такі самі
і дуже хотіли json+messagepack.
до речі приклади з zenoh додали б гарного трекшену, може хтось зробить pr
p.s. це не критика проекту, а скоріше виявлення паттерну поведінки розробників, але одні здалися після тиску архітектора, а дехто пішов далі і таки зробив свій гнучкий метод )
це що хня?
are you jealous? ))
ЗЫ: обісраться простіті якщо його гуглити то гугло ші сприймає його як особисте питання а не як текст для аналізу )) тупа скотиняка сірьозно ((
щоб не писати довго що виглядє принцип управління проектом як «хто в ліс, хто по дрова», доречніше було вжити «хня», як заповідав великий Оккама з бритвою жілєткою відтинаючи зайві сутності
P.S.
до чого тут «обзавидувався»?
кому саме: першим чи другим?
а чорт йо зна а до слова
нічоссі а їх там багато? ((
... о кажеться порахував їх
(він і тебе порахував! (к))
№ 1
№ 2
а чорт йо зна хай буде качечкам! всі хочать качечками!
Замість union ValueUnion можете використовувати std::variant<...>, ще можете подивитись на std::apply і overload паттерн, можливо це зменшить кількість кода, а може і ні
Нарешті конструктивний фідбек))
Я розглядав std::variant + std::visit на початковому етапі. Проте відмовився від цього підходу з двох причин:
- серіалізація в MessagePack: бібліотека msgpack-c з коробки не розпаковує std::variant без написання додаткового адаптера. Власний типізований юніон виявився набагато простішим у нативному мапінгу на бінарні типи MsgPack;
- розмір бінарника та генерація коду: на старіших тулчейнах (типу GCC 7.x/8.x під ARM Cortex-A) шаблонний код std::visit з overload паттерном генерує відчутно більше коду й таблиць викликів, ніж прозорий switch за типом значення.
Але ідея з std::apply та overload дійсно гарна для елегантного публічного API! Можливо, поекспериментую з цим у наступних ітераціях.
і далі
Хотілось би побачити результа на ESPS32 ESP8266 та порівняти з JSON, напр.
cJSON ArduinoJson json11 nlohmann/json на них же
— які динамічні структури (без відомої наперед схеми, незрозуміло, про яку «динаміку» мова)?
— що таке «подібні гібридні контейнери»
— про які саме «специфічні обмеження пам’яті» (це скільки в кілограмах/фунтах)
— що є «старі тулчени компіляторів» (під Z80 чи старіше)?
P.S.
це в «малих вбудованих системах», на якому кристалі?
P.S.
а що нам треба максимальна швидкодія, чи економія пакета повідомлення в 20%..30%
так і не зрозумів для наскільки малих вбудованих систем?
з тексту догадуюсь, що це
але це мені в числах нічого не каже, а шукати параметри «популярного SDR модуля» лінь
Дякую за коментар і запитання! Бачу, виникло непорозуміння щодо класу систем, під які розроблялася бібліотека. Зараз розберемось:8-бітний чи 32-бітний мікроконтролер (як ESP32 або STM32), а повноцінний SoC із двоядерним ARM Cortex-A9 (667 МГц, 512 МБ DDR3) під управлінням Embedded Linux.
1. Клас систем (Embedded Linux / SoC vs Microcontrollers)
Залізо рівня ADALM-Pluto (Xilinx Zynq-7000) — це не
Тут є і L1/L2 кеші процесора, і віртуальна пам’ять, і можливість ганяти сотні мегабіт IQ-даних на секунду. Для ESP8266 з кількома десятками кілобайт RAM цей інструмент просто не призначений — там дійсно краще брати ArduinoJson.
2. Щодо застарілих тулчейнів та C++17.
Виробники промислових SDR та FPGA-платформ (Xilinx/AMD, Analog Devices) часто заморожують свої BSP та Linux SDK на конкретних версіях GCC (наприклад, GCC 7.x—8.x у старих Vivado/Petalinux). Оновити компілятор у таких вендорських збірках — це окремий квест із ризиком зламати драйвери чи бінарні блоби. Тому C++17 обрано як розумний компроміс, який гарантовано працює на цих тулчейнах.
3. Про «динаміку» та структури без схеми.
Під динамікою мається на увазі ситуація, коли склад параметрів пакета вирішується в рантаймі (наприклад, конфігурація регістрів SDR, де набір полів змінюється залежно від підключеного плагіна або режиму роботи) і немає можливості перекомпілювати .proto схеми під кожен новий параметр.
4. Про швидкодію, MessagePack та JSON.
JSON — текстовий формат, який вимагає важкого парсингу рядків та алокацій під кожен ключ. У задачах обробки сигналів або телеметрії на сотні тисяч пакетів/сек текстовий парсинг створює величезний оверхед на CPU. MessagePack дає бінарну компактність і швидкість, але зберігши рантайм-гнучкість.
тобто «винайдений велосипед» під пропроітарний тулчейн
замість protobuf, питань нема
без тестів хіба прийняти навіру, що для такого конткретного випадку, хз, може і так, не буду сперечатись
вперше чую що AD виробляє FPGA
1. Щодо Analog Devices та FPGA:
Плата ADALM-Pluto розроблена саме Analog Devices, але всередині вона побудована на SoC Xilinx Zynq-7000 (де чип Xilinx поєднує ядра ARM Cortex-A9 та FPGA-матрицю). Ніхто й не стверджував, що AD робить власні FPGA — мова йшла про готове залізо та супутній SDK від AD/Xilinx.
2. Щодо «велосипеда»:
Жоден інструмент не виникає на порожньому місці. Протоколи Protobuf чи FlatBuffers — чудові, але вони вимагають кодогенерації й статичних схем (.proto). Якщо задача вимагає рантайм-гнучкості (динамічного додавання параметрів) без зав’язки на CI/CD з генерацією коду під застарілі вендорські тулчейни — береться бінарний MessagePack і будується зручна обгортка. Це стандартний інженерний компроміс під конкретні вимоги архітектури.
3. Щодо бенчмарків і JSON:
Код бенчмарків відкритий у репозиторії. Будь-хто може зібрати його з nlohmann/json або ArduinoJson і порівняти наскрізний цикл (pack + unpack) бінарного MessagePack проти парсингу тексту. Різниця в алокаціях та накладних витратах CPU там очевидна.
1 К.О.
2. Є реалізація на чистому С, якщо С++20 зановий або залежність буст неприйнятна (думає є і інші реалізації, але лінь шукати)
3. There’s No Time To Explain! ©
Дякую, але це мало би робитись автором на етапі порівняння рішень, а не пропонуватись кому як постфактум.
1. msgpack-c і так використовується всередині MessageFrame як підкладка під wire format. Моя бібліотека — це не новий бінарний формат, а високорівневий менеджер типів, гібридного сховища (HybridMessageMap) та zero-copy аттачментів.
2. Порівняння з JSON / Protobuf додано у текст статті як концептуальна різниця підходів. Детальні бенчмарки на різних платформах — це чудовий матеріал для окремої публікації.
Я колись писав кастомний протокол зверху USB bulk для керування регістрами DSP. Система була:
Linux(MIPS|ARM64) <-USB-> ARMCortexM0+ <-SPI+I2S-> DSP
Жодних проблем з кастомним протоколом не сталося, тому не розумію суті статті.
Чудовий приклад із власного досвіду!
Але ви описуєте трохи іншу задачу: для статичного керування регістрами конкретного DSP через USB/SPI структура даних відома заздалегідь і не змінюється в рантаймі. У такому випадку простий кастомний бінарний протокол або простаC-структура — дійсно найкраще й найпростіше рішення.
Суть MessageFrame зовсім в іншому — це інструмент для ситуацій, коли:
- структура повідомлення динамічна і формується в рантаймі (наприклад, конфігурації та телеметрія від багатьох різних модулів, набір яких заздалегідь невідомий);
- потрібна міжплатформна сумісність (завдяки MessagePack ці дані можна одразу прочитати в Python/Node.js/Go без написання кастомних парсерів під кожну мову);
- потрібно передавати «важкі» бінарні блоки (IQ-потоки, спектрограми) без зайвого копіювання пам’яті поруч із метаданими.
Для жорстко фіксованого набору регістрів конкретної мікросхеми складні абстракції справді не потрібні. Але коли система розростається, кастомні бінарні протоколи стають важкими в підтримці та розширенні.
Не було структури даних — протокол дозволяв довільні записи, читання, та бітові операції в/з пам’яттю та регістрами DSP. Також була підтримка масивів, щоб записувати шматки пам’яті чи групи регістрів. І була підтримка змішаних операцій (точковий доступ до декількох регістрів в одному меседжі).
Сам протокол обмежував регістри лише8-бітною нумерацією та адреси пам’яті — 16-бітною. Себто, за вашим означенням він є динамічним, і меседжі формуються в рантаймі.
Виходячи з того, що протокол був описаний в сішних хедерах, його можна було підключити до будь-якої іншої мови, що вміє працювати з масивами байтів. Або за бажанням можна було б скомпілити код, котрий ним користувався, і підключити як бібліотеку до іншої мови — для покращення швидкодії.
«Важких» блоків в мене не було, бо на CortexM0+ 8 кілобайтів оперативки. Але цей же протокол оперував двома двоспрямованими голосовими каналами по 64 Кб/с паралельно до роботи з регістрами DSP. Себто, замість бінарних блоків були ріалтаймові потоки.
Логіка вашого рішення тепер повністю зрозуміла.C-хедери, писати сирі зсуви байтів чи збирати C-бібліотеки через FFI.
Описаний вами протокол — це ефективний низькорівневий протокол прямого доступу до пам’яті та регістрів (фактично аналог команд POKE/PEEK або розширеного Register Bus). Для Cortex-M0+ з 8 КБ RAM та прямої маніпуляції регістрами DSP це, мабуть, ідеальний підхід.
Але між нашими рішеннями є важлива різниця в рівні абстракції:
«Прямі адреси vs Семантичні ключі».
У вашому випадку клієнт має чітко знати карту пам’яті DSP (що за адресою 0×12 лежить саме gain). У MessageFrame адресація є семантичною («sdr_1» -> «tx_gain»). Це рівень високорівневих сервісів та телеметрії, де отримувач не повинен знати фізичні адреси регістрів заліза.
Self-describing формат: Оскільки MessageFrame базується на MessagePack, будь-який зовнішній софт (Python/Node.js/Go) розпаковує ці дані штатними стандартними бібліотеками msgpack без необхідності підключати
Тобто ваш протокол вирішує задачу «як швидко змінити байти в регістрах конкретної мікросхеми через USB», а MessageFrame — «як обмінятися типізованими структурованими повідомленнями між абстрактними модулями системи без кодогенерації». Це просто абсолютно різні рівні абстракції під різні задачі.
Ось натрапив на критику MessagePack zguide.zeromq.org/.../#Serialization-Libraries
У цьому розділі ZeroMQ Guide автори справді критикують MessagePack (як і Protobuf/JSON, тобто все) за додатковий оверхед порівняно з чистимиC-структурами та memcpy.C-структур ідеальний за швидкістю, але крихкий при зміні версій даних або передачі динамічних контейнерів. MessagePack дає гнучкість бінарного формата без важкої кодогенерації.
В MessageFrame пакувальник MessagePack свідомо обрано як базовий двигун серіалізації завдяки
компромісу між гнучкістю та оверхедом. Чистий memcpy
MessageFrame загортає роботу з MessagePack у зручний заголовочний C++17 wrapper, мінімізуючи накладні витрати на рівні API для типових сценаріїв.
Тож зауваження авторів з книги цілком справедливе щодо оверхеду будь-яких універсальних бінарних форматів, але для задач MessageFrame це свідомий вибір на користь динамічності та відсутності кодогенерації.
Ви не читали текст за посиланням
Якраз навпаки — ту неосяжну главу з ZeroMQ Guide я прочитав від початку до кінця, хоч і довелося заради цього пожертвувати кількома годинами нічного сну.
Автори ZeroMQ там проповідують патерн Cheap or Nasty, закликаючи не змішувати в один універсальний формат важкі дані (Data) та легкі команди (Control).
Іронія в тому, що MessageFrame робить саме це, просто адаптовано під Embedded-реалії. Дивіться, як цей «брутальний» патерн працює в коді:
1. На вході (чистий zero-copy):
Завдяки std::move мегабайтні буфери спектрограм чи IQ-семплів потрапляють у бібліотеку взагалі без жодного копіювання пам’яті.2. На етапі серіалізації (Cheap vs Nasty):
Жодного оверхеду на обхід структур, парсинг чи кодогенерацію. Важкі дані просто зливаються у вихідний буфер одним швидким системнимкадр ділиться на 3 ізольовані частини.
HeaderтаParameters— це наш «Cheap» шар (динамічна рантайм-телеметрія). А для «Nasty» шару (Attachments) дані йдуть в обхід логіки серіалізації через прямий запис у бінарне тіло:memcpy.Так, це одне лінійне копіювання для фінального кадру. Але на Embedded Linux процесорі це мізерна плата за те, що розробник на Python чи Go на іншій стороні може розпарсити цей пакет стандартною бібліотекою MessagePack, навіть не підозрюючи про існування MessageFrame.
У підсумку маємо чітку ізоляцію: динамічні метадані пакуються гнучко, а сирі масиви — максимально «брутально» через лінійний
memcpy. Це класичний інженерний компроміс, який повністю вкладається у філософію, описану авторами ZeroMQ.чому не робити зеро-копі між процесами?
По-перше, з тексту випливає, що читали не ви, а ЛЛМ. А вона це робить швидше, ніж за години сну.
По-друге, ви знову випустили основний момент, чому автори ZeroMQ називають протокол для високонавантажених даних Nasty.
ще потрібно розрізняти крайні випадки
— багато дрібних повідомлень (напр., ретрансляція з CANbus)
— передача великого об"єму даних (мгеабайти)
і різних стратегії для них, які несумісні при бажанні досягнути максимальну пропускну здатність каналу
Саме про це й був розділ за посиланням. ЛЛМ не може таке осягнути, а автор топіку не хоче відповідати на коменти самотужки.
Замість того, щоб шукати сліди ЛЛМ у коментарях, краще підкажіть: як у вашій практиці передачі мегабайтних потоків через мережу ви обходилися без фінального збирання пакета в лінійний буфер перед відправкою в сокет?
Я не займався мережами.
Натомість, ви не читали, чому автори ZeroMQ розділяють два види трафіку.
думаю, автор не працював з телеметрією в так званому автопромі, де з одного боку потрібно вистрілює як з кулемета не одна шина CAN, а з іншого боку можше щось іти по «повільному УАРТ/LIN», і потрібно то все організувати та прокинути в рілтаймі (напр. одним блобом в секунду) кудись далі (в інший сервіс, в БД якщо нема зв«язку, чи на хмару)
в нього своя специфічна задача, і він її рішає/вирішив винайшовши велосипед під «старий компілятор тулчейна»
В автопромі я не працював. Але бачу намагання натягнути транспортний фреймворк для великих мережевих пакетів (Ethernet/Wi-Fi) на специфіку повільних UART/LIN ліній мікроконтролерів. Ніби автор питання не розуміє різниці між можливістю упакувати дані та доцільністю накладних витрат.
не зрозуміло контексту в якому веде автор боротьбу за оптимізацію обміну даними
WAT?
в чому проблема твого FPGA з ARM процесором, він не здатен швидко пакувати/архівувати дані, не здатен перекинути через механізм зерокопі з одного ядра на інше, чи DMA чи як?
ти так і не відповів, що в тебе за «мережа», там все в межах однії плати і та фізичної шини RAM, чи потрібно через wired Ethernet пересилати на супутній комп"ютер?
тут не іде мова про автопром (CAN, UART, SPI whatever), іде мова про патерни передачі даних: часто але байтами, чи повільно але мегабайтами, чи і то і друге, надійна доставка чи можуть бути втрати, рілтайм (мягкий/твердий) чи ні без нього
Книга закликає не парсити мегабайти універсальними інструментами — ми їх і не парсимо, а проганяємо лінійним memcpy в обхід логіки MessagePack.
Автори ZeroMQ кажуть, що трафік з даними треба архівувати, і алгоритм архівування залежить від природи даних. Він включатиме дедуплікацію та стиснення.
Ви натомість робите memcpy без стиснення кодеком.
Архівування — це задача транспортного шару або прикладного рівня, а не самої бібліотеки серіалізації. Зашивати архівування намертво всередину MessageFrame немає жодного сенсу, оскільки природа даних буває різною (наприклад, уже зашифрований або стиснений апаратним кодеком потік повторно не стискають). Бібліотека формує кадр, а архівувати його чи гнати як є — вирішує транспорт.
Так, тому ваша розробка потрапляє до uncanny valley, як і описано в книжці про ZeroMQ:
— Control plane потребує простого читаємого протоколу, на зразок JSON або HTTP.
— Data plane оптимізують руками під конкретні типи даних в стрімі.
Ви не надаєте ані одне, ані інше, як я зрозумів.
Теорія з розділу про серіалізацію виглядає переконливо, але раджу дочитати книгу про ZeroMQ до кінця.
Якби ви користувалися ZMQ в реальних проектах, то знали б, що її можливостей достатньо для гнучкого проектування без рознесення метаданих (Gain/Freq/Channel/SamleRate) та тіла кадру (IQ) на різні сокети. Філософія ZMQ ідеально вирішує це через multipart повідомлення: за допомогою прапорця ZMQ_SNDMORE ми атомарно пакуємо Header, Parameters та Attachments в один логічний кадр, який на PUB-SUB сокетах чудово розподіляється по підписці без ризику десинхронізації.
Але ZMQ — це суто транспорт, який пересилає сирі бінарні фрейми й поняття не має про їхній вміст. MessageFrame якраз і створювався як високорівнева обгортка, яка формує «на льоту» внутрішню структуру цих фреймів без кодогенерації та статичних схем. Завдяки цьому колеги на Python/Go можуть розпарсити вміст стандартним MessagePack-декодером, не пишучи кастомні парсери зсувів байтів.
Я вважаю, що ваш підхід є сліпим слідуванням догмам з окремої глави. Моя бібліотека є втіленням балансу компромісів, що й вимагає сучасна прагматична інженерія.
Дякую за дискусію.
так яка мережа, внутріплантна між процесами (чому тоді не зеро-копі), чи іде на хмару чи в локальну іншому набортному компутеру?
у вас, я так розумію іде стрімінг, чому не робити ключові фрейми, і не передавати дельту (різницю), чи може бути UDP чи потрібна гарантована доставка?
у вас що транспорт TCP?
є вимоги рілтайма?
не пробували zip-увати щоб зменшити трафік?
для мережі в чанк даних більше 10К ліпше зжати перед передачаю,
якщо багато дрібних (напр. з CAN шини), то ліпше зібрати в 1 з розміром біля 1Кб і передати без зжимання
і т.д. з різними стратегіями оптимізації, залежно від умов та важливості даних. Можливо для стрімінга один механізм, а для службових повідомлень інший?
ну, ми ж це вже обговорили вище: MessageFrame категорично не підходить для CAN-bus
чому? він не здатен «серіалізувати обгорнуті дані»?
звідки така категоричність? не підходить тільки у твому випадку чи взагалі?
У вас інлайн код поїхав в тексті.
Дякую, поправив.