Frage

Wenn C/C ++ -Codes mit GCC/G ++ kompiliert werden, kann es mir sagen, wenn es mein Register ignoriert? Zum Beispiel in diesem Code

int main()
{
    register int j;
    int k;
    for(k = 0; k < 1000; k++)
        for(j = 0; j < 32000; j++)
            ;
    return 0;
}

J wird als Register verwendet, aber in diesem Code

int main()
{
    register int j;
    int k;
    for(k = 0; k < 1000; k++)
        for(j = 0; j < 32000; j++)
            ;
    int * a = &j;
    return 0;
}

J wird eine normale Variable sein. Kann es mir sagen, ob eine von mir verwendete Variable, die ich verwendet habe, wirklich in einem CPU -Register gespeichert ist?

War es hilfreich?

Lösung

Sie können fair annehmen, dass GCC die ignoriert register Schlüsselwort außer vielleicht bei -O0. Es sollte jedoch keinen Unterschied auf die eine oder andere Weise bewirken, und wenn Sie in solcher Tiefe sind, sollten Sie den Montagecode bereits lesen.

Hier ist ein informativer Thread zu diesem Thema: http://gcc.gnu.org/ml/gcc/2010-05/msg00098.html . Zurück in den alten Tagen, register In der Tat hat Compiler geholfen, eine Variable in Register zuzuordnen, aber heute kann die Registrierungszuweisung optimal und automatisch ohne Hinweise erreicht werden. Das Schlüsselwort dient weiterhin zwei Zwecke in C:

  1. In C verhindert es, dass Sie die Adresse einer Variablen übernehmen. Da Register keine Adressen haben, kann diese Einschränkung einem einfachen C -Compiler helfen. (Einfache C ++ - Compiler existieren nicht.)
  2. EIN register Objekt kann nicht deklariert werden restrict. Da restrict In Bezug auf Adressen ist ihre Schnittstelle sinnlos. (C ++ hat noch nicht restrict, Und trotzdem ist diese Regel ein bisschen trivial.)

Für C ++ wurde das Schlüsselwort seit C ++ 11 und veraltet zum Entfernen vorgeschlagen aus der für 2017 geplanten Standardrevision.

Einige Compiler haben verwendet register Bei Parameterdeklarationen zur Bestimmung der aufrufenden Funktionskonvention, wobei der ABI gemischte Stapel- und Register-basierte Parameter ermöglicht. Dies scheint nicht konform zu sein, es tritt tendenziell bei einer erweiterten Syntax auf register("A1"), und ich weiß nicht, ob ein solcher Compiler noch verwendet wird.

Andere Tipps

In Bezug auf moderne Zusammenstellung und Optimierungstechniken die register Annotation macht überhaupt keinen Sinn. In Ihrem zweiten Programm nehmen Sie die Adresse von an j, und Register haben keine Adressen, aber eine gleiche lokale oder statische Variable kann während ihres Lebens an zwei verschiedenen Speicherorten oder manchmal in Erinnerung und manchmal in einem Register oder überhaupt nicht in einem Register gespeichert werden. In der Tat würde ein optimierender Compiler Ihre verschachtelten Loops als nichts zusammenstellen k und j. Und dann diese Zuordnungen weglassen, da der verbleibende Code diese Werte nicht verwendet.

Sie können die Adresse eines Registers in C nicht erhalten, und der Compiler kann Sie völlig ignorieren. C99 Standard, Abschnitt 6.7.1 (PDF):

Die Implementierung kann jede Registererklärung einfach als Auto -Erklärung behandeln. Unabhängig davon, ob tatsächlich adressierbarer Speicher verwendet wird oder nicht, kann die Adresse eines Teils eines mit Speicherklassen-Spezifikatorregisters deklarierten Objekts entweder explizit (unter Verwendung des Unary & Operators gemäß 6.5.3.2) oder implizit (durch Verwendung des Unary & Operators gemäß 6.5.3.2) berechnet werden. Durch Konvertieren eines Array -Namens in einen Zeiger, wie in 6.3.2.1). Der einzige Bediener, der auf ein mit Speicherklassenspezifizierungsregister deklarierter Array angewendet werden kann, ist die Größe.

Wenn Sie nicht auf 8-Bit-AVRs oder -Pics herumfassen, wird der Compiler wahrscheinlich über Sie denken, dass Sie es am besten wissen und Ihre Bitten ignorieren. Sogar bei ihnen habe ich gedacht, ich wusste ein paar Mal besser und fand Wege, den Compiler (mit einiger Inline -ASM) auszutricksen, aber mein Code explodierte, weil er eine Reihe anderer Daten massieren musste, um meine Sturheit umzuarbeiten.

Diese Frage und einige der Antworten und einige andere Diskussionen der Schlüsselwörter "Register", die ich gesehen habe - scheinen implizit anzunehmen, dass alle Einheimischen entweder einem bestimmten Register oder einem bestimmten Speicherort auf dem Stapel zugeordnet werden. Dies war im Allgemeinen bis vor 15-25 Jahren zutrifft, und es ist wahr, wenn Sie die Optimierung deaktivieren, aber es gilt überhaupt nicht, wenn die Standardoptimierung durchgeführt wird. Die Einheimischen werden jetzt von Optimierern als symbolische Namen gesehen, mit denen Sie den Datenfluss beschreiben, und nicht als Werte, die an bestimmten Stellen gespeichert werden müssen.

HINWEIS: Mit 'Einheimischen' hier meine ich: Skalare Variablen der Speicherklasse Auto (oder 'Register'), die niemals als Operand von '&' verwendet werden. Compiler können manchmal automatische Strukturen, Gewerkschaften oder Arrays in individuelle „lokale“ Variablen zerlegen.

Um dies zu veranschaulichen: Angenommen, ich schreibe dies oben in einer Funktion:

int factor = 8;

.. und dann die einzige Verwendung der factor Variable besteht darin, sich mit verschiedenen Dingen zu multiplizieren:

arr[i + factor*j] = arr[i - factor*k];

In diesem Fall - versuchen Sie es, wenn Sie möchten - es wird nein geben factor Variable. Die Codeanalyse zeigt das factor ist immer 8, und so werden alle Verschiebungen in sich verwandeln <<3. Wenn Sie 1985 das Gleiche getan haben, c, C. factor Würde einen Standort auf dem Stapel bekommen, und es würde Mehrkollegen geben, da die Compiler im Grunde genommen jeweils jeweils eine Aussage bearbeitet haben und sich an nichts über die Werte der Variablen erinnerten. Damals würden Programmierer eher einsetzen #define factor 8 Um einen besseren Code in dieser Situation zu erhalten, während Sie einstellbar erhalten werden factor.

Wenn du benutzt -O0 (Optimierung aus) - Sie erhalten tatsächlich eine Variable für factor. Dies ermöglicht es Ihnen beispielsweise, über die zu treten factor=8 Aussage und dann ändern factor bis 11 mit dem Debugger und weitermachen. Damit dies funktioniert, kann der Compiler nicht behalten irgendetwas in Registern zwischen Aussagen, mit Ausnahme von Variablen, die bestimmten Registern zugeordnet sind; Und in diesem Fall wird der Debugger darüber informiert. Und es kann nicht versuchen, etwas über die Werte von Variablen zu wissen, da der Debugger sie ändern könnte. Mit anderen Worten, Sie benötigen die Situation von 1985, wenn Sie beim Debuggen lokale Variablen ändern möchten.

Moderne Compiler erstellen im Allgemeinen eine Funktion wie folgt:

(1) Wenn eine lokale Variable mehr als einmal in einer Funktion zugeordnet ist, erstellt der Compiler verschiedene "Versionen" der Variablen, so dass jeder nur an einem Ort zugewiesen wird. Alle "Reads" der Variablen beziehen sich auf eine bestimmte Version.

(2) Jeder dieser Einheimischen wird einem "virtuellen" Register zugeordnet. Intermediate -Berechnungsergebnisse werden auch Variablen/Register zugewiesen; Also

  a = b*c + 2*k;

wird so etwas wie

       t1 = b*c;
       t2 = 2;
       t3 = k*t2;
       a = t1 + t3;

(3) Der Compiler nimmt dann alle diese Operationen an und sucht nach gängigen Unterexpressionen usw. Da jedes der neuen Register nur einmal geschrieben wird, ist es ziemlich einfacher, sie neu zu ordnen, während die Korrektheit beibehalten wird. Ich werde nicht einmal mit der Schleifenanalyse beginnen.

(4) Der Compiler versucht dann, alle diese virtuellen Register in tatsächliche Register zuzuordnen, um Code zu generieren. Da jedes virtuelle Register eine begrenzte Lebensdauer hat, ist es möglich, tatsächliche Register stark wiederzuverwenden - 'T1' im oben genannten wird nur bis zum Add, das 'a' erzeugt, benötigt, sodass es im selben Register wie "a" gehalten werden kann. Wenn es nicht genügend Register gibt, können einige der virtuellen Register dem Speicher zugewiesen werden - oder - oder - ein Wert kann in einem bestimmten Register gehalten, für eine Weile in den Speicher gespeichert und in ein (möglicherweise) anderes Register später zurückgeladen werden . Auf einer Last-Store-Maschine, wobei nur Werte in Registern in Berechnungen verwendet werden können, ist diese zweite Strategie genau das gut.

Aus dem obigen sollte dies klar sein: Es ist leicht zu bestimmen, dass das virtuelle Register zugeordnet ist factor ist dasselbe wie die Konstante '8' und so alle Multiplikationen von factor sind Multiplikationen von 8. Auch wenn factor wird später geändert, das ist eine "neue" Variable und hat keine frühere Verwendung von factor.

Eine weitere Implikation, wenn Sie schreiben

 vara = varb;

.. Es kann der Fall sein oder nicht, dass im Code eine entsprechende Kopie vorhanden ist. Zum Beispiel

int *resultp= ...
int acc = arr[0] + arr[1];
int acc0 = acc;    // save this for later
int more = func(resultp,3)+ func(resultp,-3);
acc += more;         // add some more stuff
if( ...){
    resultp = getptr();
    resultp[0] = acc0;
    resultp[1] = acc;
}

In den oben genannten zwei "Versionen" von ACC (anfänglich und nach dem Hinzufügen von "mehr") könnten in zwei verschiedenen Registern stattfinden, und "ACC0" wäre dann das gleiche wie das Inital "ACC". Für 'ACC0 = ACC' wäre also keine Registerkopie erforderlich. Ein weiterer Punkt: Das 'ErgebnisP' wird zweimal zugeordnet, und da die zweite Zuordnung den vorherigen Wert ignoriert, gibt es im Code im Wesentlichen zwei unterschiedliche "Ergebnisvariablen", und dies kann leicht durch Analyse bestimmt werden.

Eine Implikation von all dem: Seien Sie nicht zögern, komplexe Ausdrücke in kleinere Ausdrücke mit zusätzlichen Einheimischen für Zwischenprodukte auszudehnen, wenn der Code leichter zu folgen ist. Im Grunde genommen gibt es dafür keine Laufzeitstrafe, da der Optimierer trotzdem dasselbe sieht.

Wenn Sie mehr erfahren möchten, können Sie hier beginnen: http://en.wikipedia.org/wiki/static_single_asssignment_form

Es geht darum, (a) eine Vorstellung davon zu geben, wie moderne Compiler funktionieren, und (b) darauf hinweisen wirklich Sinn machen. Jede "Variable" kann vom Optimierer als mehrere Variablen gesehen werden, von denen einige in Schleifen stark verwendet werden können, andere nicht. Einige Variablen verschwinden - zB durch konstantes; Oder manchmal die in einem Tausch verwendete Temperaturvariable. Oder Berechnungen nicht wirklich verwendet. Der Compiler ist für das gleiche Register für verschiedene Dinge in verschiedenen Teilen des Codes ausgestattet, je nachdem, was tatsächlich am besten auf der Maschine ist, für die Sie zusammenstellen.

Der Begriff, den Compiler anzeigen, welche Variablen in Registern sein sollten, geht davon aus, dass jede lokale Variable an ein Register oder an einen Speicherort kartiert. Dies gilt zurück, als Kernighan + Ritchie die C -Sprache entwarf, aber nicht mehr wahr ist.

In Bezug auf die Einschränkung, dass Sie die Adresse einer Registervariablen nicht annehmen können: Es gibt eindeutig keine Möglichkeit, die Adresse einer in einem Register gehaltenen Variablen zu übernehmen, aber Sie könnten fragen - da der Compiler Diskretion hat, um das 'Register' zu ignorieren ' - Warum ist diese Regel vorhanden? Warum kann der Compiler nicht einfach das 'Register' ignorieren, wenn ich die Adresse nehme? (wie in C ++ der Fall).

Wieder musst du zum alten Compiler zurückkehren. Der ursprüngliche K+R -Compiler würde eine lokale variable Deklaration analysieren, und dann würde dann sofort Entscheiden Sie, ob Sie es einem Register zuweisen oder nicht (und wenn ja, welches Register). Dann würde es Ausdrücke kompilieren und den Assembler für jede Aussage einzeln ausgeben. Wenn später festgestellt wurde, dass Sie die Adresse einer "Register" -Variable übernommen haben, die einem Register zugewiesen worden war, gab es keine Möglichkeit, dies zu behandeln, da die Aufgabe im Allgemeinen bis dahin irreversibel war. Es war jedoch möglich, eine Fehlermeldung zu generieren und zu kompilieren.

Fazit scheint es, dass 'Register' im Wesentlichen veraltet ist:

  • C ++ - Compiler ignorieren es vollständig
  • C Compiler ignorieren es, außer um die Einschränkung durchzusetzen & - und ignorieren es möglicherweise nicht bei -O0 wo es tatsächlich zu Allokation führen kann, wie angefordert. Bei -O0 sind Sie jedoch keine Sorgen um die Codegeschwindigkeit.

Es gibt also im Grunde genommen jetzt für die Rückwärtskompatibilität und wahrscheinlich auf der Grundlage, dass einige Implementierungen es noch für "Hinweise" verwenden könnten. Ich benutze es nie-und ich schreibe Echtzeit-DSP-Code und verbringe ein bisschen Zeit damit, generierten Code zu suchen und Wege zu finden, um ihn schneller zu machen. Es gibt viele Möglichkeiten, den Code zu ändern, damit er schneller ausgeführt wird, und es ist sehr hilfreich, zu wissen, wie Compiler funktionieren. Es ist in der Tat schon lange her, dass ich das letzte Mal festgestellt habe, dass das Hinzufügen von 'Register' zu diesen Wegen gehörte.


Nachtrag

Ich habe oben aus meiner besonderen Definition von "Einheimischen", Variablen, zu denen ausgeschlossen & wird angewendet (diese sind natürlich im üblichen Sinne des Begriffs enthalten).

Betrachten Sie den folgenden Code:

void
somefunc()
{
    int h,w;
    int i,j;
    extern int pitch;

    get_hw( &h,&w );  // get shape of array

    for( int i = 0; i < h; i++ ){
        for( int j = 0; j < w; j++ ){
            Arr[i*pitch + j] = generate_func(i,j);
        }
    }
}

Dies mag vollkommen harmlos aussehen. Wenn Sie jedoch über die Ausführungsgeschwindigkeit besorgt sind, sollten Sie Folgendes bedenken: Der Compiler übergibt die Adressen von h und w zu get_hw, und dann später anrufen generate_func. Nehmen wir an, der Compiler weiß nichts darüber, was in diesen Funktionen ist (was der allgemeine Fall ist). Der Compiler muss Angenommen, der Anruf an generate_func könnte sich ändern h oder w. Das ist eine vollkommen rechtliche Verwendung des Zeigers get_hw - Sie können es irgendwo aufbewahren und später verwenden, solange der Umfang enthält h,w ist noch im Spiel, diese Variablen zu lesen oder zu schreiben.

Somit muss der Compiler speichern h und w Im Speicher auf dem Stapel und kann nichts im Voraus darüber bestimmen, wie lange die Schleife ausgeführt wird. Es werden also bestimmte Optimierungen unmöglich sein, und die Schleife könnte infolge welches ist gelegentlich in der inneren Schleife aufgerufen, abhängig von einer Bedingung).

Ein weiteres Problem hier ist das generate_func könnte ändern pitch, und so i*pitch muss jedes Mal erledigen und nicht nur wann i Änderungen.

Es kann wiederhergestellt werden als:

void
somefunc()
{
    int h0,w0;
    int h,w;
    int i,j;
    extern int pitch;
    int apit = pitch;

    get_hw( &h0,&w0 );  // get shape of array
    h= h0;
    w= w0;

    for( int i = 0; i < h; i++ ){
        for( int j = 0; j < w; j++ ){
            Arr[i*apit + j] = generate_func(i,j);
        }
    }
}

Jetzt die Variablen apit,h,w sind alle "sichere" Einheimische in dem Sinne, den ich oben definiert habe, und der Compiler kann sicher sein, dass sie durch Funktionsaufrufe nicht geändert werden. Angenommen, ich bin nicht irgendetwas ändern generate_func, Der Code hat den gleichen Effekt wie zuvor, könnte aber effizienter sein.

Jens Gustedt hat die Verwendung des Schlüsselworts "Register" vorgeschlagen, um Schlüsselvariablen zu markieren, um die Verwendung von zu hemmen & bei ihnen, zB durch andere, die den Code pflegen & ohne es). Meinerseits denke ich immer sorgfältig nach, bevor ich mich bewerbe & an jeden lokalen Skalar in einem zeitkritischen Bereich des Codes und meiner Ansicht nach mit 'Register', um dies durchzusetzen Ignorieren Sie das 'Register').

Im Hinblick auf die Codeeffizienz ist der beste Weg, um eine Funktion zu haben, zwei Werte mit einer Struktur:

struct hw {  // this is what get_hw returns
   int h,w;
};

void
somefunc()
{
    int h,w;
    int i,j;

    struct hw hwval = get_hw();  // get shape of array
    h = hwval.h;
    w = hwval.w;
    ...

Dies mag umständlich aussehen (und ist umständlich zu schreiben), generiert aber saubereren Code als die vorherigen Beispiele. Die 'Struct HW' wird tatsächlich in zwei Registern zurückgegeben (ohnehin auf den meisten modernen Abis). Und aufgrund der Art und Weise, wie die "hwval" -Struktur verwendet wird, wird der Optimierer sie effektiv in zwei "Einheimische" zerlegt hwval.h und hwval.w, und dann bestimmen, dass diese gleichwertig sind h und w -- Also hwval wird im Wesentlichen im Code verschwinden. Es müssen keine Zeiger übergeben werden, es ist keine Funktion, die Variablen einer anderen Funktion über Zeiger zu ändern. Es ist nur so, als hätte er zwei unterschiedliche Skalarrenditewerte. Dies ist jetzt viel einfacher in C ++ 11 - mit std::tie und std::tuple, Sie können diese Methode mit weniger Ausführlichkeit verwenden (und ohne eine Strukturdefinition schreiben zu müssen).

Ihr zweites Beispiel ist in C ungültig register Schlüsselwort ändert etwas (in C). Es ist nur für diesen Zweck da, die Annahme einer Adresse einer Variablen zu hemmen. Nehmen Sie also einfach nicht den Namen "Register" verbal, es ist eine Fehlbezeichnung, sondern halten Sie sich an seine Definition.

Dass C ++ zu ignorieren scheint register, Nun, sie müssen ihren Grund dafür haben, aber ich finde es irgendwie traurig, einen dieser subtilen Unterschiede wieder zu finden, bei dem gültiger Code für den einen für den anderen ungültig ist.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top