Обновить

Комментарии 21

спасибо большое за ваш труд, прочитал статью с огромным удовольствием)

кстати подобное шаманство работает не только в богоподобном си, но и я в других языках просто нужно знать подробности реализации. В том же Go можно структуру в байты напрямую похожим образом, без лишних телодвижений просто используя сам указатель на структуру и тут то важно правильно расположить что бы работало.

Есть тонкий момент: до С++23 аккуратно подложены грабельки:

Member access specifiers may affect class layout: the addresses of non-static data members are only guaranteed to increase in order of declaration for the members not separated by an access specifier(until C++11)with the same access(since C++11).

https://en.cppreference.com/w/cpp/language/access.html

Из этого следует, что гарантия последовательного расположения есть только в рамках одной секции. Стало быть, примеры, начиная с упаковки "bool", следует поправить, увеличив количество геттеров и сеттеров, иначе размер может и не получится.

Впрочем, я не знаю ни одного компилятора, который решился бы на подобные переупорядочения.

Ухх, какой подлый нюанс :) Он мне был не известен. Выходит, чтобы теоретически обезапаситься, есть два пути:

  • Пойти вашим путем и тотально замести все поля под private

  • Наоборот занести все под public, посыпать "приватные" поля комментариями "НЕ ТРОГАТЬ", а голову посыпать пеплом

Мне вот интересно, если есть компилятор, который пользуется этой лазейкой, то как мог бы выглядеть порядок полей у класса с public и private секцией? public в памяти четные, private - нечетные? :)

Компилятор вполне мог поменять секции местами, чтобы уменьшить паддинг,
либо наоборот, чтобы выровнять размер структуры с размером кеш линии.

имеет следующее расположение полей в памяти:
..
Итого — структура занимает 40 байт

А вот нифига. В некоторых аппаратных платформах структуры выравниваются ВСЕГДА на 4 байта. Включая bool. Так что попытка записать структуру из C/C++ кода на диск (как область памяти по указателю и sizeof(..)) на одной платформе и прочитать ее в память "как есть" (наивный подход) на другой платформе приведет к проблемам. Плавали... знаем. Ща поменьше стало (sparc RIP), но все равно мир не заканчивается x86.

Так что, упаковка/распаковка данных из локального формата перед запись/чтением куда либо (диск, БД) - это вообще стандарт (должно быть).

Ладно игра... А когда в WAL Postgre все данные выровнены на границу 4-х байт (понятно конечно откуда ноги растут) и даже на первый взгляд можно получить упаковкой экономию размера файлов от 1-15% (зависит от структуры/формата полей таблиц. Меньше прикладные данные - больше экономия на заголовках в процентах)
И все ради "а побыстрее обрабатывать" (наверное)?
Opensource ПО блин. И сейчас на PG огромные системы пытаются переносить. А по факту разработчиков основных PG можно по пальцем рук пересчитать.
И банально некогда им оптимизировать.

Статья всё таки про эффективное использование памяти и кешей, а не про сериализацию. Так то надо помнить, что размер int не фиксирован, big endian никуда не делся и т.д.

да. Скорее всего ОЗУ. Но это явно нигде не сказано и та же проблема касается сохранения на диске.

Doom из прошлого века, который сейчас запускают даже на принтерах, хранит ресурсы в файле wad. Это zip-подобный архив.

Почему в архиве? Потому что в прошлом веке жёсткие диски были очень медленные. Операция

  1. прочитать маленький файл с медленного диска

  2. распаковать его в большой в памяти

была быстрее чем сразу прочитать несжатые ресурсы с диска во время игры.

Возможно и в задаче из статьи использование любого агоритма сжатия будет гораздо эффективней.

Есть еще причина хранить ресурсы из большой кучи файлов в собственном файлом контейнере: из-за того отдельные файлы занимают на диске объем всегда кратный размеру кластера\сектора файловой системы.

Я прикрылся параграфом "Дисклеймеры, оговорки" :) Но в целом согласен - зоопарк возможных платформ бесконечен, поэтому ничего точного в абсолюте быть не может. Но это тем не менее не мешает людям рассуждать о порядке полей, padding и пытаться уменьшить свои структуры.

Я даже какое-то время поддерживал пропиетарный компилятор, где char был равен машинному слову, потому что почему бы нет, поэтому не понаслышке знаю, что все действительно возможно.

Ну а читать/писать сырые данные между платформами - это совсем иная история, статья не про это, тут надо писать отдельную. И ее лучше писать вам :) у вас, кажется, обширный опыт в этой теме. Я могу козырнуть лишь #pragma pack, но подозреваю, что это не панацея. Особенно если еще есть разница LE/BE

#pragma pack нужен скорее чтобы иметь стабильное ABI, когда структуры фигурируют в интерфейсе; для записи в файл (особенно переносимый между платформами) всё равно недостаточно.

UPD: я посчитал, что статья была бы неполной, если не привести известные мне способы увидеть глазами memory layout интересующей нас структуры. Заинтересованным читать главу "Appendix I. Узнаем memory layout".

Буду рад, если вы поделитесь своими способами - хорошими, плохими, злыми - любые подойдут.

Получил огромное удовольствие, спасибо!

Сам сталкивался с подобной проблемой когда в C# делал универсальную структуру для хранения векторов и скаляров. Структура имела размер 64 байта и должна была хранить длину вектора, тип элементов и сами значения. Проблема возникла с типом decmial, который весит 16 байт и в количестве 4 штуки занимает всё место.

Выкрутился так

Поля с размером и типом впихнул в 1 байт и разместил его там, где у decimal всегда нули. Да, у этого типа реально некоторые биты ВСЕГДА равны нулю. После этого извратил хранение длины и типа так, чтобы при длине 4 и типе decimal этот байт был всегда равен нулям. Профит: при хранении других типов (которые 8 байт и меньше) данные до туда не доходят из‑за ограничений по длине, а при четырёх decimal хранение данных, длины и типа в одном месте не противоречат друг другу.

достойные извороты) прятать информацию в чужих битах - самое приятное

Не понял, откуда гарантия, что младшие биты свободны? Указатель же void*, значит не подразумевает никакого выравнивания данных. По указателю может храниться например строка или бинарный массив, не выровненный на 4 байта. А то, что сам указатель в структуре выравнивается, к этому вообще отношения не имеет.

По поводу битовых полей не понимаю, зачем их избегать. Вроде они везде одинаково работают кроме big/little-endian. Я проверял в compiler explorer на распространённых платформах и всех основных компиляторах. Кажется, только были какие-то нюансы между big и little-endian, но лучше их учесть, чем городить портянку с private и геттерами-сеттерами.

А для сериализации можно завести тип с перевёрнутым порядком байт для big-endian архитектур, не переворачивая для little-endian. Назвать типа uint32LE/uint16LE. Тогда можно будет просто писать структуры на диск.

>По поводу битовых полей не понимаю, зачем их избегать

Кроме BE/LE там достаточно своих приколов. Приведу пример из жизни.

Оказалось, что один проект несколько лет работал неверно, не сообщал об ошибке, когда надо было. Когда это обнаружили, не сразу поняли, в чём прикол.

#define ERR_CODE_CRITICAL (1 << 3)
...
data.result = ERR_CODE_CRITICAL;

Вот примерно так в поле структуры записывался код результата. Но внезапно оказывалось, что в поле result после этой записи — 0 (код для ERR_CODE_OK) вместо ожидаемого кода ошибки. Почему?

А потому что автор изначального кода отвёл на поле result всего один бит, описав его так:

typedef struct {
    ...
    int32_t result: 1;
} DATA;

Кодов тогда было всего два, ноль и один, оно работало как задумывалось.

А потом в какой-то момент кто-то решил добавить других значений, и всё сломалось.

Компилятор ошибками не ругался (инт в инт пишется же, всё окей), и никто ничего не заметил.

И вот сидишь ты в отладке, смотришь на этот код, наводишь курсор на поле result, тебе IDE услужливо подсказывает: тип int32_t, всё окей! Шаг делаешь, туда восьмёрка пишется, а получается 0. Магия! )

Т.е. чтобы изначально врубиться, что в этой строке может быть косяк, нужно непременно полезть в описание структуры и вручную глазами смотреть, что за поле такое, и как объявлено. Отличная фича для стрельбы себе в ногу, в общем.

ну извините, тут явно не проблема в битовых полях) Просто ошибка. А так ничего не мешает вместо 8 булов сделать структуру из 8 1 битовых плей и при необходимости приводить ее к чару или банальный юнион с уинт8 и структурой 1 битовых полей использовать.

"Просто ошибка" — это когда можно просто посмотреть на код и увидеть, что там ошибка. А здесь — нельзя просто посмотреть на код и увидеть, что там ошибка. Можно столкнуться случайно, когда уже поздно (что и произошло).

Сталкивался я однажды с кодом, где были переменные вида _fpressure, _fPressure,fpressure,fPressure и все 4 они в разные моменты времени могли либо приравниваться между собой либо использованы в промежуточных расчетах. Там так же много лет была ошибка ибо в одном месте стояла не та переменная. Вот вроде можно просто посмотреть и увидеть, а вроде много лет смотрели разные люди этот код и никто не видел, ну плевались что то вроде какой дурак так написал, но никто не переделывал, работает же. Так же и в вашем примере. Один человек выделил 1 бит, другой не посмотрел и вылез за пределы. Но это не означает что битовые поля это неудобно или что они могу работать как то не так. Их можно как то не так использовать, это да.

Так никто и не говорит, что битовые поля это неудобно. Удобно. Но там, где нужно максимально надёжный и портабельный код иметь (а проблем с ним — не иметь), лучше их не юзать вовсе. У нас в конторе мы их вообще запретили, из-за специфики. Самолёты прогаем, код должен на куче разных архитектур работать, с разной разрядностью и разным порядком байтов, причём вперемешку (а в протоколах ой как хочется, бывает, битовые поля заюзать). Но многолетнаяя практика показала, что это в итоге выливается в боль. А мы не хотим страдать, хотим новые крутые фичи пилить, а не внезапно всплывающие старые баги отлаживать )

>а вроде много лет смотрели разные люди этот код и никто не видел

Судя по описанию, просто не вчитывались, потому что код страшненький был, это обычное дело. А в приведённом мной случае — код простой и читаемый, от которого не ждёшь подвоха.

Поддержу комментатора выше, битовые поля для отдельных битов вполне безопасны. Проблемы начинаются с переносом многобитных полей между BE/LE платформами.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации