Gibt es eine Standardmethode oder eine Standardalternative zum Packen einer Struktur in c?

13

Bei der Programmierung in CI war es von unschätzbarem Wert, Strukturen mithilfe des GCCs- __attribute__((__packed__))Attributs zu packen , damit ich leicht einen strukturierten Teil des flüchtigen Speichers in ein Array von Bytes konvertieren kann, die über einen Bus übertragen, gespeichert oder auf einen Registerblock angewendet werden. Gepackte Strukturen garantieren, dass sie, wenn sie als Array von Bytes behandelt werden, keine Auffüllung enthalten, was sowohl verschwenderisch als auch ein mögliches Sicherheitsrisiko darstellt und möglicherweise mit Schnittstellenhardware nicht kompatibel ist.

Gibt es keinen Standard für Packstrukturen, der in allen C-Compilern funktioniert? Wenn nicht, bin ich ein Ausreißer in der Annahme, dass dies ein kritisches Merkmal für die Systemprogrammierung ist? Haben frühe Benutzer der Sprache C keine Notwendigkeit für Packstrukturen gefunden oder gibt es eine Alternative?

satur9nine
quelle
Die Verwendung von Strukturen über Kompilierungsdomänen hinweg ist eine sehr schlechte Idee, insbesondere um auf Hardware (die eine andere Kompilierungsdomäne ist) zu verweisen. Paketstrukturen sind nur ein Trick, sie haben viele schlechte Nebenwirkungen, daher gibt es viele andere Lösungen für Ihre Probleme mit weniger Nebenwirkungen, und diese sind portabler.
old_timer

Antworten:

12

In einer Struktur ist der Versatz jedes Mitglieds von der Adresse jeder Strukturinstanz von Bedeutung. Es geht nicht so sehr darum, wie eng die Dinge sind.

Ein Array spielt jedoch eine Rolle, wie es "gepackt" wird. Die Regel in C lautet, dass jedes Array-Element genau N Bytes vom vorherigen enthält, wobei N die Anzahl der Bytes ist, die zum Speichern dieses Typs verwendet werden.

Aber mit einer Struktur gibt es kein solches Bedürfnis nach Einheitlichkeit.

Hier ist ein Beispiel für ein seltsames Packschema:

Freescale (Hersteller von Kfz-Mikrocontrollern) stellt ein Mikro her, das über einen Coprozessor für die Zeitverarbeitungseinheit verfügt (Google für eTPU oder TPU). Es hat zwei native Datengrößen, 8 Bit und 24 Bit, und behandelt nur Ganzzahlen.

Diese Struktur:

struct a
{
  U24 elementA;
  U24 elementB;
};

wird sehen, dass jeder U24 seinen eigenen 32-Bit-Block speichert, aber nur im höchsten Adressbereich.

Dies:

struct b
{
  U24 elementA;
  U24 elementB;
  U8  elementC;
};

werden zwei U24s in benachbarten 32-Bit-Blöcken gespeichert, und die U8 werden in dem "Loch" vor der ersten U24 gespeichert elementA.

Sie können den Compiler jedoch anweisen, alles in einen eigenen 32-Bit-Block zu packen, wenn Sie möchten. Es ist teurer im RAM, benötigt aber weniger Anweisungen für Zugriffe.

"Packen" bedeutet nicht "dicht packen" - es bedeutet nur ein Schema zum Anordnen von Elementen einer Struktur in Bezug auf den Offset.

Es gibt kein generisches Schema, es ist vom Compiler + der Architektur abhängig.

RichColours
quelle
1
Wenn der Compiler für die TPU umlagert struct bzu bewegen , elementCbevor eine der anderen Elemente, dann ist es nicht ein konformer C - Compiler. Eine Elementumlagerung ist in C
Bart van Ingen Schenau am
Interessant, aber U24 ist kein Standard-C-Typ en.m.wikipedia.org/wiki/C_data_types, so dass es nicht verwunderlich ist, dass der Complier gezwungen ist, etwas seltsam damit umzugehen.
Samstag,
Es teilt sich den RAM mit dem Haupt-CPU-Kern, der eine Wortgröße von 32 Bit hat. Dieser Prozessor verfügt jedoch über eine ALU, die nur 24 Bit oder 8 Bit verarbeitet. Es gibt also ein Schema für die Anordnung von 24-Bit-Zahlen in 32-Bit-Wörtern. Nicht standard, aber ein großartiges Beispiel für Verpackung und Ausrichtung. Einverstanden, es ist sehr unüblich.
RichColours
6

Bei der Programmierung in CI war es von unschätzbarem Wert, Strukturen mit GCCs zu packen __attribute__((__packed__))[...]

Da Sie erwähnen __attribute__((__packed__)), gehe ich davon aus , dass Sie beabsichtigen, alle Auffüllungen in a zu entfernen struct(jedes Mitglied muss eine 1-Byte-Ausrichtung haben).

Gibt es keinen Standard für Packstrukturen, der in allen C-Compilern funktioniert?

... und die Antwort ist "nein". Das Auffüllen und Ausrichten von Daten in Bezug auf eine Struktur (und zusammenhängende Arrays von Strukturen im Stapel oder Heap) gibt es aus einem wichtigen Grund. Auf vielen Computern kann ein nicht ausgerichteter Speicherzugriff zu erheblichen Leistungseinbußen führen (bei einigen neueren Hardware-Versionen ist dies jedoch weniger der Fall). In einigen seltenen Fällen führt ein falsch ausgerichteter Speicherzugriff zu einem nicht behebbaren Busfehler (der sogar das gesamte Betriebssystem zum Absturz bringen kann).

Da der C-Standard auf Portabilität ausgerichtet ist, ist es wenig sinnvoll, eine Standardmethode zu haben, mit der alle Auffüllungen in einer Struktur beseitigt werden und beliebige Felder nur falsch ausgerichtet werden können, da dies möglicherweise dazu führen kann, dass C-Code nicht mehr portabel ist.

Die sicherste und portabelste Möglichkeit, solche Daten auf eine Weise an eine externe Quelle auszugeben, bei der jegliches Auffüllen vermieden wird, besteht darin, in Byte-Streams zu serialisieren, anstatt nur zu versuchen, den Rohspeicherinhalt Ihrer Daten zu übertragen structs. Dies verhindert auch, dass Ihr Programm Leistungseinbußen außerhalb dieses Serialisierungskontexts erleidet, und ermöglicht es Ihnen, neue Felder hinzuzufügen, structohne die gesamte Software auszulösen und fehlerhaft zu machen. Es gibt Ihnen auch etwas Raum, sich mit Endianness und ähnlichen Dingen zu befassen, falls dies jemals zu einem Problem wird.

Es gibt eine Möglichkeit, das gesamte Auffüllen zu eliminieren, ohne nach compilerspezifischen Anweisungen zu greifen. Dies ist jedoch nur dann möglich, wenn die relative Reihenfolge zwischen den Feldern keine Rolle spielt. Gegeben so etwas:

struct Foo
{
    double x;  // assume 8-byte alignment
    char y;    // assume 1-byte alignment
               // 7 bytes of padding for first field
};

... benötigen wir die Auffüllung für den ausgerichteten Speicherzugriff in Bezug auf die Adresse der Struktur, die diese Felder enthält, wie folgt:

0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF
x_______y.......x_______y.......x_______y.......x_______y.......

... wo .Polsterung anzeigt. Jeder xmuss an einer 8-Byte-Grenze ausgerichtet sein, um die Leistung (und manchmal sogar das richtige Verhalten) zu gewährleisten.

Sie können das Auffüllen auf tragbare Weise beseitigen, indem Sie eine SoA-Darstellung (Structure of Array) wie folgt verwenden (nehmen wir an, wir benötigen 8 FooInstanzen):

struct Foos
{
   double x[8];
   char y[8];
};

Wir haben die Struktur effektiv abgerissen. In diesem Fall sieht die Speicherdarstellung folgendermaßen aus:

0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF
x_______x_______x_______x_______x_______x_______x_______x_______

... und das:

01234567
yyyyyyyy

... kein Padding-Overhead mehr und kein falsch ausgerichteter Speicherzugriff, da auf diese Datenfelder nicht mehr als Versatz einer Strukturadresse zugegriffen wird, sondern als Versatz einer Basisadresse für ein Array.

Dies hat auch den Vorteil, dass der sequenzielle Zugriff schneller ist, da weniger Daten verbraucht werden müssen (keine irrelevanten Auffüllungen im Mix mehr, um die relevante Datenverbrauchsrate der Maschine zu verlangsamen) und der Compiler die Verarbeitung sehr trivial vektorisieren kann .

Der Nachteil ist, dass das Codieren eine PITA ist. Es ist auch potenziell weniger effizient für den wahlfreien Zugriff mit größeren Schritten zwischen den Feldern, bei denen AoS- oder AoSoA-Mitarbeiter häufig besser abschneiden. Aber das ist eine Standardmethode, um das Polstern und Verpacken von Dingen so eng wie möglich zu machen, ohne mit der Ausrichtung von allem zu schrauben.

ChrisF
quelle
2
Ich würde argumentieren, dass ein Mittel zur expliziten Spezifizierung des Strukturlayouts die Portabilität massiv verbessern würde . Während einige Layouts auf einigen Computern zu sehr effizientem Code und auf anderen Computern zu sehr ineffizientem Code führen würden, würde der Code auf allen Computern funktionieren und zumindest auf einigen Computern effizient sein. Im Gegensatz dazu besteht die einzige Möglichkeit, Code auf allen Computern zum Laufen zu bringen, darin, ihn entweder auf allen Computern ineffizient zu machen oder eine Reihe von Makros und bedingten Kompilierungen zu verwenden, um ein schnelles, nicht portierbares Produkt zu kombinieren Programm und ein langsames tragbares Programm in derselben Quelle.
Supercat
Konzeptionell ja, wenn wir alles bis auf Bit- und Bytedarstellungen, Ausrichtungsanforderungen, Endianness usw. spezifizieren und eine Funktion haben könnten, die eine solche explizite Steuerung in C ermöglicht, während sie optional von der zugrunde liegenden Architektur getrennt wird ... Aber ich habe gerade darüber gesprochen ATM - Derzeit ist es die portabelste Lösung für einen Serialisierer, ihn so zu schreiben, dass er nicht von der exakten Darstellung von Bits und Bytes und der Ausrichtung von Datentypen abhängt. Leider fehlen uns die Mittel ATM, um sonst effektiv (in C) zu tun.
5

Nicht alle Architekturen sind gleich. Aktivieren Sie einfach die 32-Bit-Option auf einem Modul und sehen Sie, was passiert, wenn Sie denselben Quellcode und denselben Compiler verwenden. Die Bytereihenfolge ist eine weitere bekannte Einschränkung. Wenn Sie die Gleitkommadarstellung verwenden, werden die Probleme noch schlimmer. Das Senden von Binärdaten mit Packing ist nicht portierbar. Um es so zu standardisieren, dass es praktisch nutzbar ist, müssen Sie die C-Sprachspezifikation neu definieren.

Obwohl üblich, ist die Verwendung von Pack zum Senden von Binärdaten eine schlechte Idee, wenn Sie Sicherheit, Portabilität oder Langlebigkeit der Daten wünschen. Wie oft lesen Sie einen Binär-Blob aus einer Quelle in Ihr Programm ein? Wie oft überprüfen Sie, ob alle Werte korrekt sind, die ein Hacker oder eine Programmänderung nicht auf die Daten "bekommen" hat? Wenn Sie eine Prüfroutine programmiert haben, können Sie auch Import- und Exportroutinen verwenden.

mattnz
quelle
0

Eine sehr häufige Alternative ist "Named Padding":

struct s {
  short s1;
  char  c2;
  char  reserved; // Padding
};

Dies setzt voraus, dass die Struktur nicht auf 8 Bytes aufgefüllt wird.

MSalters
quelle