Потоки бітів у C++
Читання і запис бітів у потік — типова задача. У цій статті оглянуто методи, з якими стикався автор, і пропонується один в стилі метапрограмування С++. Серед традиційних найбільш помітні такі:
- Застосувати побітові операції — рутинна робота, проте згенерований код виходить ефективний
- Використати бітові поля — ефективний код, менше рутини, але початковий код може бути заплутаний і не дуже портовний між платформами з різним порядком байтів
- Створити власний клас, який би відслідковував читання окремих частин байтів під час виконання — у вебі є багато прикладів.
Ось інший підхід, який настільки ж ефективний, як і ручний код, але набагато зручніший. Хай компілятор наперед знає якомога більше, і матиме змогу генерувати спеціалізований оптимізований машинний код. Розгляньмо файл з початковим кодом BitStream.cpp. В ньому визначені дві шаблонні функції:
// Прочитати 'count' бітів з пам’яті за адресою біту 'offset'.
// Компілятор легко зоптимізує згенерований код.
template <unsigned offset, unsigned count>
inline uint32_t Read(const uint8_t *src, uint32_t accum = 0);
// Записати задане value у пам’ять за адресою біту 'offset'
// шириною 'count' бітів
template <unsigned offset, unsigned count>
inline void Write(uint8_t *dst, uint32_t value);
Для кожної комбінації параметрів offset
і count
, генерується цільова
функція, довжиною всього кілька інструкцій.
Використовувати їх легко. Наприклад, розгляньмо розбір заголовка
RTP:
const uint8_t *packet = ...;
uint32_t V = Read<0,2>(packet);
uint32_t P = Read<2,1>(packet);
uint32_t X = Read<3,1>(packet);
uint32_t CC = Read<4,4>(packet);
uint16_t seqNum = Read<16,16>(packet);
В якості ілюстрації, можна подивитися згенерований код у compiler explorer (посилання):
Недоліком такого підходу, на мою думку, є те, що згенерований код залежить від налаштунків оптимізації компілятора. Таким чином, налагоджувальна збірка може працювати помітно повільніше.