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