Думаю, многим разработчикам на управляемом коде всегда интересовало: сколько же байт занимает экземпляр объекта? А каков лимит размера одного объекта в CLR? Существуют ли различия в выделении памяти между 32-битными и 64-битными системами?

Предисловие

Прежде вспомним, что в .NET существует 2 вида объектов: value types и reference types, которые создаются, соответственно, в стеке и куче (управляемом сборщиком мусора). Value types предназначены для хранения простых данных, будь то число, символ. Во время присваивания значения переменной происходит копирование каждого поля объекта. Также время жизни таких объектов зависит от области видимости. Размеры value types определены в Common Type System и составляют:

CTS-Тип Количество байт
System.Byte 1
System.SByte 1
System.Int16 2
System.Int32 4
System.Int64 8
System.UInt16 2
System.UInt32 4
System.UInt64 8
System.Single 4
System.Double 8
System.Char 2
System.Decimal 16

Reference types, наоборот, представляют собой ссылку на область памяти, занимаемой экземпляром объекта в куче.

Ниже приведена внутренняя структура CLR-объектов:

Для переменных ссылочных типов, в стек помещается значение фиксированного размера (4 байта, тип DWORD), содержащее адрес экземпляра объекта, созданного в обычной куче (есть еще Large Object Heap, HighFrequencyHeap и т.п., но на них мы заострять внимание не будем). Например, в C++ это значение называется указателем на объект, а в мире .NET — ссылкой на объект.

Первоначально значение SyncBlock равно нулю. Однако в SyncBlock может хранится хеш-код объекта (при вызове метода GetHashCode), или номер записи syncblk, который помещает в заголовок объекта среда при синхронизации (использование lock, либо напрямую Monitor.Enter).

Каждый тип имеет свой MethodTable, и все экземпляры объектов одного и того же типа ссылаются на один и тот же MethodTable. Данная таблица хранит информацию о самом типе (интерфейс, абстрактный класс и т.д.).

Reference type pointer — ссылка на объект, хранящаяся в переменной, размещенной в стеке со смещением 4. Остальное представляет собой поля класса.

SOS

Перейдем от теории к практике. Стандартными средствами CLR невозможно установить размер объекта. Да есть оператор sizeof в C#, но предназначен он для установления размера unmanaged-объектов, а также размеров value types. В вопросах ссылочных типов – бесполезен.

Именно для этих целей существует расширение дебаггера Visual Studio – SOS (Son of Strike).

Перед началом использование необходимо разрешить unmanaged code debugging:

Для активации SOS, во время отладки необходимо открыть VS > Debug > Windows > Immediate Window и ввести следующее:

.load sos.dll

После чего увидим его успешную загрузку:

SOS имеет большое количество команд. В нашем случае необходимы будут лишь следующие:

  • !DumpStackObjects (!DSO) – отображает список обнаруженных объектов в пределах текущего стека
  • !DumpObj (!DO) – отображает информацию об объекте по заданному адресу
  • !ObjSize – возвращает полный размер объекта. Чуть позже мы рассмотрим его предназначение

Остальные команды можно узнать набрав !Help.

Для демонстрации создадим простое консольное приложение и напишем класс MyExampleClass:

class MyExampleClass
{
  byte ByteValue = 255;           // 1 байт
  sbyte SByteValue = 127;         // 1 байт
  char CharValue = 'a';           // 2 байта
  short ShortValue = 128;         // 2 байта
  ushort UShortValue = 65000;     // 2 байта
  int Int32Value = 255;           // 4 байта
  uint UInt32Value = 255;         // 4 байта
  long LongValue = 512;           // 8 байт
  ulong ULongValue = 512;         // 8 байт
  float FloatValue = 128F;        // 4 байта
  double DoubleValue = 512D;      // 8 байт
  decimal DecimalValue = 10M;     // 16 байт
  string StringValue = "String";  // 4 байта
}

Возьмем калькулятор и посчитаем предполагаемый размер для экземпляра класса – пока что 64 байт.

Однако помните в начале статьи про структуру объектов? Так вот окончательный размер будет равен: CLR-объект = SyncBlock (4) + TypeHandle (4) + Fields (64) = 72

Проверим теорию. Добавим следующий код:

class Program
{
  static void Main(string[] args)
  {
    var myObject = new MyExampleClass();
    
    Console.ReadKey(); //Ставим здесь breakpoint
  }
}

И запустим отладку (F5). Введем следующие команды в Immediate Window:

.load sos.dll !DSO

На приведенном скриншоте выделен адрес объекта myObject, который передадим в качестве параметра команде !DO:

Ну что же, размер myObject составляет 72 байта. Не так ли? Ответ нет. Дело в том, что мы забыли еще добавить размер строки переменной StringValue. Ее 4 байта – это только ссылка. А вот истинный размер мы сейчас и проверим.

Введем команду !ObjSize:

Таким образом, настоящий размер myObject составляет 100 байт.

Дополнительные 28 байт занимает переменная StringValue.

Однако проверим это. Для этого используем адрес переменной StringValue 01b8c008:

Из чего складывается размер System.String?

Во-первых, в CTS символы (тип System.Char) представлены в Unicode и занимают 2 байта.

Во-вторых, строка – есть не что иное, как массив символов. Так в StringValue мы записали значение “String”, что равно 12 байт.

В-третьих, System.String – ссылочный тип, а это значит, что располагается он в GC Heap, и будет состоять из SyncBlock, TypeHandle, Reference point + остальные поля класса. Reference point здесь браться в расчет не будет, т.к. уже посчитан в самом классе MyExampleClass (ссылка 4 байта).

В-четвертых, структура System.String выглядит следующим образом:

Дополнительные поля класса составляют переменные m_stringLength типа Int32 (4 байта), m_firstChar типа Char (2 байта), переменная Empty считаться не будет, т.к. является пустой статичной строкой.

Также обратим внимание на размер – 26 байт вместо 28, посчитанных ранее. Сложим все вместе: StringValue = SyncBlock (4) + TypeHandle (4) + m_stringLength (4) + m_firstChar (2) + “String” (12) = 26

Дополнительные 2 байта образуются из-за выравнивания, производимого менеджером памяти CLR.

x86 vs. x64

Основное различие заключается в размере DWORD – указателя памяти. В 32-битных системах он составляет 4 байта, в 64-битных уже 8 байт. Так, если пустой класс будет равен в x86 лишь 12 байт, то в x64 уже 24 байта.

Лимит размеров CLR-объектов

Принято считать, что размер System.String ограничено лишь доступной системной памятью.

Однако любой экземпляр любого типа не может занимать более 2 Gb памяти. И это ограничение распространяется как на x86, так и x64 системы.

Так, List, хотя и имеет метод LongCount(), это не означает возможности расположить 2^64 объектов. Решением может быть использование класса BigArray, предназначенного для этих целей.

Послесловие

В данной статье захотелось затронуть именно вопрос нахождения размеров CLR-объектов. Конечно, существуют подводные камни, особенно с командой !ObjSize, когда может произойти двойной счет из-за применения intern-строк.

В большинстве своем вопрос размера объектов, их выравнивания в памяти приходят только при сильной необходимости – возможности оптимизации использования ресурсов.

Полезные ссылки