Gibt es Leistungsprobleme bei der Verwendung von Pragma Pack(1)?

Lesezeit: 8 Minuten

Benutzer-Avatar von Nicolas
Nicolas

Unsere Header verwenden #pragma pack(1) um die meisten unserer Strukturen (wird für Netz- und Datei-I/O verwendet). Ich verstehe, dass dadurch die Ausrichtung von Strukturen vom Standardwert von 8 Byte auf eine Ausrichtung von 1 Byte geändert wird.

Unter der Annahme, dass alles unter 32-Bit-Linux (vielleicht auch unter Windows) läuft, gibt es dann irgendwelche Leistungseinbußen, die sich aus dieser Paketausrichtung ergeben?

Ich mache mir keine Sorgen um die Portabilität von Bibliotheken, sondern eher um die Kompatibilität von Datei- und Netzwerk-E/A mit verschiedenen #pragma-Paketen und um Leistungsprobleme.

Der Speicherzugriff ist am schnellsten, wenn er an wortausgerichteten Speicheradressen erfolgen kann. Das einfachste Beispiel ist die folgende Struktur (die auch @Didier verwendet hat):

struct sample {
   char a;
   int b;
};

Standardmäßig fügt GCC Auffüllungen ein, sodass a am Offset 0 und b am Offset 4 liegt (wortausgerichtet). Ohne Auffüllung ist b nicht wortausgerichtet und der Zugriff ist langsamer.

Wie viel langsamer?

  • Für 32-Bit x86, laut Softwareentwicklerhandbuch für Intel 64- und IA32-Architekturen:

    Der Prozessor benötigt zwei Speicherzugriffe, um einen nicht ausgerichteten Speicherzugriff durchzuführen; ausgerichtete Zugriffe erfordern nur einen Speicherzugriff. Ein Wort- oder Doppelwortoperand, der eine 4-Byte-Grenze überschreitet, oder ein Quadword-Operand, der eine 8-Byte-Grenze überschreitet, gilt als nicht ausgerichtet und erfordert zwei separate Speicherbuszyklen für den Zugriff.

    Wie bei den meisten Leistungsfragen müssen Sie Ihre Anwendung einem Benchmarking unterziehen, um zu sehen, wie problematisch dies in der Praxis ist.

  • Entsprechend Wikipediax86-Erweiterungen wie SSE2 erfordern Wortausrichtung.
  • Viele andere Architekturen erfordern eine Wortausrichtung (und erzeugen SIGBUS-Fehler, wenn Datenstrukturen nicht wortausgerichtet sind).

Bezüglich der Portabilität: Ich gehe davon aus, dass Sie es verwenden #pragma pack(1) So können Sie Strukturen über die Leitung und von und zur Festplatte senden, ohne sich Gedanken darüber machen zu müssen, dass verschiedene Compiler oder Plattformen Strukturen unterschiedlich verpacken. Dies ist zwar gültig, es sind jedoch einige Punkte zu beachten:

  • Dies trägt nicht dazu bei, Big-Endian- und Little-Endian-Probleme zu lösen. Sie können diese Probleme lösen, indem Sie die anrufen htons Familie von Funktionen für alle Ints, Unsigned usw. in Ihren Strukturen.
  • Meiner Erfahrung nach macht die Arbeit mit gepackten, serialisierbaren Strukturen im Anwendungscode nicht viel Spaß. Es ist sehr schwierig, sie zu ändern und zu erweitern, ohne die Abwärtskompatibilität zu beeinträchtigen, und wie bereits erwähnt, gibt es Leistungseinbußen. Erwägen Sie, den Inhalt Ihrer gepackten, serialisierbaren Strukturen zur Verarbeitung in entsprechende, nicht gepackte, erweiterbare Strukturen zu übertragen, oder erwägen Sie die Verwendung einer vollwertigen Serialisierungsbibliothek wie z Protokollpuffer (was hat C-Bindungen).

  • +1 für eine hervorragende Antwort und für den Hinweis, dass es tatsächlich einige Nicht-x86-Architekturen gibt erfordern richtige Ausrichtung für bestimmte Datentypen.

    – Paul R

    17. Okt. 2011 um 12:47

  • Endinaness wird nicht wirklich behandelt, aber es ist „OK“, da unser gesamtes Backoffice Linux-gesteuert ist. Ich werde tatsächlich einen Benchmark durchführen und vielleicht hier darüber berichten. Danke für die Antwort.

    – Nicolas

    17. Okt. 2011 um 13:22

Ja. Gibt es auf jeden Fall.

Wenn Sie beispielsweise eine Struktur definieren:

struct dumb {
    char c;
    int  i;
};

Wenn Sie dann auf das Mitglied i zugreifen, wird die CPU verlangsamt, da auf den 32-Bit-Wert i nicht auf native, ausgerichtete Weise zugegriffen werden kann. Stellen Sie sich zur Vereinfachung vor, dass die CPU 3 Bytes aus dem Speicher und dann 1 weiteres Byte vom nächsten Speicherort abrufen muss, um den Wert aus dem Speicher in die CPU-Register zu übertragen.

Benutzeravatar von Alok Save
Alok Save

Wenn Sie eine Struktur deklarieren, fügen die meisten Compiler Füllbytes zwischen den Mitgliedern ein, um sicherzustellen, dass sie an den richtigen Adressen im Speicher ausgerichtet sind (normalerweise sind die Füllbytes ein Vielfaches der Typgröße). Dies ermöglicht dem Compiler einen optimierten Zugriff beim Zugriff auf diese Mitglieder.

#pragma pack(1) weist den Compiler an, Strukturelemente mit einer bestimmten Ausrichtung zu packen. Der 1 Hier wird der Compiler angewiesen, keine Auffüllungen zwischen Mitgliedern einzufügen.

So Ja, es gibt eindeutig eine Leistungseinbußeda Sie den Compiler dazu zwingen, etwas zu tun, das über das hinausgeht, was er normalerweise zur Leistungsoptimierung tun würde. Außerdem Einige Plattformen verlangen, dass die Objekte an bestimmten Grenzen ausgerichtet werden, und die Verwendung nicht ausgerichteter Strukturen kann zu Segmentierungsfehlern führen.

Im Idealfall ist es am besten, eine Änderung der Standardregeln für die natürliche Ausrichtung zu vermeiden. Lässt sich die „Pragma Pack“-Direktive jedoch überhaupt nicht vermeiden (wie in Ihrem Fall), muss nach der Definition der Strukturen, die eine dichte Packung erfordern, das ursprüngliche Packungsschema wiederhergestellt werden.

Zum Beispiel:

//push current alignment rules to internal stack and force 1-byte alignment boundary
#pragma pack(push,1)  

/*   definition of structures that require tight packing go in here   */

//restore original alignment rules from stack    
#pragma pack(pop)

  • Oder besser: Verwenden Sie die native Version von gcc aligned Attribut um nur die aktuelle Struktur zu markieren.

    – Blagovest Büyukliew

    17. Okt. 2011 um 12:37


Benutzeravatar von Blagovest Buyukliev
Blagovest Büyukliew

Dies hängt von der zugrunde liegenden Architektur und der Art und Weise ab, wie sie mit nicht ausgerichteten Adressen umgeht.

x86 verarbeitet nicht ausgerichtete Adressen ordnungsgemäß, wenn auch mit Leistungseinbußen, während andere Architekturen wie ARM möglicherweise einen Ausrichtungsfehler hervorrufen (SIGBUS) oder „runden“ Sie sogar die falsch ausgerichtete Adresse auf die nächste Grenze. In diesem Fall wird Ihr Code auf abscheuliche Weise fehlschlagen.

Fazit: Packen Sie es nur, wenn Sie sicher sind, dass die zugrunde liegende Architektur nicht ausgerichtete Adressen verarbeiten kann und wenn die Kosten für Netzwerk-E/A höher sind als die Verarbeitungskosten.

Gibt es Leistungsprobleme bei der Verwendung von Pragma Pack(1)?

Absolut. Im Januar 2020 veröffentlichte Raymond Chen von Microsoft konkrete Beispiele für die Verwendung #pragma pack(1) kann aufgeblähte ausführbare Dateien erzeugen, die sehr viel mehr Anweisungen benötigen, um Operationen an gepackten Strukturen auszuführen. Insbesondere auf Nicht-x86-Hardware, die falsch ausgerichtete Zugriffe in der Hardware nicht direkt unterstützt.

Jeder, der schreibt #pragma pack(1) Sie können genauso gut ein Schild auf der Stirn tragen, auf dem steht: „Ich hasse RISC.“

Wenn Sie verwenden #pragma pack(1)ändert dies die Standardstrukturpackung in Bytepackung und entfernt alle Füllbytes, die normalerweise eingefügt werden, um die Ausrichtung beizubehalten.

Die Möglichkeit, dass eine P-Struktur falsch ausgerichtet sein könnte, hat erhebliche Konsequenzen für die Codegenerierung, da alle Zugriffe auf Mitglieder den Fall behandeln müssen, dass die Adresse nicht richtig ausgerichtet ist.

void UpdateS(S* s)
{
 s->total = s->a + s->b;
}

void UpdateP(P* p)
{
 p->total = p->a + p->b;
}

Obwohl die Strukturen S und P genau das gleiche Layout haben, ist die Codegenerierung aufgrund der Ausrichtung unterschiedlich.

UpdateS                       UpdateP
Intel Itanium

adds  r31 = r32, 4            adds  r31 = r32, 4
adds  r30 = r32  8 ;;         adds  r30 = r32  8 ;;
ld4   r31 = [r31]             ld1   r29 = [r31], 1
ld4   r30 = [r30] ;;          ld1   r28 = [r30], 1 ;;
                              ld1   r27 = [r31], 1
                              ld1   r26 = [r30], 1 ;;
                              dep   r29 = r27, r29, 8, 8
                              dep   r28 = r26, r28, 8, 8
                              ld1   r25 = [r31], 1
                              ld1   r24 = [r30], 1 ;;
                              dep   r29 = r25, r29, 16, 8
                              dep   r28 = r24, r28, 16, 8
                              ld1   r27 = [r31]
                              ld1   r26 = [r30] ;;
                              dep   r29 = r27, r29, 24, 8
                              dep   r28 = r26, r28, 24, 8 ;;
add   r31 = r30, r31 ;;       add   r31 = r28, r29 ;;
st4   [r32] = r31             st1   [r32] = r31
                              adds  r30 = r32, 1
                              adds  r29 = r32, 2 
                              extr  r28 = r31, 8, 8
                              extr  r27 = r31, 16, 8 ;;
                              st1   [r30] = r28
                              st1   [r29] = r27, 1
                              extr  r26 = r31, 24, 8 ;;
                              st1   [r29] = r26
br.ret.sptk.many rp           br.ret.sptk.many.rp

...
[examples from other hardware]
...

Beachten Sie, dass bei einigen RISC-Prozessoren die Codegrößenexplosion erheblich ist. Dies kann wiederum Auswirkungen auf Inlining-Entscheidungen haben.

Moral der Geschichte: Bewerben Sie sich nicht #pragma pack(1) an Bauwerken, es sei denn, dies ist unbedingt erforderlich. Es bläht Ihren Code auf und verhindert Optimierungen.

#pragma pack(1) und seine Variationen sind auch auf subtile Weise gefährlich – sogar auf x86-Systemen, wo sie angeblich „funktionieren“

Ioans Benutzeravatar
Ioan

Technisch gesehen würde es zwar die Leistung beeinträchtigen, aber nur im Hinblick auf die interne Verarbeitung. Wenn Sie die Strukturen für Netzwerk-/Datei-E/A gepackt benötigen, besteht ein Gleichgewicht zwischen der gepackten Anforderung und der rein internen Verarbeitung. Mit interner Verarbeitung meine ich die Arbeit, die Sie an den Daten zwischen den E/A durchführen. Wenn Sie nur sehr wenig verarbeiten, verlieren Sie nicht viel an Leistung. Andernfalls möchten Sie möglicherweise eine interne Verarbeitung für ordnungsgemäß ausgerichtete Strukturen durchführen und die Ergebnisse nur bei der Durchführung von E/A-Vorgängen „packen“. Oder Sie könnten dazu wechseln, nur standardmäßig ausgerichtete Strukturen zu verwenden, müssen jedoch sicherstellen, dass alle diese auf die gleiche Weise ausrichten (Netzwerk- und Datei-Clients).

Ole Dittmanns Benutzeravatar
Ole Dittmann

Es gibt bestimmte Maschinencodeanweisungen, die mit 32 Bit oder 64 Bit (oder sogar mehr) arbeiten, aber erwarten, dass die Daten an Speicheradressen ausgerichtet sind. Ist dies nicht der Fall, müssen sie mehr als einen Lese-/Schreibzyklus im Speicher ausführen, um ihre Aufgabe auszuführen. Wie stark dieser Leistungseinbruch ausfällt, hängt stark davon ab, was Sie mit den Daten machen. Wenn Sie große Arrays von Strukturen erstellen und umfangreiche Berechnungen daran durchführen, kann es zu großen Arrays kommen. Wenn Sie Daten jedoch nur einmal speichern, um sie zu einem späteren Zeitpunkt zurückzulesen und sie ohnehin in einen Byte-Stream umzuwandeln, fallen sie möglicherweise kaum auf.

1451770cookie-checkGibt es Leistungsprobleme bei der Verwendung von Pragma Pack(1)?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy