Große Unterschiede in der GCC-Codegenerierung beim Kompilieren als C++ vs. C

Lesezeit: 5 Minuten

Ich habe ein wenig mit der x86-64-Assemblierung herumgespielt, um mehr über die verschiedenen verfügbaren SIMD-Erweiterungen (MMX, SSE, AVX) zu erfahren.

Um zu sehen, wie verschiedene C- oder C++-Konstrukte von GCC in Maschinencode übersetzt werden, habe ich verwendet Compiler-Explorer das ist ein hervorragendes Werkzeug.

Während einer meiner „Spielsitzungen“ wollte ich sehen, wie GCC eine einfache Laufzeitinitialisierung eines Integer-Arrays optimieren kann. In diesem Fall habe ich versucht, die Zahlen 0 bis 2047 in ein Array von 2048 vorzeichenlosen Ganzzahlen zu schreiben.

Der Code sieht wie folgt aus:

unsigned int buffer[2048];

void setup()
{
  for (unsigned int i = 0; i < 2048; ++i)
  {
    buffer[i] = i;
  }
}

Wenn ich Optimierungen und AVX-512-Anweisungen aktiviere -O3 -mavx512f -mtune=intel GCC 6.3 generiert wirklich cleveren Code 🙂

setup():
        mov     eax, OFFSET FLAT:buffer
        mov     edx, OFFSET FLAT:buffer+8192
        vmovdqa64       zmm0, ZMMWORD PTR .LC0[rip]
        vmovdqa64       zmm1, ZMMWORD PTR .LC1[rip]
.L2:
        vmovdqa64       ZMMWORD PTR [rax], zmm0
        add     rax, 64
        cmp     rdx, rax
        vpaddd  zmm0, zmm0, zmm1
        jne     .L2
        ret
buffer:
        .zero   8192
.LC0:
        .long   0
        .long   1
        .long   2
        .long   3
        .long   4
        .long   5
        .long   6
        .long   7
        .long   8
        .long   9
        .long   10
        .long   11
        .long   12
        .long   13
        .long   14
        .long   15
.LC1:
        .long   16
        .long   16
        .long   16
        .long   16
        .long   16
        .long   16
        .long   16
        .long   16
        .long   16
        .long   16
        .long   16
        .long   16
        .long   16
        .long   16
        .long   16
        .long   16

Als ich jedoch testete, was generiert würde, wenn derselbe Code mit dem GCC-C-Compiler kompiliert würde, indem die Flags hinzugefügt würden -x c Ich war wirklich überrascht.

Ich habe ähnliche, wenn nicht identische Ergebnisse erwartet, aber der C-Compiler scheint zu generieren viel komplizierterer und vermutlich auch viel langsamerer Maschinencode. Die resultierende Baugruppe ist zu groß, um sie hier vollständig einzufügen, aber sie kann unter godbolt.org angezeigt werden, indem Sie folgen Dies Verknüpfung.

Ein Ausschnitt des generierten Codes, Zeilen 58 bis 83, ist unten zu sehen:

.L2:
        vpbroadcastd    zmm0, r8d
        lea     rsi, buffer[0+rcx*4]
        vmovdqa64       zmm1, ZMMWORD PTR .LC1[rip]
        vpaddd  zmm0, zmm0, ZMMWORD PTR .LC0[rip]
        xor     ecx, ecx
.L4:
        add     ecx, 1
        add     rsi, 64
        vmovdqa64       ZMMWORD PTR [rsi-64], zmm0
        cmp     ecx, edi
        vpaddd  zmm0, zmm0, zmm1
        jb      .L4
        sub     edx, r10d
        cmp     r9d, r10d
        lea     eax, [r8+r10]
        je      .L1
        mov     ecx, eax
        cmp     edx, 1
        mov     DWORD PTR buffer[0+rcx*4], eax
        lea     ecx, [rax+1]
        je      .L1
        mov     esi, ecx
        cmp     edx, 2
        mov     DWORD PTR buffer[0+rsi*4], ecx
        lea     ecx, [rax+2]

Wie Sie sehen können, hat dieser Code viele komplizierte Bewegungen und Sprünge und fühlt sich im Allgemeinen wie eine sehr komplexe Art an, eine einfache Array-Initialisierung durchzuführen.

Warum gibt es so einen großen Unterschied im generierten Code?

Ist der GCC C++-Compiler im Vergleich zum C-Compiler allgemein besser darin, Code zu optimieren, der sowohl in C als auch in C++ gültig ist?

  • Zusätzlicher Datenpunkt: using static unsigned int buffer[2048]; macht den C-Code auch ähnlich. Sie müssen die tatsächlich verwenden buffer damit es aber nicht ganz wegfällt. Sieht so aus, als ob es sich um ein Ausrichtungsproblem handelt, der zusätzliche Code dient dazu, eine Fehlausrichtung zu behandeln.

    – Narr

    22. Dezember 2016 um 23:19 Uhr


  • @Olaf vielleicht könnten Sie uns den Unterschied zwischen der Semantik in C und C++ für diesen Codeabschnitt erläutern

    – MM

    22. Dezember 2016 um 23:23 Uhr

  • @Jester Pro-Tipp für Godbolt, Putten void g(void *); g(buffer); verhindert, dass der Puffer herausoptimiert wird

    – MM

    22. Dezember 2016 um 23:24 Uhr

  • @Olaf Warum sollte es nicht? Wenn Sie einen bestimmten Einblick haben, wie und warum gcc in diesem Fall tut, was es tut, fügen Sie eine Antwort hinzu, da es im Grunde das ist, was OP fragt.

    – Nr

    22. Dezember 2016 um 23:26 Uhr


  • Putten unsigned int buffer[2048] = { 0 }; generiert auch den einfacheren Code. Vielleicht ist Olaf tatsächlich auf etwas in C unsigned int buffer[2048] ist ein vorläufige Definition, etwas, das C++ nicht hat. Dies wirkt sich nicht wirklich auf das beobachtbare Verhalten des Programms aus, hat aber offensichtlich einen gewissen Einfluss auf die GCC-Codegenerierung.

    – MM

    22. Dezember 2016 um 23:26 Uhr

  • Beachten Sie, dass Sie zum Kompilieren der C-Code-Version mit gcc die -fno-common Compiler-Flag, oder kommentieren Sie die buffer variabel mit __attribute__((aligned(64))) und es wird ähnlicher Code wie die C++-Version generiert.

    – Nr

    22. Dezember 2016 um 23:59 Uhr


  • @MM eigentlich ist dies eine Deklaration, und sie hat eine externe Verknüpfung in C, aber eine interne in C++. Eine interne Verknüpfung bedeutet, dass sie in diesem Modul definiert werden muss, und der Compiler wird dies tun. Für C kann es durchaus in einem anderen Modul definiert sein, sodass der Compiler möglicherweise damit arbeiten muss. Natürlich wird das Hinzufügen eines Initialisierers es in eine Definition umwandeln, und Sie können auch nicht mehr davon in C haben, damit der Compiler den optimierten Code generieren kann.

    – Narr

    23. Dezember 2016 um 0:04 Uhr

  • @Jester Die vorläufige Definition darf nur innerhalb derselben Übersetzungseinheit neu definiert werden. Eine vorläufige Definition ist unter keinen Umständen eine Definition, nicht „nur eine Erklärung“. Ihr Punkt ist in Standard C immer noch falsch.

    – MM

    23. Dezember 2016 um 0:36 Uhr


  • Je länger ich es betrachte, desto mehr scheint es, als hättest du recht. Absichtlich oder nicht, aber gcc erlaubt es, dass die vorläufige Definition von einem Initialisierer aus einem anderen Modul überschrieben wird, und das zieht seine eigene Ausrichtung mit sich. Da es sich um ein undefiniertes Verhalten handelt, kann sich dies mit späteren Versionen ändern, aber für die fragliche Version scheint dies der Fall zu sein.

    – Narr

    23. Dezember 2016 um 1:13 Uhr

  • Das Zulassen von Definitionen ohne Initialisierung in mehreren Übersetzungseinheiten ist eine Unix-Erweiterung (was ein seltsames Konzept ist, da Unix fast 20 Jahre lang C hatte, bevor ANSI auf den Markt kam), die (glaube ich immer noch) in GNU-Programmen verwendet wird. Ich halte es für höchst unwahrscheinlich, dass GCC die Unterstützung dafür tatsächlich einstellen würde, selbst wenn der ANSI-Standard dies technisch zulässt.

    – Jonathan Cast

    23. Dezember 2016 um 3:28 Uhr

1386810cookie-checkGroße Unterschiede in der GCC-Codegenerierung beim Kompilieren als C++ vs. C

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

Privacy policy