StructLayoutНа самом деле довольно интересный аттрибут.
Давайте рассмотрим пример:
struct MyStruct {
public int a;
public byte b;
public int c;
public byte d;
}
Размер такой структуры определяется следующим образом:
public int a; // 4 байта
public byte b; // 1 байт
public int c; // 4 байта
public byte d; // 1 байт
Складываем, получаем 4 + 1 + 4 + 1 = 10 байт
Казалось бы, что тут сложного. Не все так просто
😉Существует такое понятие как Pack size, то есть каким образом будет выровнена в памяти, если простым языком - каждая переменная будет минимум занимать размер pack size, максимум - кратное значение этому размеру:
public byte b; // 4 байта при Pack = 4
public byte b; // 1 байт при Pack = 1
Таким образом размер структуры будет вычисляться так:
public int a; // 4 байта
public byte b; // 4 байта
public int c; // 4 байта
public byte d; // 4 байта
Итого: 16 байт вместо 10 байт
Но мы умные и давайте переставим поля таким образом:
public int a; // 4 байта
public int c; // 4 байта
public byte b; // 1 байт
public byte d; // 1 байт
Получается, что теперь будет 10? А вот и снова нет
🙂Теперь будет 12 байт. Почему так произошло?
Потому что последние два байта будут выровнены до 4х.
Что вообще такое Pack size и где он задается, о котором шла речь?
Это параметр аттрибута StructLayout:
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct MyStruct {
public int a;
public byte b;
public int c;
public byte d;
}
Мы можем задать Pack = 1, чтобы запаковать структуру по одному байту, таким образом мы получим 10 байт.
Что не так с паковкой по одному байту и почему не паковать все структуры таким образом по-умолчанию?
Ну, во-первых, это нарушает выравнивание в памяти. Например, если вы захотите после такого сделать Interlocked.Add(ref s.c), то получите краш, т.к. аддрес в памяти у поля c будет не кратным 4, а это приведет к крашу.
Во-вторых, я не знаю аллокатора, который не применяет общее выравнивание аллоцируемых объектов, т.е. в памяти он вероятнее всего будет занимать 12 байт, а не 10.
Что еще есть у StructLayout?
Еще есть Size, которым мы можем ограничить размер структуры до минимально необходимого:
[StructLayout(LayoutKind.Sequential, Size = 10)]
struct MyStruct {
public int a;
public int c;
public byte b;
public byte d;
}
Заметьте, что я специально переставил поля, т.к. если этого не сделать, то будет интересный эффект:
Размер sizeof(MyStruct) вернет нам 13 (т.к. Pack = 4, последний байт будет обрезан), а вот Marshal.SizeOf(s) вернет нам 10, т.к. он берет тип объекта и возвращает сколько нам необходимо было байт, чтобы создать этот инстанс, ведь Marshal.SizeOf принимает именно фактический инстанс объекта.
В любом случае, такого нужно не допускать.
Что про LayoutKind?
Для структур это значение может принимать 2 варианта:
LayoutKind.Sequential - как поля объявлены, так и раскладываем в памяти.
LayoutKind.Explicit - ручное распределение, необходимо указать FieldOffset аттрибут для каждого поля.
С Explicit можно "наслаивать" поля друг на друга, как самый простой вариант:
[StructLayout(LayoutKind.Explicit)]
struct MyStruct {
[FieldOffset(0)]
public int a;
[FieldOffset(4)]
public int c;
[FieldOffset(0)]
public long b;
}
Т.е. положили значения a и c, забрали одно значение b, которое будет содержать 2 int.
#unsafe #structlayout #sizeof