====== Упаковка значений вместо битовых полей ====== //Написано ИИ:// В языках семейства C битовые поля появились как попытка описывать аппаратные регистры и сетевые пакеты «в лоб», через структуру. struct { uint32_t dlc : 4; uint32_t ide : 1; uint32_t : 3; uint32_t fi : 8; }; Но за внешней простотой скрывается несколько неприятных особенностей: * порядок битов зависит от реализации; * выравнивание зависит от компилятора и ABI; * поведение при чтении и записи часто платформозависимо; * код плохо переносится; * компилятор может генерировать неожиданно тяжёлые операции. В языках вроде Oberon подход обычно другой: вместо специальных синтаксических конструкций предлагается работать с числом как с упакованным контейнером значений. Это оказывается не только проще и переносимее, но и значительно более общим методом. ===== Идея: число как контейнер ===== Допустим, есть 32-битное значение: 31 0 +--------+---+--------+ | fi |ide| dlc | +--------+---+--------+ Пусть: * ''dlc'' занимает 4 бита; * ''ide'' занимает 1 бит; * 3 бита пропущены; * ''fi'' занимает 8 бит. Тогда всё это можно хранить в одном ''INTEGER''. Вместо специальных битовых полей используются обычные операции: * ''MOD'' * ''DIV'' * ''+'' * ''-'' ===== Формирование полного значения ===== Пусть: dlc = 9 ide = 1 fi = 0ABH Размещение: ^ Поле ^ Смещение ^ | dlc | 0 | | ide | 4 | | fi | 8 | Тогда: MODULE Example; IMPORT Out; PROCEDURE Go*; VAR dlc: INTEGER; ide: INTEGER; fi: INTEGER; v: INTEGER; BEGIN dlc := 9; ide := 1; fi := 0ABH; v := dlc + ide * 10H + fi * 100H; Out.Int(v, 8); Out.Ln END Go; END Example. Получится: 0000AB19 Потому что: dlc = 9 -> 00000009 ide = 1<<4 -> 00000010 fi = AB<<8 -> 0000AB00 ------------------------ 0000AB19 ===== Чтение отдельной части ===== ==== Извлечение dlc ==== Поле занимает младшие 4 бита. dlc := v MOD 10H; Почему это работает? v = q * 16 + r где остаток ''r'' всегда находится в диапазоне ''0..15''. То есть ''MOD 10H'' отсекает всё старшее. ==== Извлечение ide ==== Сначала сдвигаем значение вправо делением: ide := v DIV 10H MOD 2; Или по шагам: v DIV 10H перемещает бит ''ide'' в младшую позицию. После этого: MOD 2 оставляет только его. ==== Извлечение fi ==== fi := v DIV 100H MOD 100H; Здесь: * ''DIV 100H'' убирает младшие 8 бит; * ''MOD 100H'' оставляет только следующие 8. ===== Изменение отдельного поля ===== Это особенно интересно. ==== Замена dlc ==== Нужно: - убрать старое значение; - вставить новое. Пусть новое значение: newDlc = 5 Тогда: v := v - (v MOD 10H); v := v + 5; ==== Замена ide ==== v := v - (v DIV 10H MOD 2) * 10H; v := v + 1 * 10H; ==== Замена fi ==== v := v - (v DIV 100H MOD 100H) * 100H; v := v + 55H * 100H; ===== Более общий вариант ===== Вся схема основана на позиционной записи числа. Фактически это работа с цифрами числа в системе счисления. Для двоичных полей основание равно: 2^ширина_поля Например: ^ Размер поля ^ Основание ^ | 1 бит | 2 | | 4 бита | 16 | | 8 бит | 256 | Поэтому: MOD base извлекает поле, а DIV base сдвигает к нему. ===== Главное достоинство: произвольные диапазоны ===== И тут начинается самое интересное. Битовые поля жёстко привязаны к степеням двойки. Но арифметическая упаковка работает вообще с любыми диапазонами. Например: секунда : 0..59 минута : 0..59 час : 0..23 Можно хранить время одним числом. ==== Упаковка ==== v := sec + min * 60 + hour * 60 * 60; ==== Извлечение ==== Секунды: sec := v MOD 60; Минуты: min := v DIV 60 MOD 60; Часы: hour := v DIV (60 * 60) MOD 24; Это уже не двоичная упаковка, а универсальная позиционная схема хранения. ===== Почему такой подход часто лучше ===== ==== Переносимость ==== Код не зависит от: * endian; * ABI; * компилятора; * порядка битовых полей. ==== Явность ==== Все смещения и диапазоны описаны явно. Нет скрытой магии компилятора. ==== Универсальность ==== Работает: * для битов; * для десятичных цифр; * для любых диапазонов; * для сериализации; * для компактных идентификаторов; * для кодирования состояний.