Накладные расходы .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%.
Почему?