В этой статье я хотел бы рассказать и показать особенности использования
"уплотнения сложных типов данных" в Delphi.

Все когда-нибудь имели дело с записями. Записи используются во многих случаях
– для создания файловых справочников, удобной группировки и представления
данных, в системном программировании и т.д. Например, открыв файл Windows.pas
(стандартный модуль, подключается в разделе uses), можно найти что-то типа
такого описания:

_POINTL = packed record
x: Longint;
y: Longint;
end;

или

TISHMisc = packed record
case Integer of //Здесь используется запись с вариантами
0: (PhysicalAddress: DWORD);
1: (VirtualSize: DWORD);
end;

Что же здесь обозначает ключевое слово packed? Слово Packed говорит Delphi
минимизировать память. Так что же получается, без этого слова у нас структура
занимает памяти больше?

Вот пример записи:

TRecord = Record
pole1 : byte;
pole2 : string[4];
pole3 : integer;
pole4 : Int64;
end;

Давайте подсчитаем ее размер: pole1 – 1 байт, pole2 – 5 байт (надеюсь, не
забыли 4 байта под символы и 1 под размер), pole3 – 4 байта, pole4 – 8 байт.
Если их сложить получиться 18 байт. Давайте проверим:

size1 := sizeof(TRecord);
ShowMessage(‘Размер обычной записи = ’+IntToStr(size1));

Результат: 24.

Удивлены? А теперь добавьте слово packed перед словом Record… Размер 18 как и
подсчитали ранее. Так куда же у нас пропали целые 6 байт?! А если у нас будет
массив из 10 000 000 таких записей, например в каком-нибудь справочнике, мы
потеряем около 57 Мегабайт?

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

procedure TForm1.Button1Click(Sender: TObject);
type
// обычная запись
TRecord = Record
pole1 : byte;
pole2 : string[4];
pole3 : integer;
pole4 : Int64;
end;
// упакованная записи
TPackedRecord = Packed record
pole1 : byte;
pole2 : string[4];
pole3 : integer;
pole4 : Int64;
end;

var
Rec : TRecord;
packedRec : TPackedRecord;
size1 : integer;
begin
ShowMessageFmt('Adress = %P',[Addr(Rec)]); //Вывод адреса нашей записи
size1 := sizeof(Rec); //Узнаем размер
// присваиваем некоторые данные нашей записи
Rec.pole1 := $FF;
Rec.pole2 :='hack';
Rec.pole3 := $AAAAAAAA;
Rec.pole4 := $1122334455667788;
ShowMessage('Размер структуры = ' + IntToStr(size1)); //выводим размер
end;

Функцию ShowMessageFmt используем для того, чтобы вывести адрес нашей
структуры. Обычный ShowMessage не поможет, т.к. нам надо вывести указатель (тип
Pointer). Этот адрес меняется, так что у вас может быть другой. Функция Addr –
это получение адреса, возвращаемый тип Pointer. Вот ее результат:

Запомним этот адрес. Теперь поставим breakpoint на последнем ShowMessage –
нажмем F5 на этой сроке.

Теперь запустим. Программа выдаст сообщение с адресом, а затем остановиться
на предпоследней строке. Нажимаем Ctrl + Alt + C. Появится окно дизассемблера.
Щелкаем правой кнопкой мыши по левой-нижней области (dump памяти) и выбираем
"Идти к адресу".

Вводим наш адрес записи (например так: $0012F568), который получили с помощью
ShowMessageFmt, и попадаем на адрес начала нашей структуры.

Помним, что pole1 мы присвоили значения $FF (т.е. =255), pole2 =’hack’, pole3
= $AAAAAAAA (4 байта, т.е. =2863311530) и т.д. Посмотрим на рисунок, на нем я
отобразил структуру байтов и как они расположены в памяти.

Как видим, после данных pole2 идут байты выравнивания, которые выравнивают
границу памяти до 8 байт. И поэтому Pole3 начинается с адреса кратному 8. То же
самое видим и с Pole4 – сначала выравнивается граница (байты 0E 00 00 00), а
затем идут сами данные (хочу заметить, что в памяти данные хранятся в обратном
порядке, что видно в Pole4).

А теперь проверим упакованную запись. Переменную Rec заменяем в коде на
packedRec, т.е. используем packed record. И проделываем все то же самое:

Здесь видим, что никаких байт выравнивания нет, а все данные (поля)
расположены друг за другом – упакованы.

Возможно возник вопрос: почему в первом примере данные выравнивались по
границе 8 байт? Все дело в настройках компилятора. Если вы откроете Project –
Option – Compiler (опции компилятора), то там будет списочек Record field
alignment, в котором можно выбрать нужное выравнивание (8,4,2 или 1 байт). Не
забываем перекомпоновать проект после изменений. Кстати выравнивание по 1 байту
и есть упаковка данных.

Второй вопрос: почему же всегда не упаковывать данные? К сожалению, упаковка
данных имеет не только плюсы, но и минусы. Так, например, не выравнивая границу
мы увеличиваем время доступа к полям. Поэтому использовать упаковку надо с умом.

Кстати, в том же файле Windows.pas можно найти не только упаковка записей, но
и массивов, вот пример:

CprMask: packed array[0..3] of DWORD;

Попробуйте сами проанализировать упаковку этого массива.

Оставить мнение

Check Also

Windows 10 против шифровальщиков. Как устроена защита в обновленной Windows 10

Этой осенью Windows 10 обновилась до версии 1709 с кодовым названием Fall Creators Update …