Deklaration von Variablen in Schleifen



  • Hello!
    Well,

    int numberA;
    // Version A
    for ( int i = 0; i < 100; i++ )
    {
    	 numberA = i;
    }
    // Version B
    for ( int i = 0; i < 100; i++ )
    {
    	 int numberB = i;
    }
    

    zwei verschieden numbers in zwei for loops.
    Hat der Prozessor version B mehr zu tun in zweiter Loop oder macht Compiler optimization.
    Thanx.


  • Mod

    Das ist völlig egal. Da braucht nicht einmal etwas optimiert zu werden, eine Variablendefinition macht schließlich nichts auf Prozessorebene, das ist reine Programmlogik. Nimm Variante B, die ist leserlicher und bringt besser zum Ausdruck, was du meinst. Es sind sogar Fälle denkbar, in denen Variante B schneller ist (falls der Compiler sehr dumm ist).



  • Ist es nicht so, dass Version B nur innerhalb der Schleife gültig ist, während bei Version A der letzte Wert auch außerhalb der Schleife zur Verfügung steht ?



  • sure. but that's not the worums mir geht.



  • Aso SeppJ meint, dass es vom Compiler abhängig ist, ob

    - die Vaiable bei jedem Durchlauf neu allokiert und freigegeben,
    - oder lediglich einmal allokiert und dann erst beim Verlassen der Schleife freigegeben wird.


  • Mod

    Pyro Phoenix schrieb:

    Aso SeppJ meint, dass es vom Compiler abhängig ist, ob

    - die Vaiable bei jedem Durchlauf neu allokiert und freigegeben,
    - oder lediglich einmal allokiert und dann erst beim Verlassen der Schleife freigegeben wird.

    Nein. Da wird überhaupt nix allokiert oder freigegeben. Das ist wie zu fragen, was die Kosten von "struct" sind. Da gibt es einfach nichts zu tun.*

    Ein sehr, sehr dummer Compiler (das ist eigentlich eher hypothetisch, so dumm gibt's in Wirklichkeit bestimmt nicht) könnte bei Variante A anderen Code erzeugen als bei Variante B, da er davon ausgehen könnte, dass der Wert später nochmal benutzt wird (bei B ist dies ausgeschlossen). Dies könnte eventuell gewisse Optimierungen verhindern.

    *: Das gilt alles für "normale" Computer und Compiler. Es könnte natürlich jemand mit böser Absicht einen Compiler bauen, der für diese Stelle mit Absicht suboptimalen Code erzeugt. Schließlich macht der Standard keine Vorgaben diesbezüglich. Er könnte auch einen Compiler schreiben, der in ein Programm Befehle einbaut, die jede Sekunde ein Sleep machen, wenn der angemeldete Nutzer Pyro Phoenix heißt. Wäre auch erlaubt.



  • Dann sollte man wohl eher fragen, wie der Compiler die Variablen handhabt ?

    €dit: Ok hat sich geklärt.


  • Mod

    Pyro Phoenix schrieb:

    Dann sollte man wohl eher fragen, wie der Compiler die Variablen handhabt ?

    Solche lokalen Variablen sind einfach irgendwelche Registerwerte bei der Codeerzeugung. Wenn es hockommt, dann bekommen sie noch eine Adresse auf dem Stack. Das kostet höchstens einmalig am Funktionsanfang eine Versetzung des Stackpointers (wenn die Architektur dies nötig hat). Diese findet aber so oder so statt, der Unterschied ist bloß, wie weit der Pointer versetzt wird.

    Die Optimierung von der ich sprach ist, dass es einem Compiler bei Variante B leichter fallen könnte, der Variablen gar keine Speicheradresse zu geben und sie stattdessen nur in Registern zu halten. Bei Variante A könnte der Wert eventuell noch abgespeichert werden für spätere Benutzung.

    Man sollte sich von der Vorstellung lösen, dass eine Variable ein wirkliches physikalisches Objekt im Computerspeicher wäre und dass die Definition der Variablen eine Anweisung an den Compiler wäre, bestimmte Maschinenbefehle zu erzeugen. Eine Variable ist bloß Programmlogik. Dem Compiler steht ziemlich frei, wie er das Konzept einer Variablen umsetzt, solange bloß das richtige Ergebnis herauskommt.



  • Wieder was gelernt 😃


  • Mod

    Noch ein Nachtrag, da manche Leute nicht glauben, was sie nicht selbst gesehen haben:

    int main()
    {
      int i;
      for (i = 0; i < 100; ++i)
        {
          int N = i;
        }
    }
    

    vs.

    int main()
    {
      int i, N;
      for (i = 0; i < 100; ++i)
       N = i;
    }
    

    GCC 4.8 ohne Optimierungen:

    .file	"test.c"
    	.text
    	.globl	main
    	.type	main, @function
    main:
    .LFB0:
    	.cfi_startproc
    	pushq	%rbp
    	.cfi_def_cfa_offset 16
    	.cfi_offset 6, -16
    	movq	%rsp, %rbp
    	.cfi_def_cfa_register 6
    	movl	$0, -4(%rbp)
    	jmp	.L2
    .L3:
    	movl	-4(%rbp), %eax
    	movl	%eax, -8(%rbp)
    	addl	$1, -4(%rbp)
    .L2:
    	cmpl	$99, -4(%rbp)
    	jle	.L3
    	popq	%rbp
    	.cfi_def_cfa 7, 8
    	ret
    	.cfi_endproc
    .LFE0:
    	.size	main, .-main
    	.ident	"GCC: (GNU) 4.8.0 20120415 (experimental)"
    	.section	.note.GNU-stack,"",@progbits
    

    Die andere Variante ist identisch.

    Erklärung (ich kann nicht so gut Assembler, könnte fehlerhaft sein):
    Zeile 8 legt 64-Bit auf den Stack und die Adresse davon setzen wir in das Register rbp (ein Register ist eine Art Zwischenspeicher im Prozessor). Man beachte, dass dies nicht die 64-Bit von i und N sind. Dies ist irgendein Pointer für technischen Kram den ich nicht verstehe (Rücksprungadresse?).
    Zeile 11 schreibt den Wert in Register rsp an rbp (auch irgendein technischer Kram, rsp ist der Stackpointer)
    Zeile 13 weist der Position rbp-4 den Wert 0 zu. Dies identifizieren wir mit dem i = 0, das i hat also die Adresse rbp-4.
    Zeile 14 springt zu Zeile 19. Dies ist ein Teil der for-schleife, wir springen zu dem Code der die Abbruchbedingung prüft und überspringen dazu erst einmal den Schleifenkörper.
    Zeile 20 Prüft, ob rbp-4 größer 99 ist, falls nein wird in Zeile 21 zu Zeile 15 gesprungen. Dies ist das i<100 der for-Schleife
    Zeile 16 schiebt den Wert von rbp-4 in das Register eax.
    Zeile 17 schreibt den Wert von eax an die Stelle rdp-8. Zeile 16 und 17 zusammen sind das N = i und wir identifizieren die Speicherstelle rbp-8 mit dem N.
    Zeile 18 ist das ++i.
    Zeile 22 popt den Wert vom Stack, der am Anfang draufgelegt wurde
    Zeile 24 kehrt zum Aufrufer zurück (wieder technischer Kram, woher der Computer weiß, wo das ist).

    Wie man sieht, gab es nirgendwo eine Assembleranweisung für int i noch int N .



  • SeppJ schrieb:

    Dies ist irgendein Pointer für technischen Kram den ich nicht verstehe (Rücksprungadresse?).

    In diesem Fall fungiert das Register RBP als Bezugsadresse für lokale Variablen ( RBP-X bzw -X[RBP] ) und für Argumente ( RBP+X bzw. X[RBP] ) (auch bei 64-Bit-Systemen mit Fastcall-Konvention können u. U. Argumente auf dem Stack landen).

    Zuerst wird RBP gesichert, da es unverändert an den Caller (hier das Betriebssystem, vielleicht auch der Programm-Startcode) zurückgegeben werden muss, da es dort ja auch als Bezugsadresse für Variablen dient. Dann bekommt RBP den aktuellen Wert des Stackpointers (RSP) zugewiesen. Da weiterer Stack nicht benötigt wird, wird RSP nicht angepasst, d.h. nicht dekrementiert. Ich könnte mir vorstellen, dass hier bei maximaler Optimierung auch das wegfällt, und RSP als Bezugsadresse verwendet wird. Bei GCC habe ich zumindest schon gesehen, dass RSP als Bezugsadresse verwendet wird.

    viele grüße
    ralph


  • Mod

    Danke für die Aufklärung, wie das mit dem Funktionsaufruf genau geht. Optimierung ist hier natürlich lustig, da dann am Ende gar nichts mehr da ist. Bei O1 macht der Code immerhin noch 100 Mal nichts (wobei von 100 bis 0 gezählt wird, anscheinend ist das effizienter 🙂 ):

    movl	$100, %eax
    .L3:
    	subl	$1, %eax	
    	jne	.L3	
    	rep
    	ret
    

    Ab O2 passiert dann gar nichts mehr:

    rep
    	ret
    

    (Wobei ich mich frage, was das rep da noch soll. Soweit ich weiß ist das in dieser Form (ohne Wert) eine Nichts-Tu-Anweisung wie nop, aber ich weiß nicht, was die hier soll. Kann eine Funktion nicht leer sein? Ist bestimmt auch wieder technisch bedingt)



  • Die Generierung von

    rep
    ret
    

    ist wohl eine Empfehlung von AMD, um ein Performanceproblem bei Sprüngen zu umgehen. Das trifft auch nur auf AMD Prozessoren zu. Da hat AMD wohl mal Mist gebaut und daher dieser "Hack".



  • rkhb schrieb:

    Bei GCC habe ich zumindest schon gesehen, dass RSP als Bezugsadresse verwendet wird.

    Auf x86-64 lässt GCC den Framepointer grundsätzlich weg (-fomit-frame-pointer), nur -O0 ist hier eine Ausnahme. Damit steht dann mit rbp auch ein weiteres General-Purpose-Register zur Verfügung.



  • SeppJ schrieb:

    Wie man sieht, gab es nirgendwo eine Assembleranweisung für int i noch int N .

    Normalerweise gibt es die aber in der Form einer Addition auf den Stackpointer. Das wird hier nicht gebraucht, wohl weil du keine weiteren Funktionen aufrufst (?) Wenn ich dasselbe Programm bei mir ausprobiere, bekomme ich folgenden Code:

    _main:
    LFB2:
            pushl   %ebp
    LCFI0:
            movl    %esp, %ebp
    LCFI1:
            subl    $24, %esp ; <--- Allokation hier
    LCFI2:
            movl    $0, -16(%ebp) ; int i = 0
            jmp     L2
    L3:
            movl    -16(%ebp), %eax ; 
            movl    %eax, -12(%ebp) ; N = i
            leal    -16(%ebp), %eax ; 
            addl    $1, (%eax)      ; i++
    L2:
            cmpl    $99, -16(%ebp) ; Abbruchbedingung
            jle     L3
            movl    $0, %eax
            leave
            ret
    

    Der Punkt ist aber, dass es so eine Allokation nur einmal pro Funktion gibt, das also gleichzeitig für alle Variablen Speicher bereitgestellt wird. Ob man in der Schleife oder außerhalb deklariert, ist also egal (jedenfalls in C, in C++ wegen der Konstruktoren/Destruktoren logischerweise nicht.)

    Das ist aber auch eine Designentscheidung. Es ist nicht unbedingt immer sinnvoll, alle Variablen aus allen Zweigen, die möglicherweise überhaupt nicht alle durchlaufen werden, zu erzeugen.



  • Bashar schrieb:

    Normalerweise gibt es die aber in der Form einer Addition auf den Stackpointer. Das wird hier nicht gebraucht, wohl weil du keine weiteren Funktionen aufrufst (?)

    Jein. Tatsächlich ist das nur für Daten möglich, die nicht über Funktionsaufrufe hinweg erhalten bleiben müssen. Eigentlicher Grund dafür ist die sogenannte "Red Zone" im AMD64-SysV-ABI (siehe 3.3.2 in der ABI-Spec). Das sind 128 Bytes unterhalb von %rsp, die ohne explizite Stackallokation frei benutzt werden können und bei denen garantiert ist, dass sie durch Signal- oder Interrupt-Handler nicht zerstört werden. Logischerweise können Blattfunktionen dann sogar ausschließlich mit der Red Zone arbeiten, wie man in Sepps Listing sehen konnte.

    Dein Listing ist hingegen i386-Code, bei dem sowas nicht vorgesehen ist.

    Ob man in der Schleife oder außerhalb deklariert, ist also egal

    Das war ja der Punkt. 😉


Anmelden zum Antworten