Вопрос

Я пытался определить накладные расходы заголовка в массиве .NET (в 32-разрядном процессе), используя этот код:

long bytes1 = GC.GetTotalMemory(false);
object[] array = new object[10000];
    for (int i = 0; i < 10000; i++)
        array[i] = new int[1];
long bytes2 = GC.GetTotalMemory(false);
array[0] = null; // ensure no garbage collection before this point

Console.WriteLine(bytes2 - bytes1);
// Calculate array overhead in bytes by subtracting the size of 
// the array elements (40000 for object[10000] and 4 for each 
// array), and dividing by the number of arrays (10001)
Console.WriteLine("Array overhead: {0:0.000}", 
                  ((double)(bytes2 - bytes1) - 40000) / 10001 - 4);
Console.Write("Press any key to continue...");
Console.ReadKey();

Результатом было

    204800
    Array overhead: 12.478

В 32-разрядном процессе object[1] должен иметь тот же размер, что и int[1], но фактически служебные данные увеличиваются на 3,28 байта до

    237568
    Array overhead: 15.755

Кто-нибудь знает почему?

(Кстати, если кому-то интересно, накладные расходы для объектов, не являющихся массивом, например(object)i в цикле выше, составляет около 8 байт (8.384).Я слышал, что в 64-битных процессах это 16 байт.)

Это было полезно?

Решение

Вот немного более аккуратная (IMO), короткая, но полная программа для демонстрации того же самого:

using System;

class Test
{
    const int Size = 100000;

    static void Main()
    {
        object[] array = new object[Size];
        long initialMemory = GC.GetTotalMemory(true);
        for (int i = 0; i < Size; i++)
        {
            array[i] = new string[0];
        }
        long finalMemory = GC.GetTotalMemory(true);
        GC.KeepAlive(array);

        long total = finalMemory - initialMemory;

        Console.WriteLine("Size of each element: {0:0.000} bytes",
                          ((double)total) / Size);
    }
}

Но я получаю те же результаты - накладные расходы для любого массива ссылочного типа составляют 16 байт, тогда как накладные расходы для любого массива типа значений составляют 12 байт.Я все еще пытаюсь понять, почему это так, с помощью спецификации CLI.Не забывайте, что массивы ссылочных типов являются ковариантными, что может иметь отношение к делу...

Редактировать:С помощью cordbg я могу подтвердить ответ Брайана - указатель типа массива ссылочного типа одинаков независимо от фактического типа элемента.Предположительно, есть какая-то странность в object.GetType() (который не является виртуальным, помните), чтобы учесть это.

Итак, с помощью кода:

object[] x = new object[1];
string[] y = new string[1];
int[] z = new int[1];
z[0] = 0x12345678;
lock(z) {}

В итоге мы получаем что-то вроде следующего:

Variables:
x=(0x1f228c8) <System.Object[]>
y=(0x1f228dc) <System.String[]>
z=(0x1f228f0) <System.Int32[]>

Memory:
0x1f228c4: 00000000 003284dc 00000001 00326d54 00000000 // Data for x
0x1f228d8: 00000000 003284dc 00000001 00329134 00000000 // Data for y
0x1f228ec: 00000000 00d443fc 00000001 12345678 // Data for z

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

Для x и y, значения являются:

  • Блок синхронизации, используемый для блокировки хэш-кода (или тонкий замок - смотрите комментарий Брайана)
  • Указатель типа
  • Размер массива
  • Указатель типа элемента
  • Нулевая ссылка (первый элемент)

Для z, значения являются:

  • Блок синхронизации
  • Указатель типа
  • Размер массива
  • 0x12345678 (первый элемент)

Массивы разных типов значений (byte[], int[] и т.д.) В конечном итоге имеют указатели разных типов, тогда как все массивы ссылочных типов используют указатель одного и того же типа, но имеют указатель другого типа элемента.Указатель типа элемента - это то же значение, которое вы могли бы найти в качестве указателя типа для объекта этого типа.Итак, если бы мы посмотрели на память строкового объекта в приведенном выше запуске, у него был бы указатель типа 0x00329134.

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

Кстати, все это справедливо только для "векторных" типов - в среде CLR тип "vector" представляет собой одномерный массив с нижней границей 0.Другие массивы будут иметь другой макет - во-первых, им потребуется сохранить нижнюю границу...

До сих пор это был эксперимент, но вот догадки - причина, по которой система реализована таким образом, каким она была.С этого момента я действительно просто предполагаю.

  • Все object[] массивы могут совместно использовать один и тот же JIT-код.Они будут вести себя одинаково с точки зрения выделения памяти, доступа к массиву, Length свойство и (что важно) расположение ссылок для GC.Сравните это с массивами типов значений, где разные типы значений могут иметь разные "следы" GC (напримеру одного может быть байт, а затем ссылка, у других вообще не будет ссылок и т.д.).
  • Каждый раз, когда вы присваиваете значение в object[] среда выполнения должна проверить, что это допустимо.Необходимо проверить, что тип объекта, ссылку на который вы используете для нового значения элемента, совместим с типом элемента массива.Например:

    object[] x = new object[1];
    object[] y = new string[1];
    x[0] = new object(); // Valid
    y[0] = new object(); // Invalid - will throw an exception
    

Это ковариация, о которой я упоминал ранее.Теперь, учитывая, что это произойдет для каждое отдельное задание, имеет смысл уменьшить количество косвенных ссылок.В частности, я подозреваю, что вы на самом деле не хотите уничтожать кеш, переходя к объекту типа для каждого присваивания, чтобы получить тип элемента.Я подозреваемый (и моя сборка x86 недостаточно хороша, чтобы проверить это), что тест представляет собой что-то вроде:

  • Является ли копируемое значение нулевой ссылкой?Если так, то это прекрасно.(Готово.)
  • Извлеките указатель типа объекта, на который указывает ссылка.
  • Является ли этот указатель типа таким же, как указатель типа элемента (простая проверка двоичного равенства)?Если так, то это прекрасно.(Готово.)
  • Совместимо ли это назначение указателя типа с указателем типа элемента?(Гораздо более сложная проверка, связанная с наследованием и интерфейсами.) Если да, то все в порядке - в противном случае выдайте исключение.

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

Итак, вот почему я считаю, что массивы ссылочных типов немного больше, чем массивы типов значений.

Отличный вопрос - действительно интересно вникнуть в него :)

Другие советы

Массив - это ссылочный тип.Все ссылочные типы содержат два дополнительных поля word.Ссылка на тип и поле индекса SyncBlock, которое, помимо прочего, используется для реализации блокировок в среде CLR.Таким образом, накладные расходы на тип ссылочных типов составляют 8 байт на 32 бита.Кроме того, сам массив также хранит длину, которая составляет еще 4 байта.Это приводит к тому, что общие служебные данные достигают 12 байт.

И я только что узнал из ответа Джона Скита, что массивы ссылочных типов имеют дополнительные накладные расходы в 4 байта.Это можно подтвердить с помощью WinDbg.Оказывается, что дополнительное слово является еще одной ссылкой на тип для типа, хранящегося в массиве.Все массивы ссылочных типов хранятся внутри как object[], с дополнительной ссылкой на объект типа фактического типа.Таким образом, a string[] на самом деле это просто object[] с дополнительной ссылкой на тип, относящейся к типу string.Более подробную информацию, пожалуйста, смотрите ниже.

Значения, хранящиеся в массивах:Массивы ссылочных типов содержат ссылки на объекты, поэтому каждая запись в массиве имеет размер ссылки (т.е.4 байта на 32 бита).Массивы типов значений хранят значения встроенно, и, таким образом, каждый элемент будет занимать размер рассматриваемого типа.

Этот вопрос также может представлять интерес: Список C #<double> размер против двойного [] размера

Кровавые Подробности

Рассмотрим следующий код

var strings = new string[1];
var ints = new int[1];

strings[0] = "hello world";
ints[0] = 42;

Прикрепление WinDbg показывает следующее:

Сначала давайте взглянем на массив типа значения.

0:000> !dumparray -details 017e2acc 
Name: System.Int32[]
MethodTable: 63b9aa40
EEClass: 6395b4d4
Size: 16(0x10) bytes
Array: Rank 1, Number of elements 1, Type Int32
Element Methodtable: 63b9aaf0
[0] 017e2ad4
    Name: System.Int32
    MethodTable 63b9aaf0
    EEClass: 6395b548
    Size: 12(0xc) bytes
     (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    63b9aaf0  40003f0        0         System.Int32  1 instance       42 m_value <=== Our value

0:000> !objsize 017e2acc 
sizeof(017e2acc) =           16 (        0x10) bytes (System.Int32[])

0:000> dd 017e2acc -0x4
017e2ac8  00000000 63b9aa40 00000001 0000002a <=== That's the value

Сначала мы выводим массив и один элемент со значением 42.Как можно видеть, размер составляет 16 байт.Это 4 байта для int32 само значение, 8 байт для служебных данных обычного ссылочного типа и еще 4 байта для длины массива.

Необработанный дамп показывает SyncBlock, таблицу методов для int[], длина и значение 42 (2a в шестнадцатеричном формате).Обратите внимание, что SyncBlock расположен непосредственно перед ссылкой на объект.

Далее, давайте посмотрим на string[] чтобы выяснить, для чего используется дополнительное слово.

0:000> !dumparray -details 017e2ab8 
Name: System.String[]
MethodTable: 63b74ed0
EEClass: 6395a8a0
Size: 20(0x14) bytes
Array: Rank 1, Number of elements 1, Type CLASS
Element Methodtable: 63b988a4
[0] 017e2a90
    Name: System.String
    MethodTable: 63b988a4
    EEClass: 6395a498
    Size: 40(0x28) bytes <=== Size of the string
     (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    String:     hello world    
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    63b9aaf0  4000096        4         System.Int32  1 instance       12 m_arrayLength
    63b9aaf0  4000097        8         System.Int32  1 instance       11 m_stringLength
    63b99584  4000098        c          System.Char  1 instance       68 m_firstChar
    63b988a4  4000099       10        System.String  0   shared   static Empty
    >> Domain:Value  00226438:017e1198 <<
    63b994d4  400009a       14        System.Char[]  0   shared   static WhitespaceChars
    >> Domain:Value  00226438:017e1760 <<

0:000> !objsize 017e2ab8 
sizeof(017e2ab8) =           60 (        0x3c) bytes (System.Object[]) <=== Notice the underlying type of the string[]

0:000> dd 017e2ab8 -0x4
017e2ab4  00000000 63b74ed0 00000001 63b988a4 <=== Method table for string
017e2ac4  017e2a90 <=== Address of the string in memory

0:000> !dumpmt 63b988a4
EEClass: 6395a498
Module: 63931000
Name: System.String
mdToken: 02000024  (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
BaseSize: 0x10
ComponentSize: 0x2
Number of IFaces in IFaceMap: 7
Slots in VTable: 196

Сначала мы выводим массив и строку.Затем мы сбрасываем размер string[].Обратите внимание, что WinDbg перечисляет тип как System.Object[] вот.Размер объекта в этом случае включает саму строку, поэтому общий размер равен 20 из массива плюс 40 для строки.

Сбрасывая необработанные байты экземпляра, мы можем увидеть следующее:Сначала у нас есть SyncBlock, затем следует таблица методов для object[], затем длина массива.После этого мы находим дополнительные 4 байта со ссылкой на таблицу методов для string.Это может быть проверено командой dumpmt, как показано выше.Наконец, мы находим единственную ссылку на фактический экземпляр string .

В заключение

Накладные расходы для массивов могут быть разбиты следующим образом (то есть на 32 бита)

  • блок синхронизации размером 4 байта
  • 4 байта для таблицы методов (ссылка на тип) для самого массива
  • 4 байта для длины массива
  • Массивы ссылочных типов добавляют еще 4 байта для хранения таблицы методов фактического типа элемента (массивы ссылочных типов являются object[] под капотом)

То есть.накладные расходы составляют 12 байт для массивов типов значений и 16 байт для массивов ссылочного типа.

Я думаю, что вы делаете некоторые ошибочные предположения при измерении, поскольку распределение памяти (через GetTotalMemory) во время вашего цикла может отличаться от фактической требуемой памяти только для массивов - память может быть выделена большими блоками, в памяти могут быть другие объекты, которые освобождаются во время цикла, и т.д.

Вот некоторая информация для вас о накладных расходах на массив:

Потому что управление кучей (поскольку вы имеете дело с GetTotalMemory) может выделять только довольно большие блоки, которые CLR выделяет меньшими порциями для целей программирования.

Прошу прощения за оффтопик, но только сегодня утром я нашел интересную информацию о перегрузке памяти.

У нас есть проект, который оперирует огромным объемом данных (до 2 ГБ).В качестве основного хранилища мы используем Dictionary<T,T>.На самом деле созданы тысячи словарей.После того, как измените его на List<T> для ключей и List<T> для значений (мы внедрили IDictionary<T,T> мы сами) использование памяти сократилось примерно на 30-40%.

Почему?

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top