Seite 1 von 1

"Temporäre" Variablen - sinnvoll?

Verfasst: 20.05.2024, 18:43
von starcow
n'Abend meine geschätzen ZFX'ler :-)

Ich habe öfters die Situation, dass sich die Frage aufdrängt, ob sich das Anlegen einer temporäre Variable lohnen könnte.

Dabei drehen sich diesbezüglich meine Überlegungen eigentlich immer um die beiden Aspekte "Optimierungen" und "Lesbarkeit".

Exemplarisch:
1) Eine for-schleife die im Bedingungsteil irgendeine Berechnung anstellen muss.

Code: Alles auswählen

for(int i = 0; i < Board->wid * Board->hei; ++i)
oder:

Code: Alles auswählen

for(int i = 0; i < sizeof(a)/sizeof(a[0]); ++i)
(Je nach dem auch etwas komplexer)

Ist der Compiler "schlau" genug, sich gewisse Resultate selbstständig zwischenzuspeichern?
Im ersten der beiden Beispiele würde man ja - im schlimmsten Falle - zweimal einen Pointer dereferenzieren und dann noch multiplizieren - und das in jedem Schleifendurchgang.
Im zweiten Beispiel würde zwar der sizeof Operator zur Compile-Time ausgewertet, doch es bliebe die Division - die ja auch nicht grad gratis ist.
Alternativ könnte man nun dazu übergehen ausserhalb der Schleife eine Stack-Variable zu deklarieren:

Code: Alles auswählen

int cnt = Board->wid * Board->hei;
Die hat dann halt ihren Scope nicht nur auf die Schleife begrenzt, was vielleicht nicht ganz so "schön" ist. Man könnte sie ja manchmal in den Schlaufenkopf nehmen - dass geht aber ja bekanntlich nur, wenn sie vom gleichen Typ ist, wie die Zählvariable.

Code: Alles auswählen

for(int i = 0, v = Board->wid * Board->hei; i < v; ++i)
Ein weiteres Szenario: Indices für ein Array.

Code: Alles auswählen

a[Board->wid * Board->hei + 1]
Auch hier wieder (womöglich) die Dereferenzierung zweier Pointer und sanftes Gerechne.

Dann weiter: Fragen zur Lesbarkeit
Selbst wenn man den Wert einer temporäre Stack-Variable ohnehin immer wieder erneut ausrechnen müsste, bliebe noch das Argument der besseren Lesbarkeit (?).

Code: Alles auswählen

int index = Board->wei * Board->hei + 1;
a[index] = 5;
Sinnvoll oder unnötiges Rauschen im Code?

Zuguter letzt kommen dann noch Überlegungen zum Stack hinzu.
Der müsste ja eigentlich jedesmal aufs Neue angelegt werden (beim Eintritt in die Funktion).
Sollte ich also ein array mit z.B. 256 Elementen (Jump Table) besser static deklarieren, damit der Compiler sich das dauernde Aufbauen und Abräumen ersparen kann?

Wie handhabt ihr das in eurem Code?
(Ich gehe jetzt hier primär von C aus. Aber hinsichtlich diesen Fragen dürfte das diesmal kein Unterschied machen)

LG, starcow

Re: "Temporäre" Variablen - sinnvoll?

Verfasst: 20.05.2024, 19:49
von Krishty
Ist der Compiler "schlau" genug, sich gewisse Resultate selbstständig zwischenzuspeichern?
Im ersten der beiden Beispiele würde man ja - im schlimmsten Falle - zweimal einen Pointer dereferenzieren und dann noch multiplizieren - und das in jedem Schleifendurchgang.
Manchmal ja, oft nicht.

Das ist ein Problem der Alias-Analyse, das je nach Compiler und Optimierungsstufe unterschiedlich gelöst wird. Der Compiler kann die Variable nur zwischenspeichern, wenn er beweisen kann, dass sie sich nicht pro Schleifendurchlauf ändern wird. Die Alias-Analyse von GCC ist größtenteils typbasiert. Sind wid und hei bspw. vom Typ int und verändert der Schleifenrumpf eine int-Variable, für die eine Überlappung mit Board nicht ausgeschlossen werden kann, liest er sie neu.

Verändert der Schleifenrumpf Strings, die nicht ganz offensichtlich auf dem Stack liegen, lädt der Compiler grundsätzlich alles neu weil sich char * mit allem überlappen darf.

In diesen Fällen profitiert die Optimierung von einer zusätzlichen Variable.
(Ich gehe jetzt hier primär von C aus. Aber hinsichtlich diesen Fragen dürfte das diesmal kein Unterschied machen)
Das ist auch einer der Fälle, in denen C++-Referenzen besser optimieren als C-Zeiger: Falls der Compiler nicht ausschließen kann, dass sich der Zeiger Board innerhalb der Schleife ändert, muss er ihn bei jedem Durchlauf neu laden. Wäre Board hingegen eine Referenz, könnte der Compiler sich darauf verlassen, dass Referenzen in C++ unveränderlich sind und ein erneutes Laden von Board vermeiden.
Sollte ich also ein array mit z.B. 256 Elementen (Jump Table) besser static deklarieren, damit der Compiler sich das dauernde Aufbauen und Abräumen ersparen kann?
Dass Bereitstellen eines Arrays kostet nur Zeit, wenn es auch initialisiert werden muss. Ist der Inhalt bei jedem Aufruf unterschiedlich, musst du sowieso alles überschreiben und gewinnst nichts durch ein Auslagern in static (IMHO büßt du eher noch Multi-Threading-Tauglichkeit ein.) Ist es bei jedem Aufruf gleich, sollte es eh static const sein, damit die Initialisierung beim Kompilieren stattfindet. Dann kostet das Array zur Laufzeit nichts.

Re: "Temporäre" Variablen - sinnvoll?

Verfasst: 20.05.2024, 20:06
von Krishty
Als Nachtrag ein Code-Beispiel; Link zu Compiler Explorer:

Code: Alles auswählen

struct Board {
    int wid;
    int hei;
};

void aliasedLoop1(Board * b, int * lol) {
    for(int i = 0; i < b->wid * b->hei; ++i)
        *lol = 123; // This could change b->wid or b->hei!
}
Die Zuweisung zu lol könnte die Dimension des Boards ändern. So absurd der Anwendungsfall für einen Menschen auch aussieht – so lange der Compiler nicht beweisen kann, dass lol nicht b überlappt, darf er das Programmverhalten nicht optimieren. Dementsprechend das Disassembly mit GCC auf voller Optimierung:

Code: Alles auswählen

aliasedLoop1(Board*, int*):
        mov     eax, DWORD PTR [rdi]
        imul    eax, DWORD PTR [rdi+4]
        test    eax, eax
        jle     .L1
        xor     edx, edx
.L3:
        mov     DWORD PTR [rsi], 123
        mov     eax, DWORD PTR [rdi]
        add     edx, 1
        imul    eax, DWORD PTR [rdi+4]    <------ da ist deine Multiplikation
        cmp     eax, edx
        jg      .L3
.L1:
        ret
Ähnlich unoptimiertes Assembly kommt raus, wenn im Funktionsrumpf ein char * geschrieben wird. Logging ist da ein ganz ganz heißer Kandidat:

Code: Alles auswählen

#include <cstring>
char logBuffer[10];
void aliasedLoop2(Board * b) {
    for(int i = 0; i < b->wid * b->hei; ++i)
        if(123 == i)
            strcpy(logBuffer, "at [123]");
}
char darf jeden anderen Typ überlappen, also könnte dein Board ja vielleicht einfach eine andere Ansicht des logBuffer sein? In diesem Aufruf bspw.: aliasedLoop2((Board*)&logBuffer, logBuffer). Würde der Compiler b->wid * b->hei zwischenspeichern, hätte er das Verhalten deines Programms nach der 123. Iteration verändert.

Sehr viel effizienter ist es, sobald du in float schreibst – das kann kein int überlappen:

Code: Alles auswählen

void fastLoop(Board * b, float * lol) {
    for(int i = 0; i < b->wid * b->hei; ++i)
        *lol = 123.f; // cannot change the board - optimizes much better
}
Das kompiliert zu nur halb so vielen Befehlen wie die int-Schleife:

Code: Alles auswählen

fastLoop(Board*, float*):
        mov     eax, DWORD PTR [rdi]
        imul    eax, DWORD PTR [rdi+4]
        test    eax, eax
        jle     .L15
        mov     DWORD PTR [rsi], 0x42f60000
.L15:
        ret
Probier bei solchen Fragen ruhig mit dem Compiler Explorer rum; man lernt eine Menge.

Re: "Temporäre" Variablen - sinnvoll?

Verfasst: 20.05.2024, 22:02
von Alexander Kornrumpf
Wenn du einen trade-off zwischen Lesbarkeit und Laufzeitperformance hast solltest du fast immer auf Lesbarkeit optimieren, aber was das in der Praxis heißt ist schwer zu sagen weil Lesbarkeit subjektiv ist. Das Verhältnis von zusätzlichen Bezeichnern zu Lesbarkeit ist auch ganz sicher nicht monoton, du kannst definitiv - wie du dir sicher leicht selbst überlegen kannst - an den Punkt kommen an dem zusätzliche Bezeichner den Code _weniger_ lesbar machen würden. Nur wo genau dieser Punkt ist, ist halt subjektiv.

So oder so, nimm es mir bitte nicht übel, wenn das Beispiel int cnt = Board->wid * Board->hei; repräsentativ für den Code ist, den du schreibst hast du ein Lesbarkeitsproblem, das völlig orthogonal zu der Frage hier im Thread ist.

Re: "Temporäre" Variablen - sinnvoll?

Verfasst: 20.05.2024, 22:34
von starcow
Uiii :-)! Einmal mehr: Danke Krishty! Sehr eindrücklich deine Aufschlüsselung. Macht wohl auch eindeutig Sinn, sich bei solchen Gelegenheiten mal den Assembler-Code anzuschauen. Werde den Ratschlag bei nächster Gelegenheit gerne beherzigen.
Womöglich macht jetzt der entscheidende Unterschied, ob ich Board als const übergebe - oder gar als restrict Pointer. Werde das Verhalten dazu mal im Compiler-Explorer vergleichen!
Krishty hat geschrieben: 20.05.2024, 19:49 Ist es bei jedem Aufruf gleich, sollte es eh static const sein, damit die Initialisierung beim Kompilieren stattfindet. Dann kostet das Array zur Laufzeit nichts.
Ok, sehr gut!
Alexander Kornrumpf hat geschrieben: 20.05.2024, 22:02 So oder so, nimm es mir bitte nicht übel, wenn das Beispiel int cnt = Board->wid * Board->hei; repräsentativ für den Code ist, den du schreibst hast du ein Lesbarkeitsproblem, das völlig orthogonal zu der Frage hier im Thread ist.
Vielleicht eine blöde Frage, aber ich kann dir gard nicht ganz folgen. Diese Codezeile könnte so durchaus in meiner Funktion stehen.
Findest du denn die Variablen-Namen ungünstig gewählt (wegen den Abkürzungen: cnt für "counter", wid für "width", hei für "height")?
Oder versteh ich dich grad falsch? :-)

Re: "Temporäre" Variablen - sinnvoll?

Verfasst: 23.05.2024, 21:26
von Lord Delvin
Dir fehlt's auch ein bisschen an englischem Sprachgefühl ;)

Versuch's mal mit sowas wie begin/end wenn es in einer Funktion nur diese eine Schleife gibt.
Ich würde, wenn Performance zählt Schleifenbedingungen immer manuell hoisten, weil es einfach ist, das zu optimieren, aber andersrum nicht. Im Kern stimmt Kristys Begründung wie immer. Insbesondere hier. Es ist bei allgemeinem Code aber tausendmal komplizierter als das.

Wenn Performance egal ist, würde ich einfach das aufschreiben, was in meinem Hirn vorgeht.

Ich würde aus Worten niemals Buchstaben entfernen um sie kürzer zu machen. Verwende IDEs, Autocompletion und lerne eine Tastatur effizient zu bedienen, falls das das Problem wäre ;)

Re: "Temporäre" Variablen - sinnvoll?

Verfasst: 30.05.2024, 22:35
von starcow
Ich hab jetzt mal im Compiler Explorer (danke Krishty) einige Experimente angestellt.
Überraschend war für mich:
Selbst bei -O0 berechnet der Compiler einen Ausdruck wie

Code: Alles auswählen

sizeof(array)/sizeof(array[0])
direkt zur Compiletime. Die Division entfällt somit zur Laufzeit. Perfekt!
Lord Delvin hat geschrieben: 23.05.2024, 21:26 Versuch's mal mit sowas wie begin/end wenn es in einer Funktion nur diese eine Schleife gibt.
Sorry fürs Nachhaken an der Stelle - ich versteh hier grad nicht, was du mit "begin" und "end" meinst? Als Bezeichner der Variablen? Anstelle von i?
Lord Delvin hat geschrieben: 23.05.2024, 21:26 Ich würde aus Worten niemals Buchstaben entfernen um sie kürzer zu machen. Verwende IDEs, Autocompletion und lerne eine Tastatur effizient zu bedienen, falls das das Problem wäre ;)
Interessant, dass du das sagst. Ich seh das eigentlich ziemlich oft - auch in sehr prominentem Code (z.B. der stdlib oder auch dem Linux Kernel).
num, pos, sz, x, y, dest, src (um nur mal die Gängisten aufzuzählen).
Ich empfand es eigentlich immer so, dass der Code dadurch an Klarheit gewinnt - weil etwas kompakter und dadurch auch besser zu überschauen.
Ist aber wohl einfach subjektiv - resp. eine Frage des Geschmacks.

Re: "Temporäre" Variablen - sinnvoll?

Verfasst: 31.05.2024, 00:11
von Krishty
C++ bietet std::begin() und std::end(), um Iteratoren auf Anfang und Ende von Dingen wie Arrays und Listen zu bekommen. Du kannst eigene Funktionen/Methoden begin() und end() auf deine Typen definieren, um davon Gebrauch zu machen.

Iteratoren sind oft effizienter als Zählervariablen (du sparst ein Register weil du nur zwei Variablen hast statt drei) und der C++-Standard garantiert bei range-based for, dass end() nur einmal aufgerufen wird.

Code: Alles auswählen

struct Board {
  int wid;
  int hei;
  int * tiles;

  int * begin() { return tiles; }
  int * end() { return tiles + wid * hei; }
};

…
// Set all tiles in the board to 123
for(auto & tile : board)
  tile = 123;
Voller Artikel hier https://en.cppreference.com/w/cpp/language/range-for

Re: "Temporäre" Variablen - sinnvoll?

Verfasst: 31.05.2024, 12:49
von Lord Delvin
Ich begin/end eigentlich als Bezeichner für die Konstanten, die genau das halten, was Krishty in die Funktionsbodies geschoben hat. Mit vergleichbarer Begründung. Wenn man begin()/end() nicht verkackt ist Krishtys Lösung natürlich noch schöner :)

pos und src würde ich auch verwenden, dest gelegentlich. num schon eher nicht. Die Einbuchstabenbezeichner wo sie plausibel sind; das sogar häufig wenn der Scope klein ist.

Ich würde versuchen zu optimieren, wie schnell du den Code in drei Jahren noch lesen kannst. Das braucht natürlich Erfahrung :D