Содержание
Упаковка значений вместо битовых полей
Написано ИИ:
В языках семейства 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.
Вместо специальных битовых полей используются обычные операции:
MODDIV+-
Формирование полного значения
Пусть:
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;
- компилятора;
- порядка битовых полей.
Явность
Все смещения и диапазоны описаны явно.
Нет скрытой магии компилятора.
Универсальность
Работает:
- для битов;
- для десятичных цифр;
- для любых диапазонов;
- для сериализации;
- для компактных идентификаторов;
- для кодирования состояний.
