2016-09-13 1 views
9

Diese Frage ist teilweise eine Nachfolgefrage zu GCC 5.1 Loop unrolling.Loop Abrollverhalten in GCC

Nach dem GCC documentation, und wie in meiner Antwort auf die oben gestellte Frage angegeben, Flaggen wie -funroll-loops wiederum auf „vollständige Schleife Peeling (das heißt eine vollständige Entfernung von Schleifen mit einer kleinen konstanten Anzahl von Iterationen)“. Wenn ein solches Flag aktiviert ist, kann der Compiler daher eine Schleife abwickeln, wenn festgestellt wird, dass dies die Ausführung eines bestimmten Codeabschnitts optimieren würde.

Trotzdem bemerkte ich in einem meiner Projekte, dass GCC Loops manchmal ausrollen würde, obwohl die relevanten Flags nicht aktiviert waren. Betrachten wir zum Beispiel die folgende einfache Stück Code:

int main(int argc, char **argv) 
{ 
    int k = 0; 
    for(k = 0; k < 5; ++k) 
    { 
    volatile int temp = k; 
    } 
} 

Wenn mit -O1 kompilieren, wird die Schleife entrollt und die folgende Assembler-Code ist mit jedem modernen Version von GCC generiert:

main: 
     movl $0, -4(%rsp) 
     movl $1, -4(%rsp) 
     movl $2, -4(%rsp) 
     movl $3, -4(%rsp) 
     movl $4, -4(%rsp) 
     movl $0, %eax 
     ret 

Selbst wenn Kompilieren mit dem zusätzlichen -fno-unroll-loops -fno-peel-loops, um sicherzustellen, dass die Flags deaktiviert deaktiviert sind, führt GCC unerwartet weiterhin Loop Enrolling in dem oben beschriebenen Beispiel.

Diese Beobachtung führt mich zu den folgenden eng verwandten Fragen. Warum führt GCC ein Loop-Abrolling durch, obwohl die diesem Verhalten entsprechenden Flags deaktiviert sind? Wird das Abrollen auch von anderen Flags gesteuert, die dazu führen können, dass der Compiler in einigen Fällen eine Schleife abrollt, obwohl -funroll-loops deaktiviert ist? Gibt es eine Möglichkeit, das Loop-Enrolling in GCC vollständig zu deaktivieren (ein Teil der Kompilierung mit -O0)?

Interessanterweise hat der Compiler Clang das erwartete Verhalten hier, und scheint nur Abrollung durchzuführen, wenn -funroll-loops aktiviert ist, und nicht in anderen Fällen.

Vielen Dank im Voraus, alle zusätzlichen Erkenntnisse zu diesem Thema würden sehr geschätzt!

+0

Herzlichen Glückwunsch. Sie haben festgestellt, dass sich verschiedene Compiler im Verhalten unterscheiden und dass die Flags, die Sie ihnen übergeben, nicht immer das bedeuten, was Sie vielleicht meinen. Willkommen in der realen Welt. –

+0

Funktioniert es die Funktionalität Ihres Programms? – Serge

+0

Nein, es bricht nicht die Funktionalität. Es ist eher eine Frage von allgemeinem Interesse, wie GCC Loop Enrolling durchführt und wie dieses Verhalten eingestellt wird. – Pyves

Antwort

7

Warum führt GCC einen Loop-Abrollvorgang durch, obwohl die diesem Verhalten entsprechenden Flags deaktiviert sind?

Denken Sie aus einer pragmatischen Sicht: Was wollen Sie, wenn Sie solche Flag an den Compiler übergeben? Kein C++ - Entwickler wird GCC bitten, Schleifen aufzurollen oder nicht abzurollen, nur um Schleifen zu haben oder nicht, in Assembler-Code, gibt es ein Ziel. Das Ziel mit -fno-unroll-loops ist zum Beispiel, ein wenig Geschwindigkeit zu opfern, um die Größe Ihrer Binärdatei zu reduzieren, wenn Sie eine eingebettete Software mit begrenztem Speicher entwickeln. Auf der anderen Seite ist das Ziel mit -funrool-loops, dem Compiler zu sagen, dass Sie sich nicht um die Größe Ihrer Binärdatei kümmern, also sollte es nicht zögern, Loops auszuwickeln.

Aber das bedeutet nicht, dass der Compiler wird blind entrollen oder nicht alle Ihre Schleifen!

In Ihrem Beispiel ist der Grund einfach: Die Schleife enthält nur ein Anweisung - wenige Bytes auf allen Plattformen - und der Compiler weiß, dass diese negligeable und wird ohnehin fast die gleiche Größe wie der Assembler-Code übernehmen, die für die Schleife (sub + mov + jne auf x86-64).

Aus diesem Grund gcc 6.2, mit -O3 -fno-unroll-loops diesen Code verwandelt:

int mul(int k, int j) 
{ 
    for (int i = 0; i < 5; ++i) 
    volatile int k = j; 

    return k; 
} 

... der folgenden Assembler-Code:

mul(int, int): 
    mov DWORD PTR [rsp-0x4],esi 
    mov eax,edi 
    mov DWORD PTR [rsp-0x4],esi 
    mov DWORD PTR [rsp-0x4],esi 
    mov DWORD PTR [rsp-0x4],esi 
    mov DWORD PTR [rsp-0x4],esi 
    ret  

Es hört nicht auf Sie, weil es würde (fast , abhängig von der Architektur) ändern Sie nicht die Größe der Binärdatei, aber es ist schneller. Wenn Sie ein wenig Ihre Schleifenzähler erhöhen jedoch ...

int mul(int k, int j) 
{ 
    for (int i = 0; i < 20; ++i) 
    volatile int k = j; 

    return k; 
} 

... es folgt Ihr Tipp:

mul(int, int): 
    mov eax,edi 
    mov edx,0x14 
    nop WORD PTR [rax+rax*1+0x0] 
    sub edx,0x1 
    mov DWORD PTR [rsp-0x4],esi 
    jne 400520 <mul(int, int)+0x10> 
    repz ret 

Sie das gleiche Verhalten, wenn man sich 5 Ihre Schleifenzähler halten, aber Sie fügen etwas Code in die Schleife ein.

Zusammenfassend, denken Sie an alle diese Optimierungsflags als Hinweis für den Compiler, und von einem pragmatischen Entwickler Sicht. Es ist immer ein Kompromiss, und wenn Sie eine Software erstellen, Sie nie wollen alle oder keine Schleife entrollen fragen. Ein letztes sehr ähnliches Beispiel ist das -f(no-)inline-functions Flag. Ich kämpfe jeden Tag den Compiler inline (oder nicht!) Einige meiner Funktionen (mit dem inline Schlüsselwort und __attribute__ ((noinline)) mit GCC), und wenn ich den Assembler-Code überprüfe, sehe ich, dass dieser Klugscheißer immer noch manchmal macht, was er will, wenn ich eine Funktion einbauen möchte, die definitiv zu lang für ihren Geschmack ist. Und die meiste Zeit ist es das Richtige und ich bin glücklich!

+0

Zumindest Compiler * do * hören normalerweise auf '__attribute__ (((no) inline))' und sowas wie schnelle/strikte Mathematik. Ich kann mir nicht vorstellen, dass ein Compiler eine strikt mathematische Flagge ignoriert. – Mysticial