Каков размер логического в С#? Действительно ли это занимает 4 байта?

У меня есть две структуры с массивами байтов и булевых:

using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct struct1
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
    public byte[] values;
}

[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct struct2
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
    public bool[] values;
}

И следующий код:

class main
{
    public static void Main()
    {
        Console.WriteLine("sizeof array of bytes: "+Marshal.SizeOf(typeof(struct1)));
        Console.WriteLine("sizeof array of bools: " + Marshal.SizeOf(typeof(struct2)));
        Console.ReadKey();
    }
}

Это дает мне следующий результат:

sizeof array of bytes: 3
sizeof array of bools: 12

Кажется, что boolean занимает 4 байта памяти. В идеале a boolean будет принимать только один бит (false или true, 0 или 1 и т.д.).

Что здесь происходит? Действительно ли тип boolean настолько неэффективен?

Ответ 1

Тип bool имеет клетчатую историю со многими несовместимыми выборами между языковыми режимами. Это началось с исторического выбора дизайна, сделанного Деннисом Ритчи, парнем, который изобрел C-язык. У него не было типа bool, альтернативой было int, где значение 0 представляет значение false, а любое другое значение считается истинным.

Этот выбор был перенесен в Winapi, основной причиной использования pinvoke, он имеет typedef для BOOL, который является псевдонимом для ключевого слова int compiler. Если вы не примените явный атрибут [MarshalAs], тогда С# bool преобразуется в BOOL, создавая поле длиной 4 байта.

Независимо от того, что вы делаете, ваше объявление структуры должно соответствовать совпадению с выбором времени выполнения, выполняемым на языке, с которым вы взаимодействуете. Как уже отмечалось, BOOL для winapi, но большинство реализаций на С++ выбрали байты, большинство COM Automation interop использует VARIANT_BOOL, который является коротким.

Фактический размер С# BOOL - один байт. Сильная цель дизайна CLR заключается в том, что вы не можете узнать. Макет - это деталь реализации, которая слишком сильно зависит от процессора. Процессоры очень разборчивы в отношении типов переменных и выравнивания, неправильный выбор может существенно повлиять на производительность и вызвать ошибки времени выполнения. Сделав макет неоткрываемым,.NET может обеспечить универсальную систему типов, которая не зависит от реальной реализации выполнения.

Другими словами, вы всегда должны маршировать структуру во время выполнения, чтобы приглушить макет. В это время выполняется преобразование из внутренней компоновки в макет interop. Это может быть очень быстрым, если макет идентичен, медленный, когда поля необходимо переустановить, поскольку для этого всегда требуется создать копию структуры. Технический термин для этого является blittable, передача blittable struct в собственный код выполняется быстро, потому что маркерщик pinvoke может просто передать указатель.

Производительность также является основной причиной того, что bool не является одним битом. Существует несколько процессоров, которые делают бит непосредственно адресуемым, а наименьший - байтом. Требуется дополнительная инструкция, чтобы выловить бит из байта, который не приходит бесплатно. И он никогда не является атомарным.

Компилятор С# не стесняется рассказывать вам, что он занимает 1 байт, используйте sizeof(bool). Это все еще не фантастический предсказатель для того, сколько байтов занимает поле во время выполнения, для CLR также необходимо реализовать модель памяти .NET, а promises - то, что простые обновления переменных являются атомарными. Это требует, чтобы переменные были правильно выровнены в памяти, чтобы процессор мог обновить его с помощью одного цикла шины памяти. Из-за этого довольно часто для bool на самом деле требуется 4 или 8 байтов. Дополнительное дополнение, которое было добавлено для обеспечения правильного выравнивания следующего элемента.

CLR на самом деле использует возможность того, что макет не может быть распознан, он может оптимизировать макет класса и перенастроить поля, чтобы скрыть его. Итак, скажем, если у вас есть класс с членом bool + int + bool, тогда он будет принимать 1 + (3) + 4 + 1 + (3) байты памяти, (3) является дополнением, в общей сложности 12 байт. 50% отходов. Автоматическая компоновка перестраивается до 1 + 1 + (2) + 4 = 8 байтов. Только класс имеет автоматическую компоновку, по умолчанию структуры имеют последовательный макет.

Более мрачно, bool может потребовать 32 байта в программе на С++, скомпилированной с помощью современного компилятора С++, который поддерживает набор инструкций AVX. Что требует 32-байтового требования к выравниванию, переменная bool может закончиться 31 байтом заполнения. Кроме того, основная причина, по которой .NET-джиттер не выводит SIMD-инструкции, если явно не упакован, он не может получить гарантию выравнивания.

Ответ 2

Во-первых, это только размер для взаимодействия. Он не представляет размер в управляемом коде массива. Это 1 байт за bool - по крайней мере, на моей машине. Вы можете проверить его самостоятельно с помощью этого кода:

using System;
class Program 
{ 
    static void Main(string[] args) 
    { 
        int size = 10000000;
        object array = null;
        long before = GC.GetTotalMemory(true); 
        array = new bool[size];
        long after = GC.GetTotalMemory(true); 

        double diff = after - before; 

        Console.WriteLine("Per value: " + diff / size);

        // Stop the GC from messing up our measurements 
        GC.KeepAlive(array); 
    } 
}

Теперь, для сортировки массивов по значению, как вы, документации, говорится:

Если для свойства MarshalAsAttribute.Value установлено значение ByValArray, поле SizeConst должно быть установлено так, чтобы указывать количество элементов в массиве. Поле ArraySubType может необязательно содержать UnmanagedType элементов массива, когда необходимо различать типы строк. Вы можете использовать этот UnmanagedType только в массиве, элементы которого отображаются как поля в структуре.

Итак, мы смотрим ArraySubType и имеем документацию по адресу:

Вы можете установить этот параметр в значение из перечисления UnmanagedType, чтобы указать тип элементов массива. Если тип не указан, используется неуправляемый тип по умолчанию, соответствующий типу элемента управляемого массива.

Теперь, глядя на UnmanagedType, есть:

Bool
4-байтовое логическое значение (true!= 0, false = 0). Это тип Win32 BOOL.

Итак, по умолчанию для bool и 4 байта, потому что это соответствует типу Win32 BOOL, поэтому, если вы взаимодействуете с кодом, ожидающим массив bool, он делает именно то, что вы хотите.

Теперь вы можете указать ArraySubType как I1, который задокументирован как:

1-байтовое целое число со знаком. Вы можете использовать этот элемент, чтобы преобразовать логическое значение в 1-байтовое, c-style bool (true = 1, false = 0).

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

[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3, ArraySubType = UnmanagedType.I1)]
public bool[] values;

В этом случае ваш код будет показывать, что, как и ожидалось, занимает 1 байт за значение.