c - pointer - std::array




Array-Syntax vs. Zeigersyntax und Codegenerierung? (6)

In dem Buch "Verständnis und Verwendung von C-Zeigern" von Richard Reese heißt es auf Seite 85:

int vector[5] = {1, 2, 3, 4, 5};

Der von vector[i] generierte Code unterscheidet sich von dem von *(vector+i) generierten Code. Der Notationsvektor vector[i] generiert Maschinencode, der am Positionsvektor beginnt, i Positionen von dieser Position entfernt und seinen Inhalt verwendet. Die Notation *(vector+i) generiert Maschinencode, der am Positionsvektor beginnt, i zur Adresse hinzufügt und dann den Inhalt an dieser Adresse verwendet. Während das Ergebnis das gleiche ist, ist der generierte Maschinencode unterschiedlich. Dieser Unterschied ist für die meisten Programmierer selten von Bedeutung.

Den Auszug sehen Sie hier . Was bedeutet diese Passage? In welchem ​​Kontext würde ein Compiler einen anderen Code für diese beiden generieren? Gibt es einen Unterschied zwischen "Verschieben" von der Basis und "Hinzufügen" zur Basis? Ich war nicht in der Lage, dies auf GCC zum Laufen zu bringen - es wurde ein anderer Maschinencode generiert.


Das Zitat ist einfach falsch. Ziemlich tragisch, dass solch ein Müll noch in diesem Jahrzehnt veröffentlicht wird. Tatsächlich definiert der C-Standard x[y] als *(x+y) .

Der Teil über lWerte später auf der Seite ist ebenfalls völlig und gänzlich falsch.

Meiner Meinung nach ist der beste Weg, dieses Buch zu verwenden, es in einen Papierkorb zu werfen oder es zu verbrennen.


Der Standard spezifiziert das Verhalten von arr[i] wenn arr ein Array-Objekt ist, als äquivalent zur Zerlegung von arr in einen Zeiger, zum Hinzufügen von i und zur Dereferenzierung des Ergebnisses. Obwohl das Verhalten in allen vom Standard definierten Fällen gleich ist, verarbeiten Compiler in einigen Fällen Aktionen sinnvoll, obwohl dies vom Standard verlangt wird, und die Behandlung von arrayLvalue[i] und *(arrayLvalue+i) kann sich daher unterscheiden .

Zum Beispiel gegeben

char arr[5][5];
union { unsigned short h[4]; unsigned int w[2]; } u;

int atest1(int i, int j)
{
if (arr[1][i])
    arr[0][j]++;
return arr[1][i];
}
int atest2(int i, int j)
{
if (*(arr[1]+i))
    *((arr[0])+j)+=1;
return *(arr[1]+i);
}
int utest1(int i, int j)
{
    if (u.h[i])
        u.w[j]=1;
    return u.h[i];
}
int utest2(int i, int j)
{
    if (*(u.h+i))
        *(u.w+j)=1;
    return *(u.h+i);
}

GCCs generierter Code für test1 geht davon aus, dass arr [1] [i] und arr [0] [j] kein Alias ​​sind, aber der generierte Code für test2 ermöglicht Zeigerarithmetik, um auf das gesamte Array zuzugreifen Erkennen Sie, dass in utest1 die lvalue-Ausdrücke uh [i] und uw [j] beide auf die gleiche Vereinigung zugreifen, aber nicht differenziert genug sind, um dasselbe über * (u.h + i) und * (u.w + j) in zu bemerken utest2.


Dies ist eine Beispiel-Array-Syntax, wie sie in C verwendet wird.

int a[10] = {1,2,3,4,5,6,7,8,9,10};

Ich denke, worauf sich der ursprüngliche Text möglicherweise bezieht, sind einige Optimierungen, die ein Compiler möglicherweise durchführt oder nicht.

Beispiel:

for ( int i = 0; i < 5; i++ ) {
  vector[i] = something;
}

gegen

for ( int i = 0; i < 5; i++ ) {
  *(vector+i) = something;
}

Im ersten Fall kann ein optimierender Compiler erkennen, dass der Array- vector Element für Element durchlaufen wird, und so etwas wie erzeugen

void* tempPtr = vector;
for ( int i = 0; i < 5; i++ ) {
  *((int*)tempPtr) = something;
  tempPtr += sizeof(int); // _move_ the pointer; simple addition of a constant.
}

Möglicherweise kann es sogar die Anweisungen der Ziel-CPU nach dem Inkrementieren verwenden, sofern verfügbar.

Für den zweiten Fall ist es für den Compiler "schwerer" zu erkennen, dass die Adresse , die über einen "willkürlichen" Zeigerarithmetikausdruck berechnet wird, die gleiche Eigenschaft aufweist, bei jeder Iteration einen festen Betrag monoton vorzurücken. Es kann daher sein, dass es die Optimierung nicht findet und ((void*)vector+i*sizeof(int)) in jeder Iteration berechnet, die eine zusätzliche Multiplikation verwendet. In diesem Fall gibt es keinen (temporären) Zeiger, der "verschoben" wird, sondern nur eine neu berechnete temporäre Adresse.

Die Aussage ist jedoch wahrscheinlich nicht für alle C-Compiler in allen Versionen allgemein gültig.

Aktualisieren:

Ich habe das obige Beispiel überprüft. Es scheint, dass mindestens gcc-8.1 x86-64 ohne aktivierte Optimierungen mehr Code (2 zusätzliche Anweisungen) für die zweite Form (Zeiger-Arithmetik) generiert als für die erste Form (Array-Index).

Siehe: https://godbolt.org/g/7DaPHG

Bei -O3 Optimierungen ( -O ... -O3 ) ist der generierte Code jedoch für beide gleich (Länge).


Ich habe den Code auf einige Compiler-Varianten getestet. Die meisten geben mir für beide Anweisungen den gleichen Assembly-Code (getestet für x86 ohne Optimierung). Interessant ist, dass der gcc 4.4.7 genau das macht, was Sie erwähnt haben: Beispiel:

Andere Sprachen wie ARM oder MIPS tun manchmal das Gleiche, aber ich habe nicht alles getestet. Es scheint also ein Unterschied zu sein, aber spätere Versionen von gcc haben diesen Fehler "behoben".


Lassen Sie mich versuchen, dies "im engeren Sinne" zu beantworten (andere haben bereits beschrieben, warum die Beschreibung "wie sie ist" etwas fehlt / unvollständig / irreführend ist):

In welchem ​​Kontext würde ein Compiler einen anderen Code für diese beiden generieren?

Ein "nicht sehr optimierender" Compiler kann in nahezu jedem Kontext unterschiedlichen Code generieren, da es beim Parsen einen Unterschied gibt: x[y] ist ein Ausdruck (Index in ein Array), während *(x+y) zwei Ausdrücke (fügen Sie einem Zeiger eine Ganzzahl hinzu und dereferenzieren Sie ihn dann). Sicher, es ist nicht sehr schwer, dies zu erkennen (auch beim Parsen) und es gleich zu behandeln, aber wenn Sie einen einfachen / schnellen Compiler schreiben, vermeiden Sie es, "zu viel Smart in ihn" zu stecken. Als Beispiel:

char vector[] = ...;
char f(int i) {
    return vector[i];
}
char g(int i) {
    return *(vector + i);
}

Der Compiler erkennt beim Parsen von f() die "Indizierung" und generiert möglicherweise so etwas wie (für eine 68000-ähnliche CPU):

MOVE D0, [A0 + D1] ; A0/vector, D1/i, D0/result of function

OTOH, für g() , sieht der Compiler zwei Dinge: Erstens eine Dereferenzierung (von "noch etwas Zukünftigem") und dann das Hinzufügen einer Ganzzahl zu einem Zeiger / Array.

MOVE A1, A0   ; A1/t = A0/vector
ADD A1, D1    ; t += i/D1
MOVE D0, [A1] ; D0/result = *t

Dies hängt natürlich stark von der Implementierung ab. Einige Compiler mögen möglicherweise auch keine komplexen Anweisungen für f() (die Verwendung komplexer Anweisungen erschwert das Debuggen des Compilers). Die CPU verfügt möglicherweise nicht über solche komplexen Anweisungen usw.

Gibt es einen Unterschied zwischen "Verschieben" von der Basis und "Hinzufügen" zur Basis?

Die Beschreibung im Buch ist wohl nicht gut formuliert. Aber ich denke, der Autor wollte den oben gezeigten Unterschied beschreiben - Indizieren ("Verschieben" von der Basis) ist ein Ausdruck, während "Hinzufügen und dann Dereferenzieren" zwei Ausdrücke sind.

Hier geht es um die Implementierung des Compilers , nicht um die Sprachdefinition, die Unterscheidung, die im Buch auch explizit hätte angegeben werden müssen.





errata