Подтвердить что ты не робот

Почему выравнивание структуры зависит от того, является ли тип поля примитивным или определяемым пользователем?

В 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, что я хотел бы, чтобы поля были упакованы без указания постоянного смещения поля?

4b9b3361

Ответ 1

Я думаю, что это ошибка. Вы видите побочный эффект автоматической компоновки, ему нравится выравнивать нетривиальные поля с адресом, кратным 8 байтам в 64-битном режиме. Это происходит даже тогда, когда вы явно применяете атрибут [StructLayout(LayoutKind.Sequential)]. Это не должно произойти.

Вы можете увидеть это, сделав члены структуры общедоступными и добавив тестовый код следующим образом:

    var test = new RefAndTwoInt32Wrappers();
    test.text = "adsf";
    test.x.x = 0x11111111;
    test.y.x = 0x22222222;
    Console.ReadLine();      // <=== Breakpoint here

При достижении точки останова используйте Debug + Windows + Memory + Memory 1. Переключитесь на 4-байтные целые числа и поместите &test в поле Address:

 0x000000E928B5DE98  0ed750e0 000000e9 11111111 00000000 22222222 00000000 

0xe90ed750e0 - это указатель на моей машине (а не ваш). Вы можете легко увидеть Int32Wrappers, с дополнительными 4 байтами заполнения, которые превратили размер в 24 байта. Вернитесь к структуре и поместите последнюю. Повторите, и вы увидите, что указатель строки по-прежнему первый. Нарушение LayoutKind.Sequential, вы получили LayoutKind.Auto.

Трудно убедить Microsoft исправить это, он слишком долго работал таким образом, чтобы любое изменение что-то сломало. CLR только делает попытку почтить [StructLayout] для управляемой версии структуры и сделать ее более мягкой, она в целом быстро сдается. Известно, что любая структура, содержащая DateTime. Вы получаете истинную гарантию LayoutKind при маршалинге структуры. Маршалированная версия, конечно же, составляет 16 байт, так как Marshal.SizeOf() расскажет вам.

Использование LayoutKind.Explicit исправляет его, а не то, что вы хотели услышать.

Ответ 2

EDIT2

struct RefAndTwoInt32Wrappers
{
    public int x;
    public string s;
}

Этот код будет выровнен по 8 байт, поэтому структура будет иметь 16 байт. Для сравнения это:

struct RefAndTwoInt32Wrappers
{
    public int x,y;
    public string s;
}

Будет выровнено по 4 байт, поэтому эта структура также будет иметь 16 байт. Таким образом, обоснование здесь заключается в том, что struct aligment в CLR определяется количеством наиболее выровненных полей, классы, очевидно, не могут этого сделать, поэтому они останутся выровненными по 8 байт.

Теперь, если мы объединим все это и создадим struct:

struct RefAndTwoInt32Wrappers
{
    public int x,y;
    public Int32Wrapper z;
    public string s;
}

Он будет иметь 24 байта. {x, y} будет иметь по 4 байта каждый, а {z, s} будет иметь 8 байтов. Когда мы вводим тип ref в структуре, CLR всегда будет выравнивать нашу пользовательскую структуру, чтобы она соответствовала выравниванию класса.

struct RefAndTwoInt32Wrappers
{
    public Int32Wrapper z;
    public long l;
    public int x,y;  
}

Этот код будет содержать 24 байта, так как Int32Wrapper будет выровнен так же, как и длинный. Таким образом, настраиваемая оболочка структуры всегда будет соответствовать самому высокому/наилучшему выравниваемому полю в структуре или к собственным внутренним наиболее значимым полям. Таким образом, в случае строки ref, которая выравнивается по 8 байт, оболочка структуры будет соответствовать этому.

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


EDIT

Размеры фактически точны только при распределении в куче, но сами структуры имеют меньшие размеры (точные размеры полей). Дальнейший анализ шва, чтобы предположить, что это может быть ошибкой в ​​коде CLR, но нуждается в подкреплении доказательствами.

Я буду проверять код cli и публиковать дополнительные обновления, если будет найдено что-то полезное.


Это стратегия выравнивания, используемая распределителем .NET mem.

public static RefAndTwoInt32s[] test = new RefAndTwoInt32s[1];

static void Main()
{
    test[0].text = "a";
    test[0].x = 1;
    test[0].x = 1;

    Console.ReadKey();
}

Этот код, скомпилированный с .net40 под x64, В WinDbg позволяет сделать следующее:

Сначала найдите тип в куче:

    0:004> !dumpheap -type Ref
       Address               MT     Size
0000000003e72c78 000007fe61e8fb58       56    
0000000003e72d08 000007fe039d3b78       40    

Statistics:
              MT    Count    TotalSize Class Name
000007fe039d3b78        1           40 RefAndTwoInt32s[]
000007fe61e8fb58        1           56 System.Reflection.RuntimeAssembly
Total 2 objects

Как только мы это увидим, посмотрим, что по этому адресу:

    0:004> !do 0000000003e72d08
Name:        RefAndTwoInt32s[]
MethodTable: 000007fe039d3b78
EEClass:     000007fe039d3ad0
Size:        40(0x28) bytes
Array:       Rank 1, Number of elements 1, Type VALUETYPE
Fields:
None

Мы видим, что это ValueType и тот, который мы создали. Поскольку это массив, нам нужно получить значение ValueType def одного элемента в массиве:

    0:004> !dumparray -details 0000000003e72d08
Name:        RefAndTwoInt32s[]
MethodTable: 000007fe039d3b78
EEClass:     000007fe039d3ad0
Size:        40(0x28) bytes
Array:       Rank 1, Number of elements 1, Type VALUETYPE
Element Methodtable: 000007fe039d3a58
[0] 0000000003e72d18
    Name:        RefAndTwoInt32s
    MethodTable: 000007fe039d3a58
    EEClass:     000007fe03ae2338
    Size:        32(0x20) bytes
    File:        C:\ConsoleApplication8\bin\Release\ConsoleApplication8.exe
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        000007fe61e8c358  4000006        0            System.String      0     instance     0000000003e72d30     text
        000007fe61e8f108  4000007        8             System.Int32      1     instance                    1     x
        000007fe61e8f108  4000008        c             System.Int32      1     instance                    0     y

Структура на самом деле составляет 32 байта, так как 16 байтов зарезервированы для заполнения, поэтому в действительности каждая структура имеет размер не менее 16 байтов от get go.

если вы добавите 16 байтов из ints и строку ref: 0000000003e72d18 + 8 байтов EE/padding, вы закончите с 0000000003e72d30, и это будет точкой отсчета для ссылки на строку, и поскольку все ссылки имеют 8 байтов, заполненных от их первого фактическое поле данных это составляет для наших 32 байтов для этой структуры.

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

0:004> !do 0000000003e72d30    
Name:        System.String
MethodTable: 000007fe61e8c358
EEClass:     000007fe617f3720
Size:        28(0x1c) bytes
File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String:      a
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
000007fe61e8f108  40000aa        8         System.Int32  1 instance                1 m_stringLength
000007fe61e8d640  40000ab        c          System.Char  1 instance               61 m_firstChar
000007fe61e8c358  40000ac       18        System.String  0   shared           static Empty
                                 >> Domain:Value  0000000001577e90:NotInit  <<

Теперь проанализируем вышеуказанную программу таким же образом:

public static RefAndTwoInt32Wrappers[] test = new RefAndTwoInt32Wrappers[1];

static void Main()
{
    test[0].text = "a";
    test[0].x.x = 1;
    test[0].y.x = 1;

    Console.ReadKey();
}

0:004> !dumpheap -type Ref
     Address               MT     Size
0000000003c22c78 000007fe61e8fb58       56    
0000000003c22d08 000007fe039d3c00       48    

Statistics:
              MT    Count    TotalSize Class Name
000007fe039d3c00        1           48 RefAndTwoInt32Wrappers[]
000007fe61e8fb58        1           56 System.Reflection.RuntimeAssembly
Total 2 objects

Наша структура теперь составляет 48 байтов.

0:004> !dumparray -details 0000000003c22d08
Name:        RefAndTwoInt32Wrappers[]
MethodTable: 000007fe039d3c00
EEClass:     000007fe039d3b58
Size:        48(0x30) bytes
Array:       Rank 1, Number of elements 1, Type VALUETYPE
Element Methodtable: 000007fe039d3ae0
[0] 0000000003c22d18
    Name:        RefAndTwoInt32Wrappers
    MethodTable: 000007fe039d3ae0
    EEClass:     000007fe03ae2338
    Size:        40(0x28) bytes
    File:        C:\ConsoleApplication8\bin\Release\ConsoleApplication8.exe
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        000007fe61e8c358  4000009        0            System.String      0     instance     0000000003c22d38     text
        000007fe039d3a20  400000a        8             Int32Wrapper      1     instance     0000000003c22d20     x
        000007fe039d3a20  400000b       10             Int32Wrapper      1     instance     0000000003c22d28     y

Здесь ситуация та же, если мы добавим к 0000000003c22d18 + 8 байтам строки ref, мы закончим в начале первой Int-оболочки, где значение фактически укажет на адрес, на котором мы находимся.

Теперь мы можем видеть, что каждое значение является ссылкой на объект снова позволяет подтвердить это путем peeking 0000000003c22d20.

0:004> !do 0000000003c22d20
<Note: this object has an invalid CLASS field>
Invalid object

На самом деле это верно, поскольку его структура не сообщает нам ничего, если это obj или vt.

0:004> !dumpvc 000007fe039d3a20   0000000003c22d20    
Name:        Int32Wrapper
MethodTable: 000007fe039d3a20
EEClass:     000007fe03ae23c8
Size:        24(0x18) bytes
File:        C:\ConsoleApplication8\bin\Release\ConsoleApplication8.exe
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
000007fe61e8f108  4000001        0         System.Int32  1 instance                1 x

Таким образом, в действительности это больше похоже на тип Union, который на этот раз выровняет 8 байт (все прокладки будут выровнены с родительской структурой). Если бы это было не так, мы закончили бы с 20 байтами и это не было бы оптимально, поэтому распределитель памяти никогда не позволит ему произойти. Если вы снова сделаете математику, окажется, что структура действительно 40 байтов размера.

Итак, если вы хотите быть более консервативным с памятью, вы никогда не должны упаковывать его в структурированный тип структуры, но вместо этого используйте простые массивы. Другим способом является выделение памяти из кучи (например, VirtualAllocEx)   таким образом вам предоставляется собственный блок памяти, и вы управляете им так, как хотите.

Последний вопрос заключается в том, почему мы внезапно получаем такой макет. Хорошо, если вы сравниваете jited-код и производительность инкремента int [] с помощью struct [] с приращением поля счетчика, второй будет генерировать 8-байтовый выровненный адрес, являющийся объединением, но когда он смещен, это переводится в более оптимизированный ассемблерный код (singe LEA против нескольких MOV). Однако в случае, описанном здесь, производительность будет на самом деле хуже, поэтому я считаю, что это согласуется с базовой реализацией CLR, поскольку это настраиваемый тип, который может иметь несколько полей, поэтому может быть проще/лучше разместить начальный адрес вместо значение (так как это было бы невозможно) и там было бы заполнение структуры, что привело бы к большему размеру байта.

Ответ 3

Резюме см. @Aans Passant ответ, вероятно, выше. Layout Sequential не работает.


Некоторые тесты:

Это определенно только на 64 бит, а ссылка на объект "отравляет" структуру. 32 бит делает то, что вы ожидаете:

Environment: CLR 4.0.30319.34209 on Microsoft Windows NT 6.2.9200.0 (32 bit)
ConsoleApplication1.Int32Wrapper: 4
ConsoleApplication1.TwoInt32s: 8
ConsoleApplication1.TwoInt32Wrappers: 8
ConsoleApplication1.ThreeInt32Wrappers: 12
ConsoleApplication1.Ref: 4
ConsoleApplication1.RefAndTwoInt32s: 12
ConsoleApplication1.RefAndTwoInt32Wrappers: 12
ConsoleApplication1.RefAndThreeInt32s: 16
ConsoleApplication1.RefAndThreeInt32Wrappers: 16

Как только добавляется ссылка на объект, все структуры расширяются до 8 байтов, а размер их 4 байта. Расширение тестов:

Environment: CLR 4.0.30319.34209 on Microsoft Windows NT 6.2.9200.0 (64 bit)
ConsoleApplication1.Int32Wrapper: 4
ConsoleApplication1.TwoInt32s: 8
ConsoleApplication1.TwoInt32Wrappers: 8
ConsoleApplication1.ThreeInt32Wrappers: 12
ConsoleApplication1.Ref: 8
ConsoleApplication1.RefAndTwoInt32s: 16
ConsoleApplication1.RefAndTwoInt32sSequential: 16
ConsoleApplication1.RefAndTwoInt32Wrappers: 24
ConsoleApplication1.RefAndThreeInt32s: 24
ConsoleApplication1.RefAndThreeInt32Wrappers: 32
ConsoleApplication1.RefAndFourInt32s: 24
ConsoleApplication1.RefAndFourInt32Wrappers: 40

Как вы можете видеть, как только добавляется ссылка, каждый Int32Wrapper становится 8 байтами, поэтому это не просто выравнивание. Я сократил выделение массива, поскольку это было распределение LoH, которое по-разному выровнено.

Ответ 4

Просто добавьте некоторые данные в микс - я создал еще один тип из тех, что у вас были:

struct RefAndTwoInt32Wrappers2
{
    string text;
    TwoInt32Wrappers z;
}

Программа выписывает:

RefAndTwoInt32Wrappers2: 16

Итак, структура TwoInt32Wrappers корректно выравнивается в новой структуре RefAndTwoInt32Wrappers2.