Оптимизация размера класса в Delphi. Есть ли что-то вроде "упакованных классов"? - PullRequest
4 голосов
/ 06 мая 2009

Я пытаюсь оптимизировать размер моих классов Delphi, чтобы они занимали как можно меньше памяти, потому что я их создаю.

Дело в том, что сами классы довольно маленькие, но они не занимают места, которое я ожидал. Например, если у меня есть

type MyClass = class
  private
    mMember1 : integer;
    mMember2 : boolean;
    mMember3 : byte;
end;

Я ожидаю, что он будет использовать 6 байтов, но из-за выравнивания он заканчивается использованием 12 байтов, то есть логические значения используют 4 байта вместо 1 байта ... и то же самое относится к полю байтов ...

Для записей вы можете использовать директиву {$ A1} или объявить ее как упакованную запись, чтобы она использовала только необходимую память.

Есть ли способ сделать то же самое с классами? (Может быть, какое-нибудь руководство о том, как правильно переопределить метод класса NewInstance?)

Редактировать: Хорошо, небольшое объяснение того, что я делаю ...

Во-первых, реальный размер класса составляет около 40 байтов, включая пространство, занимаемое VMT и указателями интерфейса.

Все классы наследуются от базового класса RefCounting, размер которого составляет 8 байтов (целое число FRefCount и некоторые методы для подсчета ссылок), и они ДОЛЖНЫ поддерживать интерфейсы (следовательно, вообще не использовать упакованные записи).

Эти объекты передаются и превращаются в несколько вещей, при этом обработчики не знают, что они получили. Например, у меня есть класс, который получает список элементов и выполняет что-то вроде:

if Supports(List[i], IValuable, IValInstance) then
  Eval(IValInstance.Value);

тогда другой обработчик может проверить другой интерфейс

If Supports(List[i], IStringObject, IStringInstance) then
  Compose(IStringInstance.Value)

Таким образом, список обрабатывается по-разному каждым обработчиком ...

О том, как получить общий размер класса, я использую модифицированный менеджер памяти, чтобы я мог отслеживать, сколько памяти использует «настоящий» менеджер памяти для класса. Таким образом, я уверен, что экземпляры не упаковываются.

Наконец, это в Delphi 7. Я безуспешно пытался использовать директиву прекомпилятора {$ A1}, поля в любом случае выровнялись, и в худшем случае я могу иметь несколько миллионов экземпляров, так что сохранение 6 байтов может результат на нескольких МБ сохраняется.

Ответы [ 9 ]

10 голосов
/ 07 мая 2009

Вы можете использовать упакованную запись в качестве поля ваших объектов:

type
  TMyRecord = packed record
    Member1 : integer;
    Member2 : boolean;
    Member3 : byte;
  end;

  TMyClass = class
  private
    FData : TMyRecord;
   function GetMember1 : Integer;
  public
    property Member1 : Integer read GetMember1;
    // Later versions of Delphi allow "read FData.Member1;", not sure when from
  end;

function TMyClass.GetMember1 : integer;
begin
  result := FData.Member1;
end;
4 голосов
/ 07 мая 2009

Если вы так сильно беспокоитесь о нескольких байтах (вы упомянули 6 против 12), вам вообще не следует использовать класс. Вместо этого используйте запись. Затем вы можете использовать упакованный для устранения отходов выравнивания; однако, будьте готовы к снижению производительности, так как выравнивание по умолчанию «без упаковки» настроено на самый быстрый доступ ЦП.

4 голосов
/ 06 мая 2009

Абсолютно. Вы можете упаковать наборы, массивы, записи, объекты и типы файлов. Обратите внимание, что использование pack приводит к замедлению доступа к данным и может вызвать проблемы с совместимостью типов.

Я попробовал это в Delphi 2006. Проверка синтаксиса редактора пометила его как ошибку, но он скомпилировался просто отлично.

Согласно документации Delphi переключатель $ A применяется как к типам классов, так и к типам записей.

Обновление:

Я попробовал это и в Delphi 6. Он успешно компилируется. Если упакованные классы не будут компилироваться в Delphi 7, возможно, вы обнаружили ошибку. Если это ошибка, то вряд ли Embarcadero что-либо с этим сделает, если только это не произойдет в последней версии Delphi, что, похоже, не так.

4 голосов
/ 06 мая 2009

Почему бы просто не использовать упакованные записи для начала? Это исключит накладные расходы (незначительные), вызванные спуском с TObject ...

3 голосов
/ 07 мая 2009

Может быть, немного оффтоп, но я боролся с этим раньше (до D2006, так что никаких записей) для некоторой платформы ORM. Предполагая, что материал "класса" установлен в камне:

Советы и подсказки:

  1. проблема упаковки, которую я обошел, имея методы получения и установки полей, сохраняя их в массиве байтов класса. Может даже быть упакован. Если сеттеры / геттеры являются встраиваемыми (тогда для меня это не вариант, D6), это может быть довольно дешево даже.
  2. попробуйте собрать служебные данные кучи (как административные, так и свободные места), самостоятельно инициализируя блок памяти, устанавливая VMT и вызывая для него конструктор. Накладные расходы кучи IIRC составляли 8 байтов, а гранулярность выделения старого heapmgr - 8 байтов, а fastmm - 16 байтов. Если вы сортируете классы по размеру, вы можете использовать растровое изображение в качестве структуры размещения
  3. Если вы особенно злой, помните, что указатель имеет 2 или 3 бита. Я использовал эти биты в качестве идентификатора для чрезвычайно часто используемого типа распределения, сохраняя 4-байтовые резервы кучи для хранения размера.
  4. Обратите внимание на ваши индексы. Если вы получаете много объектов (у меня их было около 6 миллионов), вы должны быть осторожны и со своими типами индексов. (без tstringlist, пожалуйста)
  5. Всегда держите необфусцированный материал в ifdef, чтобы облегчить тестирование при отладке (*)
  6. никогда не используйте строки в качестве ключа. Хеш, если обязательно. Нормализующие структуры полезны не только для баз данных

(*) Позже я перекомпилировал «чистую» версию под 64-битной FPC, и она работала после нескольких незначительных sizeof (pointer ()), несмотря на уродливость точек 1 и 2

2 голосов
/ 07 мая 2009

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

Затем в классе вы можете хранить только одно или два поля для индексации в куче и смещении. Если вам удастся избежать только одного большого блока памяти, вы можете уменьшить его до смещения.

TPackedRecord = packed record ... end;
PPackedRecord = ^TPackedRecord;
TPackedRecordHeap = class
  ...
  function  Add: PPackedRecord;
  procedure Release( entry: PPackedRecord );
end;

TUsableClass = class
private
  heap: TPackedRecordHeap;
  data: PPackedRecord;
public
  constructor Create( heap: TPackedRecordHeap );
  ...
end;
2 голосов
/ 07 мая 2009

Вручную упакуйте свои данные . Возьмите каждые 4 байта и поместите их в один кардинал. Если у вас есть две короткие строки, не кратные длине 4, поместите их в одну короткую строку и просто прочитайте части для каждой.

С вашей стороны потребуется немного больше инженерии, чтобы все выстроить вручную, но благодаря использованию методов получения и установки поведение будет прозрачным вне класса. Вы можете действительно приблизиться к тем же результатам, что и компилятор, упаковывающий его таким образом.

1 голос
/ 07 мая 2009

Я ожидаю, что он будет использовать 6 байтов, но из-за выравнивания он будет использовать 12 байтов

Даже если вы напишите «TMyClass = class end;» класс будет наследоваться от TObject, который имеет виртуальные методы.

Что делает

  4 Bytes (VMT)
+ 4 Bytes (member1: Integer)
+ 1 Byte  (member2: Boolean)
+ 1 Byte  (member3: Byte);
+ 2 Bytes (alignment)
---------
 12 Bytes

Таким образом, если вы отключили выравнивание, вы выиграете только 2 байта.

Упорядочение полей по размеру типа данных (в более крупном классе, который вы упомянули) может устранить некоторые дыры выравнивания. И $ A- (Delphi 5) или $ A1 (новее) не работает. Ни в Delphi 7, ни в Delphi 2009.

Кстати: в Delphi 2009 у вас есть дополнительные 4 байта для Thread.Monitor, увеличивая общий размер класса до 16 байт.

1 голос
/ 07 мая 2009

К вашему сведению, если бы это была запись, это было бы 8 байтов и 6 байтов в качестве упакованной записи. Таким образом, вы смотрите на 4-байтовые издержки для указателя класса (при условии, что вы находитесь в Delphi до 2009 года) с возможностью восстановления 2 байтов, если он был упакован.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...