В Noda Time v2 мы переходим к наносекундному разрешению. Это означает, что мы больше не можем использовать 8-байтовое целое число для представления всего интересующего нас диапазона времени. Это побудило меня исследовать использование памяти (многими) структурами Noda Time, что, в свою очередь, привело меня к чтобы выявить небольшую странность в решении CLR о выравнивании.
Во - первых, я понимаю , что это является решение реализации, и что поведение по умолчанию может измениться в любой момент. Я понимаю, что могу изменить его с помощью [StructLayout]
и [FieldOffset]
, но я бы предпочел найти решение, которое не требует этого, если это возможно.
Мой основной сценарий состоит в том, что у меня есть struct
поле, содержащее поле ссылочного типа и два других поля типа значения, для которых эти поля являются простыми оболочками int
. Я надеялся , что это будет представлено как 16 байтов в 64-битной среде CLR (8 для справки и 4 для каждого из остальных), но по какой-то причине он использует 24 байта. Между прочим, я измеряю пространство с помощью массивов - я понимаю, что макет может отличаться в разных ситуациях, но это было разумной отправной точкой.
Вот пример программы, демонстрирующей проблему:
using System;
using System.Runtime.InteropServices;
#pragma warning disable 0169
struct Int32Wrapper
{
int x;
}
struct TwoInt32s
{
int x, y;
}
struct TwoInt32Wrappers
{
Int32Wrapper x, y;
}
struct RefAndTwoInt32s
{
string text;
int x, y;
}
struct RefAndTwoInt32Wrappers
{
string text;
Int32Wrapper x, y;
}
class Test
{
static void Main()
{
Console.WriteLine("Environment: CLR {0} on {1} ({2})",
Environment.Version,
Environment.OSVersion,
Environment.Is64BitProcess ? "64 bit" : "32 bit");
ShowSize<Int32Wrapper>();
ShowSize<TwoInt32s>();
ShowSize<TwoInt32Wrappers>();
ShowSize<RefAndTwoInt32s>();
ShowSize<RefAndTwoInt32Wrappers>();
}
static void ShowSize<T>()
{
long before = GC.GetTotalMemory(true);
T[] array = new T[100000];
long after = GC.GetTotalMemory(true);
Console.WriteLine("{0}: {1}", typeof(T),
(after - before) / array.Length);
}
}
И сборка и вывод на моем ноутбуке:
c:\Users\Jon\Test>csc /debug- /o+ ShowMemory.cs
Microsoft (R) Visual C# Compiler version 12.0.30501.0
for C# 5
Copyright (C) Microsoft Corporation. All rights reserved.
c:\Users\Jon\Test>ShowMemory.exe
Environment: CLR 4.0.30319.34014 on Microsoft Windows NT 6.2.9200.0 (64 bit)
Int32Wrapper: 4
TwoInt32s: 8
TwoInt32Wrappers: 8
RefAndTwoInt32s: 16
RefAndTwoInt32Wrappers: 24
Так:
- Если у вас нет поля ссылочного типа, среда CLR с радостью упакует
Int32Wrapper
поля вместе (TwoInt32Wrappers
имеет размер 8). - Даже с полем ссылочного типа среда CLR по-прежнему может упаковывать
int
поля вместе (RefAndTwoInt32s
имеет размер 16). - Комбинируя эти два, каждое
Int32Wrapper
поле кажется дополненным / выровненным до 8 байтов. (RefAndTwoInt32Wrappers
имеет размер 24.) - Выполнение того же кода в отладчике (но все еще сборка выпуска) показывает размер 12.
Еще несколько экспериментов дали аналогичные результаты:
- Помещение поля ссылочного типа после полей типа значения не помогает
- Использование
object
вместоstring
не помогает (я ожидаю, что это "любой ссылочный тип") - Использование другой структуры в качестве «оболочки» вокруг ссылки не помогает
- Использование общей структуры в качестве оболочки вокруг ссылки не помогает
- Если я продолжу добавлять поля (попарно для простоты),
int
поля все равно будут считаться 4 байтами, аInt32Wrapper
поля - 8 байтами. - Добавление
[StructLayout(LayoutKind.Sequential, Pack = 4)]
к каждой видимой структуре не меняет результатов
Есть ли у кого-нибудь объяснение этого (в идеале со справочной документацией) или предложение о том, как я могу намекнуть CLR, что я бы хотел, чтобы поля были упакованы без указания постоянного смещения поля?
Ref<T>
а используетеstring
вместо этого, не то чтобы это должно иметь значение.TwoInt32Wrappers
, илиInt64
и аTwoInt32Wrappers
? Как насчет того, чтобы создать общий шаблон,Pair<T1,T2> {public T1 f1; public T2 f2;}
а затем создатьPair<string,Pair<int,int>>
иPair<string,Pair<Int32Wrapper,Int32Wrapper>>
? Какие комбинации вынуждают JITter сглаживать вещи?Pair<string, TwoInt32Wrappers>
это даст только 16 байт, так что бы решить эту проблему. Захватывающий.Marshal.SizeOf
вернет размер структуры, которая будет передана в машинный код, который не обязательно должен иметь какое-либо отношение к размеру структуры в .NET-коде.Ответы:
Я считаю это ошибкой. Вы видите побочный эффект автоматической компоновки: ей нравится выравнивать нетривиальные поля по адресу, кратному 8 байтам в 64-битном режиме. Это происходит даже тогда, когда вы явно применяете
[StructLayout(LayoutKind.Sequential)]
атрибут. Этого не должно быть.Вы можете увидеть это, сделав члены структуры общедоступными и добавив тестовый код следующим образом:
При достижении точки останова используйте Debug + Windows + Memory + Memory 1. Переключитесь на 4-байтовые целые числа и введите
&test
в поле Address:0xe90ed750e0
- это строковый указатель на моей машине (не на вашей). Вы можете легко увидетьInt32Wrappers
, с дополнительными 4 байтами заполнения, которые превратили размер в 24 байта. Вернитесь к структуре и поместите строку последней. Повторите, и вы увидите, что указатель на строку все еще находится первым. НарушаяLayoutKind.Sequential
, вы попалиLayoutKind.Auto
.Будет сложно убедить Microsoft исправить это, это работало так слишком долго, поэтому любое изменение что-то сломает . CLR только пытается
[StructLayout]
учесть управляемую версию структуры и сделать ее непреобразуемой, но в целом быстро отказывается. Как известно, для любой структуры, содержащей DateTime. Вы получаете настоящую гарантию LayoutKind только при маршалинге структуры. Маршалированная версия, конечно же, составляет 16 байт, какMarshal.SizeOf()
вы скажете.Использование
LayoutKind.Explicit
исправляет это, а не то, что вы хотели услышать.источник
string
другим новым ссылочным типом (class
), к которому он был применен[StructLayout(LayoutKind.Sequential)]
, похоже, ничего не меняет. В обратном направлении, применяя[StructLayout(LayoutKind.Auto)]
кstruct Int32Wrapper
изменениям использования памяти вTwoInt32Wrappers
.EDIT2
Этот код будет выровнен по 8 байтов, поэтому структура будет иметь 16 байтов. Для сравнения:
Будет выровнено по 4 байта, поэтому эта структура также будет иметь 16 байтов. Таким образом, обоснование здесь состоит в том, что выравнивание структуры в CLR определяется количеством наиболее выровненных полей, очевидно, что clases не могут этого сделать, поэтому они будут оставаться выровненными на 8 байт.
Теперь, если мы объединим все это и создадим структуру:
Он будет иметь 24 байта, {x, y} будет иметь 4 байта каждый, а {z, s} будет иметь 8 байтов. После того, как мы введем тип ref в структуру, CLR всегда будет выравнивать нашу настраиваемую структуру в соответствии с выравниванием класса.
Этот код будет иметь 24 байта, поскольку Int32Wrapper будет выровнен так же, как и long. Таким образом, пользовательская оболочка структуры всегда будет выравниваться по полю с самым высоким / наилучшим выравниванием в структуре или по своим внутренним наиболее значимым полям. Таким образом, в случае строки ссылки, которая выровнена по 8 байтов, оболочка структуры будет выровнена по ней.
Заключение настраиваемого поля структуры внутри структуры всегда будет выровнено по наивысшему выровненному полю экземпляра в структуре. Теперь, если я не уверен, что это ошибка, но без каких-либо доказательств, я буду придерживаться своего мнения, что это может быть сознательное решение.
РЕДАКТИРОВАТЬ
На самом деле размеры точны только при размещении в куче, но сами структуры имеют меньшие размеры (точные размеры их полей). Дальнейший анализ позволяет предположить, что это может быть ошибка в коде CLR, но она должна быть подтверждена доказательствами.
Я проверю код cli и опубликую обновления, если будет найдено что-то полезное.
Это стратегия выравнивания, используемая распределителем памяти .NET.
Этот код, скомпилированный с .net40 под x64, в WinDbg позволяет делать следующее:
Давайте сначала найдем тип в куче:
Как только он у нас будет, давайте посмотрим, что находится под этим адресом:
Мы видим, что это ValueType, и это тот, который мы создали. Поскольку это массив, нам нужно получить значение ValueType def для одного элемента в массиве:
На самом деле структура составляет 32 байта, так как 16 байтов зарезервированы для заполнения, поэтому на самом деле каждая структура с самого начала имеет размер не менее 16 байтов.
если вы добавите 16 байтов из целых чисел и ссылку на строку в: 0000000003e72d18 + 8 байтов EE / заполнение, вы получите 0000000003e72d30, и это начальная точка для ссылки на строку, и поскольку все ссылки заполнены 8 байтами от их первого фактического поля данных это составляет 32 байта для этой структуры.
Посмотрим, действительно ли строка заполнена таким образом:
Теперь давайте проанализируем вышеуказанную программу таким же образом:
Наша структура теперь составляет 48 байтов.
Здесь ситуация такая же, если мы добавим к 0000000003c22d18 + 8 байтов строки ref, мы окажемся в начале первой оболочки Int, где значение фактически указывает на адрес, по которому мы находимся.
Теперь мы видим, что каждое значение является ссылкой на объект, что снова позволяет подтвердить это, просмотрев 0000000003c22d20.
На самом деле это правильно, так как это структура, адрес которой ничего не говорит нам, если это объект или vt.
Так что на самом деле это больше похоже на тип Union, который на этот раз будет выровнен по 8 байтам (все отступы будут выровнены с родительской структурой). Если бы это было не так, у нас было бы 20 байтов, а это не оптимально, поэтому распределитель памяти никогда не допустит этого. Если вы снова выполните математические вычисления, окажется, что структура действительно имеет размер 40 байт.
Поэтому, если вы хотите быть более консервативным с памятью, вам никогда не следует упаковывать ее в специальный тип структуры struct, а вместо этого использовать простые массивы. Другой способ - выделить память из кучи (например, VirtualAllocEx). Таким образом, вам предоставляется собственный блок памяти, и вы управляете им так, как хотите.
Последний вопрос: почему вдруг мы можем получить такой макет? Хорошо, если вы сравните код jited и производительность приращения int [] с struct [] с приращением поля счетчика, второй будет генерировать 8-байтовый выровненный адрес, являющийся объединением, но при jited это переводится в более оптимизированный код сборки (одиночный LEA против нескольких MOV). Однако в случае, описанном здесь, производительность будет на самом деле хуже, поэтому я считаю, что это согласуется с базовой реализацией CLR, поскольку это настраиваемый тип, который может иметь несколько полей, поэтому может быть проще / лучше указать начальный адрес вместо value (поскольку это было бы невозможно) и сделайте там заполнение структуры, что приведет к большему размеру байта.
источник
RefAndTwoInt32Wrappers
не 32 байта, а 24, что соответствует моему коду. Если вы посмотрите в представление памяти вместо использованияdumparray
и посмотрите на память для массива с (скажем) 3 элементами с различимыми значениями, вы можете ясно увидеть, что каждый элемент состоит из 8-байтовой строковой ссылки и двух 8-байтовых целых чисел , Я подозреваю, чтоdumparray
он показывает значения как ссылки просто потому, что не умеет отображатьInt32Wrapper
значения. Эти «ссылки» указывают на самих себя; это не отдельные ценности.dumparray
показано.Int32
. Честно говоря, меня не слишком беспокоит, что он делает в стеке, но я его не проверял.Резюме см., Вероятно, ответ @Hans Passant выше. Layout Sequential не работает
Некоторое тестирование:
Это определенно только на 64-битной, и ссылка на объект «отравляет» структуру. 32 бит делает то, что вы ожидаете:
Как только добавляется ссылка на объект, все структуры расширяются до 8 байтов, а не до 4 байтов. Расширяем тесты:
Как вы можете видеть, как только ссылка добавляется, каждый Int32Wrapper становится 8 байтов, поэтому выравнивание не простое. Я сократил выделение массива, если это было выделение LoH, которое выровнено по-другому.
источник
Просто чтобы добавить немного данных в микс - я создал еще один тип из тех, что были у вас:
Программа выписывает:
Итак, похоже, что
TwoInt32Wrappers
структура правильно выровнена в новойRefAndTwoInt32Wrappers2
структуре.источник