Упаковка сложных типов данных в Delphi

В этой статье я хотел бы рассказать и показать особенности использования "уплотнения сложных типов данных" в 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;

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

Взято с http://www.xakep.ru/post/49795/default.asp


Ведете ли вы блог?

Да
Нет
Планирую


Результаты опроса

Новостной блок