Каков размер логического значения в C #? Неужели это занимает 4 байта?

138

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

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

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

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

бив
источник
7
Это одно из самых ироничных столкновений в продолжающейся битве за причины удержания: два отличных ответа Джона и Ганса только что дали его, хотя ответы на этот вопрос будут, как правило, почти полностью основываться на мнениях, а не на фактах, ссылках, или конкретный опыт.
TaW
12
@TaW: Я предполагаю, что близкие голоса были вызваны не ответами, а исходным тоном ОП, когда они впервые задали вопрос - они явно намеревались начать драку и прямо показали это в теперь удаленных комментариях. Большая часть мусора спрятана под ковер, но посмотрите историю изменений, чтобы понять, что я имею в виду.
BoltClock
1
Почему бы не использовать BitArray?
ded '16

Ответы:

245

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

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

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

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

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

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

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

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

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

Ганс Пассан
источник
2
Для заинтересованного, но не информированного читателя, не могли бы вы пояснить, действительно ли последний абзац должен читать 32 байта, а не бит ?
Silly Freak
3
Не уверен, почему я просто прочитал все это (поскольку мне не нужно столько подробностей), но это интересно и хорошо написано.
Фрэнк V
3
@Silly - это байты . AVX использует 512-битные переменные для вычисления 8 значений с плавающей запятой с помощью одной инструкции. Такая 512-битная переменная требует выравнивания до 32.
Ханс Пассан
3
Вот это да! один пост дал чертовски много тем для понимания. Вот почему мне нравится просто читать самые популярные вопросы.
Чайтанья Гадкари
154

Во-первых, это только размер для взаимодействия. Он не представляет размер в управляемом коде массива. Это 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массив, он делает именно то, что вы хотите.

Теперь вы можете указать ArraySubTypeas I1вместо этого, что задокументировано как:

1-байтовое целое число со знаком. Этот член можно использовать для преобразования логического значения в 1-байтовое логическое значение в стиле C (true = 1, false = 0).

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

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

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

Джон Скит
источник