Besonderheit des xchg-Befehls


  • Mod

    Brasilian schrieb:

    Achso, wenn das die Syntax vorschreibt.

    Mich wundert der technische Hintergrund nun aber schon etwas, da es sich bei der C++ Variabeln und bei dem Register, doch nur im irgendwelche Speicherplätze handelt und diese doch gleich behandelt werden könnten, oder liege ich das falsch ? In welchem "Register" oder besser gesagt, wo befinden sich den überhaupt die C++ Variabeln, ist dies klar definiert ?

    Gruß
    Brasi

    ein register ist nicht 'irgendein' speicherplatz sondern ein spezieller speicher in der cpu. als solcher ist er nicht teil des allgemeinen speichers und hat auch keine adresse (mit anderen worten, die einzige möglichkeit, ein register zu benutzen, ist sein name - eine indirektion per pointer ist nicht möglich).

    die folgenden ausführungen sind compiler spezifisch, sie gelten so für visual C++, sinngemäss wahrscheinlich auch für borland; bei g++ gelten die ausfürungen über automatische variablen nur bedingt (und die syntax ist ohnehin anders)

    wo sich eine variable befindet (sofern der der compiler überhaupt speicher dafür reserviert - entspr. der as-if regel) hängt davon ab, wo sie definiert ist: variablen mit static storage haben eine feste adresse die vom linker/loader vergeben wird, jeder zugriff über ihren symbolischen namen darauf ist, dann im prinzip äquvivalent zu:

    type ptr ds:[addr]
    

    . im gegensatz dazu befinden sich automatische variablen auf dem stack und werden per:

    type ptr ds:[ebp+offset]
    

    angesprochen, wenn ihr symbolischer name verwendet wird. d.h. dass in allen funktionen, die inline assembler und symbolische namen für lokale variablen im inline assembler code verwenden, automatisch ein stackrahmen eingerichtet wird (omit-frame-pointer ist hier also wirkungslos, ausserdem kann die variable nicht wegoptimiert werden oder register storage haben). will man das verhindern, muss man sich die hände dreckig machen, und variablen direkt per type ptr [esp+offset] verwenden (ein #define kann helfen, das ganze leserlich zu halten). dann muss man allerdings auch sehr genau auf veränderungen von esp achten.


  • Mod

    Brasilian schrieb:

    Hallo,

    ich interessiere mich seit kurzem für die Einbettung von Assembler in C++.
    Im großen und ganzen klappt das auch ohne Probleme, doch bin ich nun auf einen Sachverhalt gestossen welchen ich nicht verstehe.

    Ich habe folgenden Code implement;

    char var1 = 'A', var2 = 'B';
      
    asm
    {
         mov  al,var1
         xchg al,var2                
         mov  var1,al
    }
    

    Das funktioniert auch ohne Probleme. Jetzt habe ich mir gedacht, dass es mittels xchg-Befehls doch auch möglich sein muss, die beiden C++-Vaariabeln direkt zu vertauschen, doch dies führt zu einem Laufzeitfehler, warum ?

    char var1 = 'A', var2 = 'B';
      
    asm
    {
         xchg var1,var2
    }
    

    jetzt zu den technischen beschränkungen. für x86-kompatible prozessoren gilt stets, dass höchtens ein operand eines befehls ein speicher-operand sein kann - das hat gründe, die in der architektur zu finden sind - als programmikerer muss man das so hinnehmen. folglich ist

    xchg var1, var2
    

    illegal und sollte theoretisch zu einem compiler fehler führen - auf jeden fall ist der resultierende maschinen-code etwas völlig anderes (und führt in diesem falle offenbar glücklicherweise zum abbruch).

    mov  al,var1
         xchg al,var2                
         mov  var1,al
    

    das ist nat. legal, allerdings ungünstig aus zwei gründen:
    a) xchg mit speicheroperanden führt stets zu einem impliziten lock, d.h. diese operation ist stets atomisch - mit den entsprechenden performanz problen (und wenn man lokale variablen vertauscht, sollte ein lock prinzipiell nicht notwendig sein).
    b) moderne prozessoren können befehle out-of-order (ab pentium pro) und/oder parallel (ab pentium) ausführen, sofern dies die semantik eines programms nicht verändert. diese codestück kann allerdings nicht umgeordnet werden, somit geht hier die chance zum optimieren verloren, besser ist (auch in hinsicht von problem a) ):

    mov al, var1
        mov cl, var2
        mov var2, al
        mov var1, cl
    

    hier können der 1. und 2. bzw. der 3. und 4. befehl in beliebiger reihenfolge oder auch parallel ausgeführt werden - das ist, im übrigen der klassische dreieckstausch.
    c) zumindest bei der netburst-architektur(P4) ist sogar ein xchg mit registern langsamer als ein dreickstausch - sofern man ein temporäres register zur verfügung hat und code-grösse nicht das problem ist, sollte man zumindest bei diesen prozessoren von xchg abstand nehmen.



  • Hallo Camper,

    erst mal vielen Dank für diese beiden so ausführlichen Beiträge. Ich würde jedoch lügen, wenn ich nach zweimaligem Lesen alles 100%tig verstanden hätte. Deswegen würde ich gerne bei dem ein oder anderen Punkt nochmals nachfragen:

    ein register ist nicht 'irgendein' speicherplatz sondern ein spezieller speicher in der cpu. als solcher ist er nicht teil des allgemeinen speichers und hat auch keine adresse (mit anderen worten, die einzige möglichkeit, ein register zu benutzen, ist sein name - eine indirektion per pointer ist nicht möglich).

    Das ein Register nur über einen Namen angesprochen werden kann und sich nicht im allgemeinen Speicher befindet sehe ich auch so, doch ist es nicht so, dass sich auch hinter jedem Registernamen nur eine Speicheradresse verbirgt. Für uns als Anwender ist es natürlich komfortabler einen Namen als eine Adresse anzusprechen.

    Assembler Code:
        mov al, var1
        mov cl, var2
        mov var2, al
        mov var1, cl
    

    hier können der 1. und 2. bzw. der 3. und 4. befehl in beliebiger reihenfolge oder auch parallel ausgeführt werden - das ist, im übrigen der klassische dreieckstausch.

    Wenn ich dich richtig verstehe ist dieser vierzeilige Code schneller als der von mir angegebene dreizeilige Code (mit xchg). Dies liegt daran, dass hier nur der Elementarbefehl mov auftaucht, wogegen der xchg-Befehl zeitintensiver ist als zwei mov-Befehle. Gibt es überhaupt eine Möglichkeit die Zeit oder die Mips eines Programms zu messen ?
    Du schreibst auch, dass die Ausführungsreihenfolge beliebig sein kann. Würde dies aber nicht bedeuten, dass es theoretisch möglich ist, dass Zeile 3 vor Zeile 1 ausgeführt werden könnte, was anschließend zu einem völlig falschen Ergebnis führen würde ?
    Oder wenn nur Befehle parallel ausgeführt, bzw. ihre Reihenfolge im Programmcode vernachlässigt, wenn sie keinen direkten Einfluss auf kommende Anweisungen haben.

    Gruß
    Brasi



  • Das ein Register nur über einen Namen angesprochen werden kann und sich nicht im allgemeinen Speicher befindet sehe ich auch so, doch ist es nicht so, dass sich auch hinter jedem Registernamen nur eine Speicheradresse verbirgt. Für uns als Anwender ist es natürlich komfortabler einen Namen als eine Adresse anzusprechen.

    Nein, eben dies nicht. Die Register sind ein fester Bestandteil der CPU, deren Inhalt nicht im Speicher liegen und daher auch keine Adresse besitzen. Die Register sind eigenständige Speichereinheiten, die man halt durch ihre Namen bzw. durch ihre Codierung direkt angesprochen werden können.


  • Mod

    Brasilian schrieb:

    Das ein Register nur über einen Namen angesprochen werden kann und sich nicht im allgemeinen Speicher befindet sehe ich auch so, doch ist es nicht so, dass sich auch hinter jedem Registernamen nur eine Speicheradresse verbirgt. Für uns als Anwender ist es natürlich komfortabler einen Namen als eine Adresse anzusprechen.

    es hat keine adresse in dem sinne, dass man diese benutzen könnte um damit indirekt auf das register zuzugreifen. es ist also nicht teil des allgemeinen adressraumes. wie die cpu intern auf ihre register zugreift muss hier nicht interessieren.

    Oder wenn nur Befehle parallel ausgeführt, bzw. ihre Reihenfolge im Programmcode vernachlässigt, wenn sie keinen direkten Einfluss auf kommende Anweisungen haben.

    sinngemäss ja, meine formulierung war: "sofern sich die semantik des programms nicht ändert". oder anders gesagt, die cpu wird einen befehl nicht vor einem anderen ausführen, sofern ein operand vom ergebnis dieses vorhergehenden befehls abhängig ist. diese beschränkung verbietet, dass der 3. befehl vor dem 1. ausgeführt wird. ausserdem werden schreibzugriffe erst ausgeführt, nachdem alle vorhergehenden lesezugriffe auf diesen speicherbereich abgeschlossen sind. das verhindert, dass 3. vor 2. ausgeführt wird. im übrigen sind (wir reden hier nur von x86 cpus) lesezugriffe untereinander ungeordnet, schreibzugriffe sind untereinander geordnet, ausser bei streaming stores. zusätzlich gibt es einige befehle, um die herum andere befehle oder load/store nicht umgeordnet werden können. das wird in multi-prozessor umgebungen besonders wichtig.



  • Brasilian schrieb:

    Wenn ich dich richtig verstehe ist dieser vierzeilige Code schneller als der von mir angegebene dreizeilige Code (mit xchg). Dies liegt daran, dass hier nur der Elementarbefehl mov auftaucht, wogegen der xchg-Befehl zeitintensiver ist als zwei mov-Befehle. Gibt es überhaupt eine Möglichkeit die Zeit oder die Mips eines Programms zu messen ?

    Das ist nicht nur schneller, das ist VIEL schneller (Faktor 50 aufwärts geschätzt). Messen kannst du mit rdtsc. Mach eine Million Iterationen oder so und miss davor und danach.



  • Hallo,

    ich glaube es ist einiges klarer geworden, dafür dank ich euch.

    Ich bin schon etwas überrascht über den geschätzen Faktor 50 aufwärts, welchen Ringding angibt. Eine Frage hätte ich da aber noch, und zwar würde mich mal interessieren, ob ich den ASM-Code sehen könnte, der nur durch die C++ Anweisng zum Vertauschen von zwei Werten erzeugt würde.

    Also, was würde aus folgendem gemacht ?

    char var1 = 'A', var2 = 'B';
    // Ab hier
    char temp = var1;
    var1 = var2;
    var2 = var1;
    

    Wie sieht es hier den mit dem Laufzeitfaktor aus, wer traut sich eine Schätzung abzugeben :xmas1:

    Gruß
    Brasi


  • Mod

    ich nehme an, du meinst var2 = temp;

    das dürfte davon abhängen, was anschliessend damit gemacht wird, sonst optimiert dir der compiler das gleich ganz weg - auf jeden fall, kann der compiler das vertauschen hier bereits durch simple statische analyse vermeiden. die schnellste form des vertauschens ist immer noch die, die gar nicht stattfindet. mit inline assembler ist das relativ schwierig transparent zu machen - bei externem assembler kann man sich hier sehr schön durch das vertauschen von text-macros helfen.

    im übrigen ist der faktor 50 hier zu 99% durch das implizite lock von xchg bedingt.



  • ich nehme an, du meinst var2 = temp;
    

    Ja, das ist so gemeint



  • -> schrieb:

    Die Register sind ein fester Bestandteil der CPU, deren Inhalt nicht im Speicher liegen und daher auch keine Adresse besitzen. Die Register sind eigenständige Speichereinheiten, die man halt durch ihre Namen bzw. durch ihre Codierung direkt angesprochen werden können.

    Das ist beim x86 so, bei anderen kann es auch anders aussehen. So kann man beim C167 die Anzahl der (GP-)Register (bis zu einer gewissen Grenze natürlich, etwa 2KiB) ausbauen (über die Anzahl der Registerbänke). Verwenden von Registern bringt dabei Vorteile durch kompaktere Opcodes und evtl. weniger pipeline stalls, weil das Laden der Variablenadressen entfällt. Braucht man nicht so viele Register, so kann man den Speicher auch "normal" nutzen (nur für Daten, da Harvard).


Anmelden zum Antworten