Размеры CLR-объектов. Точное определение

    Думаю, многим разработчикам на управляемом коде всегда интересовало: сколько же байт занимает экземпляр объекта? А каков лимит размера одного объекта в 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-строк.

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

    Надеюсь, статья оказалась интересной и полезной. Спасибо за внимание!

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

    Share post

    Comments 26

      0
      >> существует 2 вида объектов: value types и reference types,

      А вот unsafe pointer — он value-type или reference-type? Он ведь не обычный указатель, по типу того, что используется в Си, так, и не обычная объектная ссылка? Так сколько он байт занимает?
        +2
        вообще это уже третий тип в .NET — pointer type. представляет он собой DWORD-адрес на конкретный экземпляр либого value type + bool.
        главное предназначение — избежать копирования value type объекта пр присваивании переменной. еще одна роль — вызов нативных функций.
        если говорить про DWORD — то 4 байта x86, 8 байт x64.
        а уже размеры каждого value type определены в CTS.
          0
          Если верить Эрику Липперту указатель вполне себе value-type.
          +3
          >> Прежде вспомним, что в .NET существует 2 вида объектов: value types и reference types, которые создаются, соответственно, в стеке и куче (управляемом сборщиком мусора).

          Чушь. За такое надо бить по рукам. Вы утверждаете что массив интов лежит в стеке? Или что локальная интовая переменная не может лежать в регистре (который, на минуточку не стек и не куча). Value и reference вообще никак не связаны с местом расположения переменной. Локальная переменная может лежать где угодно: в регистре, стеке и куче. Это зависит от ожидаемой продолжительности жизни — если локальная переменная участвует в лямбде то она благополучно переживет свой изначальный метод в куче.

          На всякий случай читайте первоисточники
          • UFO just landed and posted this here
            +7
            Экземпляры объектов ссылочного типа хранятся в управляемой куче всегда; экземпляры объектов типа значений — в стеке управляемого потока среды .NET, за исключением случаев, когда их время жизни должно быть продлено (объект типа значения является полем класса, элементом массива, локальной переменной в блоке итератора или локальной переменной, замкнутой в лямбде/анонимном методе). Интересно что язык C++/CLI позволяет создать массив значений в стеке.

            Вывод: автор не так уж и неправ. Reference types хранятся в куче, value types обычно хранятся в стеке. Многие авторы, включая Рихтера, связывают местоположение переменной и её тип таким образом, потому как в большинстве случаев нет необходимости погружаться в низкоуровневые подробности, особенно в подробности реализации компилятора. .NET-программист может ограничится частично верным определением, если только не пишет серьёзно много неуправляемого кода (что, впрочем, и утверждается в рекомендованной статье).
              +1
              Во-первых, reference создается и живет только в куче, причем не только в GC Heap, но может и в Large Object Heap и т.д.
              Во-вторых, время жизни value type определяется declaration scope.
              В-третьих, для увеличения продолжительности жизни экземпляра value type, можно и без анонимных методов, и без лямбда-выражений обойтись — использовать boxing (CLR заворачивает значение переменной и помещает в GC Heap).
              Возможность нахождения value type объекта в регистре зависит от платформы (так существуют отличия между desktop и server CLR не только в плане сборщика мусора), еще это зависит от самого JIT. Так Windows (и .NET соответственно) запускаются не только на x86, x64, но также еще существует ARM-версия — Windows Embedded. А еще есть .NET MicroFramework с TinyCLR.
              Иногда из-за трудности определения возможной жизни объекта, CLR может поместить value type в кучу, для обеспечения максимальной жизни.
              И, наконец, это зависит от использование нативного кода.

              Как видим, это все детали имплементации конкретной CLR + языковые возможности и требования, например C#
              0
              Правильно ли я понимаю, что если запросить по адресу размер некоторого класса А, который имеет поля классов В, С..., то SOS вернет суммарный размер класса А, B, C? Или вернет размер класса A + размер ссылок на классы B и С?
              • UFO just landed and posted this here
                  0
                  Предположим что он возвращает 1) а не 2). Тогда что должен вернуть sos для класса, который в одном из полей содержит ссылку на самого себя? +inf?
                  Делаем вывод…
                    0
                    если одно из полей экземпляра класса содержит ссылку на самого себя, то при !objsize будет возвращен удвоенный размер экземпляра + размер строки (в нашем случае 180 байт) без рекурсии.
                    !do производит инспекцию только по полям и структуре классов, без подсчета размеров др. reference types объектов.
                    0
                    SOS вернет суммарный размер класса А, B, C, если использовать !objsize.
                    Если !do, то без размеров B, C.
                      0
                      Спасибо =) Делаю вывод, что SOS исключительных ситуациях действительно будет необходим
                    0
                    меня терзают смутные сомнения (с)
                    размер decimal 16 байт.
                      0
                      прежде чем писать, может пробовали хотя бы посмотреть MSDN?
                      если да, то
                      1) decimal есть не что иное как 128-битовое число, НО
                      2) размер колеблется от ±1.0 ✕ 10^28 до ±7.9 ✕ 10^28
                      3) Содержит 128-битовые (16-байтовые) значения со знаком, представляющие 96-битовые (12-байтовые) целые числа, масштабируемые с переменной степенью 10.

                      еще можно было просто проверить через !objsize.
                      и еще посмотреть на расчеты в статье.
                      и, да, оператор sizeof врет, возвращая размер 16.
                        0
                        А почему вы не согласны, что decimal занимает 128 бит (16 байт), если он реализован согласно IEEE 754-2008 как decimal128?

                        [StructLayout(LayoutKind.Sequential, Pack=1)]
                        public struct X
                        {
                        public decimal f1;
                        public decimal f2;
                        public decimal f3;
                        public decimal f4;
                        }


                        !do 02532e2c
                        Name: Test.Program+X
                        MethodTable: 002a3094
                        EEClass: 002a135c
                        Size: 72(0x48) bytes
                        (c:\Projects\other\ClassLibrary1\ConsoleApplication1\bin\Debug\ConsoleApplication1.exe)
                        Fields:
                        MT Field Offset Type VT Attr Value Name
                        717e7f08 4000001 4 System.Decimal 1 instance 02532e30 f1
                        717e7f08 4000002 14 System.Decimal 1 instance 02532e40 f2
                        717e7f08 4000003 24 System.Decimal 1 instance 02532e50 f3
                        717e7f08 4000004 34 System.Decimal 1 instance 02532e60 f4


                        Каждый Decimal как видно занимает ровно 0x10 байт.

                        Посмотрим отдельно на decimal — состоит из 4х int:
                        Name: System.Decimal
                        ...
                        Fields:
                        MT Field Offset Type VT Attr Value Name
                        71812dbc 400037e 4 System.Int32 1 instance 0 flags
                        71812dbc 400037f 8 System.Int32 1 instance 0 hi
                        71812dbc 4000380 c System.Int32 1 instance 0 lo
                        71812dbc 4000381 10 System.Int32 1 instance 0 mid
                        ...


                        Почему врет по вашему sizeof и сколько он должен возвращать?
                      0
                      Приношу свои извинения. Действительно Decimal составляет 16 байт.
                      Именно понятие 96-битовых чисел у меня и запечатлелось как норма, что есть неправда.
                      0
                      >Reference type pointer — ссылка на объект, хранящаяся в переменной, размещенной в стеке со смещением 4.

                      несколько раз перечитал, не понял, что это
                      и почему его нет в общей сумме?
                        0
                        смотрите, в классе
                        class MyExampleClass
                        {
                          string StringValue = "String";  // 4 байта
                        }
                        

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

                          ссылочные объекты занимают 4 байта (на машине соответствующей разрядности) и вполне попадают в Object fields. незачем пихать в схему лишнюю сущность
                          тот же Рихтер рисует только Type object ptr и Sync index: www.rvenables.com/linkjackandsufferaccidentaldroptable/clr_via_csharp_f4.9.png

                          >>Однако в SyncBlock может хранится хеш-код объекта (при вызове метода GetHashCode), или номер записи syncblk
                          «все смешалось в доме Облонских»
                          все совсем наоборот. для System.Object в CLR 1-2 в качестве хешкода использовался Sync index (поэтому вызов GetHashCode() на нем приводил к созданию Sync block'a)

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

                          на ошибку с привязкой value types к стеку вам уже указали
                      0
                      а если в моем классе будет 10 строк, то по вашей логике должно быть 10 штук Reference type pointer на схеме?

                      ссылочные объекты занимают 4 байта (на машине соответствующей разрядности) и вполне попадают в Object fields. незачем пихать в схему лишнюю сущность

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

                      если картинка непонятна, то это другое дело.
                      «все смешалось в доме Облонских»
                      все совсем наоборот. для System.Object в CLR 1-2 в качестве хешкода использовался Sync index (поэтому вызов GetHashCode() на нем приводил к созданию Sync block'a)

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


                      и, наконец, насчет
                      подсчет длины строки также выполнен неверно. m_firstChar является первым элементом в последовательности символов, и поэтому присутствует один раз, а вы посчитали его дважды

                      вообще-то посчитано правильно. m_firstChar является отдельным полем класса + не является shared.
                      более того, т.к. оно еще и value type, тогда при присваивании ей значения char копируется из массива символов. поправьте меня, если я ошибаюсь.

                      на ошибку с привязкой value types к стеку вам уже указали

                      это не является ошибкой. см. мой коммент выше.
                        0
                        >если картинка непонятна, то это другое дело.

                        картинка просто не соотвествует действительности

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

                        вы путаете причину и следствие. не «SyncBlock может хранить хеш-код», а «Sync Index может использоваться в качестве хеш-кода»

                        >это не является ошибкой. см. мой коммент выше.

                        в той формулировке, как это сделано в статье, именно что ошибка. а вашим комментариям, к сожалению, доверять нельзя
                        почиайте Липперта что ли:
                        blogs.msdn.com/b/ericlippert/archive/2010/09/30/the-truth-about-value-types.aspx
                        blogs.msdn.com/b/ericlippert/archive/2009/04/27/the-stack-is-an-implementation-detail.aspx

                        >вообще-то посчитано правильно. m_firstChar является отдельным полем класса
                        >тогда при присваивании ей значения char копируется из массива символов.

                        возьмите отладчик и посмотрите побайтно, что в строке есть и чего там нет

                          0
                          я ни коим образом не пытаюсь переубедить Вас, более того, доверять или нет моим комментариям — дело Ваше.

                          P.S.
                          Эрика Липперта я читаю, и не только его ;)

                      Only users with full accounts can post comments. Log in, please.