Seltsame Assembly von Array 0-Initialisierung

Lesezeit: 5 Minuten

Benutzer-Avatar
Johann Gell

Inspiriert von der Frage „Unterschied beim Initialisieren und Nullsetzen eines Arrays in c/c++?“ habe ich mich entschlossen, die Assemblierung eines optimierten Release-Builds für Windows Mobile Professional (ARM-Prozessor, aus dem Microsoft Optimizing Compiler) in meinem Fall tatsächlich zu untersuchen. Was ich fand, war etwas überraschend, und ich frage mich, ob jemand etwas Licht in meine diesbezüglichen Fragen bringen kann.

Diese beiden Beispiele werden untersucht:

byte a[10] = { 0 };

byte b[10];
memset(b, 0, sizeof(b));

Sie werden in derselben Funktion verwendet, daher sieht der Stack folgendermaßen aus:

[ ] // padding byte to reach DWORD boundary
[ ] // padding byte to reach DWORD boundary
[ ] // b[9] (last element of b)
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ] // b[0] = sp + 12 (stack pointer + 12 bytes)
[ ] // padding byte to reach DWORD boundary
[ ] // padding byte to reach DWORD boundary
[ ] // a[9] (last element of a)
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ] // a[0] = sp (stack pointer, at bottom)

Die generierte Assembly mit meinen Kommentaren:

; byte a[10] = { 0 };

01: mov   r3, #0        // r3 = 0
02: mov   r2, #9        // 3rd arg to memset: 9 bytes, note that sizeof(a) = 10
03: mov   r1, #0        // 2nd arg to memset: 0-initializer
04: add   r0, sp, #1    // 1st arg to memset: &a[1] = a + 1, since only 9 bytes will be set
05: strb  r3, [sp]      // a[0] = r3 = 0, sets the first element of a
06: bl    memset        // continue in memset

; byte b[10];
; memset(b, 0, sizeof(b));

07: mov   r2, #0xA      // 3rd arg to memset: 10 bytes, sizeof(b)
08: mov   r1, #0        // 2nd arg to memset: 0-initializer
09: add   r0, sp, #0xC  // 1st arg to memset: sp + 12 bytes (the 10 elements
                        // of a + 2 padding bytes for alignment) = &b[0]
10: bl    memset        // continue in memset

Jetzt verwirren mich zwei Dinge:

  1. Was ist der Sinn der Zeilen 02 und 05? Warum nicht einfach &a geben[0] und 10 Bytes zu memset?
  2. Warum werden die Füllbytes einer 0 nicht initialisiert? Ist das nur zum Auffüllen in Strukturen?

Bearbeiten: Ich war zu neugierig, um den Struct-Fall nicht zu testen:

struct Padded
{
    DWORD x;
    byte y;
};

Der Assembler zum 0-Initialisieren:

; Padded p1 = { 0 };

01: mov   r3, #0
02: str   r3, [sp]
03: mov   r3, #0
04: str   r3, [sp, #4]

; Padded p2;
; memset(&p2, 0, sizeof(p2));

05: mov   r3, #0
06: str   r3, [sp]
07: andcs r4, r0, #0xFF
08: str   r3, [sp, #4]

Hier sehen wir in Zeile 04, dass tatsächlich eine Auffüllung erfolgt, da str (im Gegensatz zu strb) wird genutzt. Recht?

  • Nun, nach dem Lesen der Kommentare unten scheint es, dass msvc beim Nullen des Speichers nicht sehr konsequent ist.

    – Matt Tischler

    26. Oktober 2009 um 23:49 Uhr

Der Grund für die Zeilen 2 und 5 liegt darin, dass Sie im Array-Initialisierer eine 0 angegeben haben. Der Compiler initialisiert alle Konstanten und füllt dann den Rest mit memset aus. Wenn Sie zwei Nullen in Ihren Initialisierer einfügen würden, würden Sie strw (Wort statt Byte) und dann memset 8 Bytes sehen.

Was das Auffüllen betrifft, wird es nur zum Ausrichten von Speicherzugriffen verwendet – die Daten sollten unter normalen Umständen nicht verwendet werden, daher ist es verschwenderisch, sie zu speichern.

Bearbeiten: Für die Aufzeichnung kann ich mich in Bezug auf die obige Strw-Annahme irren. 99 % meiner ARM-Erfahrung ist das Umkehren von Code, der von GCC/LLVM auf dem iPhone generiert wurde, sodass meine Annahme möglicherweise nicht auf MSVC übertragen wird.

Beide Bits des Codes sind fehlerfrei. Die beiden erwähnten Zeilen sind nicht schlau, aber Sie beweisen nur, dass dieser Compiler suboptimalen Code ausgibt.

Füllbytes werden normalerweise nur dann initialisiert, wenn dies die Assemblierung vereinfacht oder den Code beschleunigt. Wenn Sie z. B. eine Auffüllung zwischen zwei mit Nullen gefüllten Elementen haben, ist es oft einfacher, die Auffüllung ebenfalls mit Nullen zu füllen. Wenn Sie am Ende eine Auffüllung haben und Ihr memset() für Multibyte-Schreibvorgänge optimiert ist, kann es außerdem schneller sein, diese Auffüllung zu überschreiben.

  • Eigentlich könnte dieser Code sehr gut optimal sein. Die Art und Weise, wie Anweisungen auf ARM geleitet werden, könnte es leicht effizienter machen, strb dann abzuzweigen und zu wiederholen. Allerdings wäre der Leistungsunterschied wahrscheinlich vernachlässigbar, und Sie verwenden zusätzliche 4 Bytes, also wer weiß.

    – Serafina Brocious

    10. Februar 2009 um 8:39 Uhr

  • Unwahrscheinlich. Sie haben nicht ausgerichtete Speicherzugriffe (ein Byte und 9 Bytes – ARM hat oft einen 16-Bit-Bus. Das bedeutet Lesen/Ändern/Schreiben!). Außerdem haben Sie zusätzlichen Registerdruck: Sie brauchen auch R3.

    – MSalter

    10. Februar 2009 um 12:44 Uhr

Einige Schnelltests weisen darauf hin, dass der x86-Compiler von Microsoft eine andere Assembly generiert, wenn die Initialisierungsliste leer ist, als wenn sie eine Null enthält. Vielleicht tut ihr ARM-Compiler das auch. Was passiert, wenn Sie dies tun?

byte a[10] = { };

Hier ist die Montageliste, die ich bekommen habe (mit Optionen /EHsc /FAs /O2 auf Visual Studio 2008). Beachten Sie, dass das Einfügen einer Null in die Initialisiererliste dazu führt, dass der Compiler nicht ausgerichtete Speicherzugriffe verwendet, um das Array zu initialisieren, während die leere Initialisiererlistenversion und die memset() Version verwenden beide ausgerichtete Speicherzugriffe:

; unsigned char a[10] = { };

xor eax, eax
mov DWORD PTR _a$[esp+40], eax
mov DWORD PTR _a$[esp+44], eax
mov WORD PTR _a$[esp+48], ax

; unsigned char b[10] = { 0 };

mov BYTE PTR _b$[esp+40], al
mov DWORD PTR _b$[esp+41], eax
mov DWORD PTR _b$[esp+45], eax
mov BYTE PTR _b$[esp+49], al

; unsigned char c[10];
; memset(c, 0, sizeof(c));

mov DWORD PTR _c$[esp+40], eax
mov DWORD PTR _c$[esp+44], eax
mov WORD PTR _c$[esp+48], ax

  • woah!! warum um alles in der Welt macht es das? 😛 Zumindest würden Sie erwarten, dass die explizite 0-Initialisierung zuerst den Wert in al in alle Bytes in eax kopiert. Es ist, als wäre eine Optimierung für die explizite Initialisierung mit 0 halbfertig.

    – Matt Tischler

    26. Oktober 2009 um 23:47 Uhr

1370710cookie-checkSeltsame Assembly von Array 0-Initialisierung

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

Privacy policy