Wie groß ist ein Boolescher Wert in C #? Dauert es wirklich 4 Bytes?

137

Ich habe zwei Strukturen mit Arrays von Bytes und Booleschen Werten:

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;
}

Und der folgende Code:

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();
    }
}

Das gibt mir folgende Ausgabe:

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

Es scheint, dass a boolean4 Bytes Speicher benötigt. Idealerweise boolean würde a nur ein Bit dauern ( falseoder true, 0oder 1, etc ..).

Was passiert hier? Ist der booleanTyp wirklich so ineffizient?

biv
quelle
7
Dies ist einer der ironischsten Zusammenstöße im andauernden Kampf der Gründe: Zwei ausgezeichnete Antworten von John und Hans haben es gerade geschafft, obwohl die Antworten auf diese Frage eher auf Meinungen als auf Fakten, Referenzen beruhen. oder spezifisches Fachwissen.
TaW
12
@TaW: Ich vermute, dass die engen Abstimmungen nicht auf die Antworten zurückzuführen waren, sondern auf den ursprünglichen Ton des OP, als sie die Frage zum ersten Mal stellten - sie beabsichtigten eindeutig, einen Kampf zu beginnen, und zeigten dies direkt in den jetzt gelöschten Kommentaren. Der größte Teil der Kruft wurde unter den Teppich gekehrt, aber sehen Sie sich die Revisionshistorie an, um einen Eindruck davon zu bekommen, was ich meine.
BoltClock
1
Warum nicht ein BitArray verwenden?
ded‘

Antworten:

242

Der Bool- Typ hat eine wechselvolle Historie mit vielen inkompatiblen Auswahlmöglichkeiten zwischen Sprachlaufzeiten. Dies begann mit einer historischen Designentscheidung von Dennis Ritchie, dem Erfinder der C-Sprache. Es gab keinen Bool- Typ, die Alternative war int, wobei ein Wert von 0 falsch darstellt und jeder andere Wert als wahr angesehen wurde .

Diese Auswahl wurde in Winapi übernommen, dem Hauptgrund für die Verwendung von Pinvoke. Es hat ein typedef, für BOOLdas ein Alias ​​für das Schlüsselwort int des C-Compilers ist . Wenn Sie ein explizites [MarshalAs] Attribut nicht gelten dann ein C # Bool auf eine BOOL umgewandelt wird, wodurch ein Feld erzeugt , das 4 Bytes lang ist.

Was auch immer Sie tun, Ihre Strukturdeklaration muss mit der Laufzeitauswahl in der Sprache übereinstimmen, mit der Sie interagieren. Wie bereits erwähnt, BOOL für den winapi aber die meist C ++ Implementierungen wählte Byte , die meisten COM Automation Interop verwendet VARIANT_BOOL die eine ist kurz .

Die tatsächliche Größe eines C # boolbeträgt ein Byte. Ein starkes Designziel der CLR ist, dass Sie es nicht herausfinden können. Das Layout ist ein Implementierungsdetail, das zu stark vom Prozessor abhängt. Prozessoren sind sehr wählerisch in Bezug auf Variablentypen und Ausrichtung. Falsche Entscheidungen können die Leistung erheblich beeinträchtigen und Laufzeitfehler verursachen. Indem das Layout nicht entdeckt werden kann, kann .NET ein universelles Typsystem bereitstellen, das nicht von der tatsächlichen Laufzeitimplementierung abhängt.

Mit anderen Worten, Sie müssen zur Laufzeit immer eine Struktur marshallen, um das Layout festzulegen. Zu diesem Zeitpunkt erfolgt die Konvertierung vom internen Layout zum Interop-Layout. Dies kann sehr schnell sein, wenn das Layout identisch ist, langsam, wenn Felder neu angeordnet werden müssen, da dafür immer eine Kopie der Struktur erstellt werden muss. Der Fachbegriff hierfür ist blittable . Das Übergeben einer blittable Struktur an nativen Code ist schnell, da der Pinvoke-Marshaller einfach einen Zeiger übergeben kann.

Leistung ist auch der Hauptgrund, warum ein Bool kein einzelnes Bit ist. Es gibt nur wenige Prozessoren, die ein Bit direkt adressierbar machen. Die kleinste Einheit ist ein Byte. Eine zusätzliche Anweisung ist erforderlich, um das Bit aus dem Byte zu fischen, das nicht kostenlos ist. Und es ist niemals atomar.

Der C # -Compiler scheut sich nicht, Ihnen mitzuteilen, dass es 1 Byte dauert sizeof(bool). Dies ist immer noch kein fantastischer Prädiktor für die Anzahl der Bytes, die ein Feld zur Laufzeit benötigt. Die CLR muss auch das .NET-Speichermodell implementieren und verspricht, dass einfache Variablenaktualisierungen atomar sind . Dies erfordert, dass die Variablen im Speicher richtig ausgerichtet sind, damit der Prozessor sie mit einem einzigen Speicherbuszyklus aktualisieren kann. Ziemlich oft benötigt ein Bool aus diesem Grund tatsächlich 4 oder 8 Bytes Speicher. Zusätzliche Polsterung, die hinzugefügt wurde, um sicherzustellen, dass das nächste Element richtig ausgerichtet ist.

Die CLR nutzt den Vorteil, dass das Layout nicht entdeckt werden kann. Sie kann das Layout einer Klasse optimieren und die Felder neu anordnen, sodass die Auffüllung minimiert wird. Wenn Sie also eine Klasse mit einem bool + int + bool-Mitglied haben, würde es 1 + (3) + 4 + 1 + (3) Bytes Speicher benötigen, (3) ist das Auffüllen für insgesamt 12 Bytes. 50% Abfall. Das automatische Layout wird auf 1 + 1 + (2) + 4 = 8 Byte umgestellt. Nur eine Klasse hat ein automatisches Layout, Strukturen haben standardmäßig ein sequentielles Layout.

Noch düsterer ist, dass ein Bool in einem C ++ - Programm, das mit einem modernen C ++ - Compiler kompiliert wurde, der den AVX-Befehlssatz unterstützt, bis zu 32 Byte benötigen kann. Was eine 32-Byte-Ausrichtungsanforderung auferlegt, kann die Bool-Variable mit 31 Bytes Auffüllen enden. Auch der Hauptgrund, warum ein .NET-Jitter keine SIMD-Anweisungen ausgibt, kann die Ausrichtungsgarantie nicht erhalten, es sei denn, er ist explizit verpackt.

Hans Passant
quelle
Betreff
Aron
2
Würden Sie für einen interessierten, aber nicht informierten Leser klarstellen, ob der letzte Absatz wirklich 32 Bytes und keine Bits lesen sollte ?
Dumme Freak
3
Ich bin mir nicht sicher, warum ich das alles gerade gelesen habe (da ich nicht so viele Details brauche), aber das ist faszinierend und gut geschrieben.
Frank V
2
@Silly - es sind Bytes . AVX verwendet 512-Bit-Variablen, um 8 Gleitkommawerte mit einem einzigen Befehl zu berechnen. Eine solche 512-Bit-Variable erfordert eine Ausrichtung auf 32.
Hans Passant
3
Beeindruckend! Ein Beitrag gab eine Menge Themen zu verstehen. Deshalb lese ich gerne nur die wichtigsten Fragen.
Chaitanya Gadkari
151

Erstens ist dies nur die Größe für Interop. Es repräsentiert nicht die Größe im verwalteten Code des Arrays. Das ist 1 Byte pro bool- zumindest auf meinem Computer. Sie können es selbst mit diesem Code testen:

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); 
    } 
}

Für das Marshalling von Arrays nach Wert wie in der Dokumentation heißt es in der Dokumentation :

Wenn die MarshalAsAttribute.Value-Eigenschaft auf festgelegt ist, ByValArraymuss das SizeConst-Feld festgelegt werden, um die Anzahl der Elemente im Array anzugeben. Das ArraySubTypeFeld kann optional UnmanagedTypedie Array-Elemente enthalten, wenn zwischen Zeichenfolgentypen unterschieden werden muss. Sie können dies UnmanagedTypenur für ein Array verwenden, dessen Elemente als Felder in einer Struktur angezeigt werden.

Also schauen wir uns an ArraySubType, und das hat Dokumentation von:

Sie können diesen Parameter auf einen Wert aus der UnmanagedTypeAufzählung setzen, um den Typ der Elemente des Arrays anzugeben. Wenn kein Typ angegeben wird, wird der nicht verwaltete Standardtyp verwendet, der dem Elementtyp des verwalteten Arrays entspricht.

Jetzt UnmanagedTypegibt es:

Bool
Ein 4-Byte-Boolescher Wert (true! = 0, false = 0). Dies ist der Win32 BOOL-Typ.

Das ist also die Standardeinstellung für boolund es sind 4 Bytes, da dies dem Win32 BOOL-Typ entspricht. Wenn Sie also mit Code zusammenarbeiten, der ein BOOLArray erwartet , macht es genau das, was Sie wollen.

Jetzt können Sie stattdessen das ArraySubTypeas angeben, das wie I1folgt dokumentiert ist:

Eine 1-Byte-Ganzzahl mit Vorzeichen. Mit diesem Element können Sie einen Booleschen Wert in einen 1-Byte-Bool im C-Stil umwandeln (true = 1, false = 0).

Wenn der Code, mit dem Sie zusammenarbeiten, 1 Byte pro Wert erwartet, verwenden Sie einfach:

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

Ihr Code zeigt dann an, dass erwartungsgemäß 1 Byte pro Wert belegt wird.

Jon Skeet
quelle