Накладные расходы .NET-массива?

Я пытался определить накладные расходы заголовка в .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-битном процессе объект [1] должен иметь тот же размер, что и int [1], но на самом деле служебные переходы на 3,28 байт на

    237568
    Array overhead: 15.755

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

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

Ответ 1

Здесь немного более аккуратная (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. Не забывайте, что массивы ссылочных типов ковариантны, что может быть релевантным...

EDIT: с помощью cordbg я могу подтвердить ответ Брайана - указатель типа массива ссылочного типа тот же, независимо от фактического типа элемента. Предположительно, есть некоторая funkiness в 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 "векторный" тип является одномерным массивом с нижней границей 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 недостаточно хороша, чтобы проверить это), что тест выглядит примерно так:

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

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

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

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

Ответ 2

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

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

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

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

Детали Gory

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

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 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 байта со ссылкой на таблицу методов для строки. Это может быть подтверждено командой dumpmt, как показано выше. Наконец, мы находим единственную ссылку на фактический экземпляр строки.

В заключение

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

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

т.е. служебные данные 12 байт для массивов типов значений и 16 байт для массивов ссылочного типа.

Ответ 3

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

Ниже приведена информация для вас о перенапряжении массива:

Ответ 4

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

Ответ 5

Я сожалею о оффтопике, но сегодня утром я нашел интересную информацию об утрате памяти.

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

Почему?