Warum optimieren C ++ - Compiler diese bedingte boolesche Zuweisung nicht als bedingungslose Zuweisung?

117

Betrachten Sie die folgende Funktion:

void func(bool& flag)
{
    if(!flag) flag=true;
}

Es scheint mir, dass wenn ein Flag einen gültigen booleschen Wert hat, dies einer bedingungslosen Einstellung truewie folgt entspricht:

void func(bool& flag)
{
    flag=true;
}

Weder gcc noch clang optimieren es auf diese Weise - beide generieren auf -O3Optimierungsebene Folgendes :

_Z4funcRb:
.LFB0:
    .cfi_startproc
    cmp BYTE PTR [rdi], 0
    jne .L1
    mov BYTE PTR [rdi], 1
.L1:
    rep ret

Meine Frage ist: Ist der Code nur zu speziell, um ihn zu optimieren, oder gibt es gute Gründe, warum eine solche Optimierung unerwünscht wäre, da dies flagkein Verweis auf ist volatile? Es scheint der einzige Grund zu sein, der sein könnte, dass er zum Zeitpunkt des Lesens flageinen Nicht- trueoder- falseWert ohne undefiniertes Verhalten haben könnte, aber ich bin mir nicht sicher, ob dies möglich ist.

Ruslan
quelle
8
Haben Sie Beweise dafür, dass es sich um eine "Optimierung" handelt?
David Schwartz
1
@ 200_success Ich denke nicht, dass es eine gute Sache ist, eine Codezeile mit nicht funktionierendem Markup als Titel zu setzen. Wenn Sie einen spezifischeren Titel wünschen, ist dies in Ordnung, aber wählen Sie einen englischen Satz und versuchen Sie, darin enthaltenen Code zu vermeiden (z. B. warum optimieren Compiler bedingte Schreibvorgänge nicht in bedingungslose Schreibvorgänge, wenn sie nachweisen können, dass sie gleichwertig sind? Oder ähnlich). Da Backticks nicht gerendert werden, sollten Sie sie auch dann nicht im Titel verwenden, wenn Sie Code verwenden.
Bakuriu
2
@Ruslan scheint diese Optimierung für die Funktion selbst nicht durchzuführen, aber wenn sie den Code einbinden kann, scheint dies für die Inline-Version der Fall zu sein. Oft führt dies nur zu einer Kompilierungszeitkonstante der 1Verwendung. godbolt.org/g/swe0tc
Evan Teran

Antworten:

102

Dies kann sich aufgrund von Überlegungen zur Cache-Kohärenz negativ auf die Leistung des Programms auswirken . Das Schreiben in flagjedes Mal func(), wenn aufgerufen wird, würde die enthaltene Cache-Zeile verschmutzen. Dies geschieht unabhängig davon, dass der zu schreibende Wert genau mit den Bits übereinstimmt, die vor dem Schreiben an der Zieladresse gefunden wurden.


BEARBEITEN

hvd hat einen weiteren guten Grund geliefert , der eine solche Optimierung verhindert. Es ist ein überzeugenderes Argument gegen die vorgeschlagene Optimierung, da es zu undefiniertem Verhalten führen kann, während meine (ursprüngliche) Antwort nur Leistungsaspekte betraf.

Nach etwas mehr Überlegung kann ich ein weiteres Beispiel vorschlagen, warum Compiler - sofern sie nicht nachweisen können, dass die Transformation für einen bestimmten Kontext sicher ist - von der Einführung des bedingungslosen Schreibens stark ausgeschlossen werden sollten. Betrachten Sie diesen Code:

const bool foo = true;

int main()
{
    func(const_cast<bool&>(foo));
}

Bei einem bedingungslosen Schreibvorgang func()wird definitiv ein undefiniertes Verhalten ausgelöst (das Schreiben in den Nur-Lese-Speicher beendet das Programm, selbst wenn der Effekt des Schreibvorgangs ansonsten ein No-Op wäre).

Leon
quelle
7
Dies kann sich auch positiv auf die Leistung auswirken, da Sie einen Zweig entfernen. Daher halte ich diesen speziellen Fall nicht für sinnvoll, um ihn ohne ein ganz bestimmtes System zu diskutieren.
Lundin
3
Die @ Yakk-Verhaltensdefinition wird von der Zielplattform nicht beeinflusst. Zu sagen, dass das Programm beendet wird, ist falsch, aber die UB selbst kann weitreichende Konsequenzen haben, einschließlich Nasendämonen.
John Dvorak
16
@Yakk Das hängt davon ab, was man unter "Nur-Lese-Speicher" versteht. Nein, es befindet sich nicht in einem ROM-Chip, aber es befindet sich sehr oft in einem Abschnitt, der auf eine Seite geladen ist, für die kein Schreibzugriff aktiviert ist, und Sie erhalten beispielsweise ein SIGSEGV-Signal oder eine STATUS_ACCESS_VIOLATION-Ausnahme, wenn Sie versuchen, darauf zu schreiben.
Random832
5
"Dies löst definitiv undefiniertes Verhalten aus". Nein. Undefiniertes Verhalten ist eine Eigenschaft der abstrakten Maschine. Es ist das, was der Code sagt, das bestimmt, ob UB vorhanden ist. Compiler können dies nicht verursachen (obwohl ein Compiler bei einem Fehler dazu führen kann, dass sich Programme falsch verhalten).
Eric M Schmidt
7
Es ist das Wegwerfen consteiner Funktion , die die Daten ändern kann , die die Quelle des undefinierten Verhaltens sind, nicht das bedingungslose Schreiben. Doktor, es tut weh, wenn ich das mache ...
Spencer
48

Abgesehen von Leons Antwort auf die Leistung:

Angenommen, flagist true. Angenommen, zwei Threads rufen ständig auf func(flag). Die geschriebene Funktion speichert in diesem Fall nichts in flag, daher sollte dies threadsicher sein. Zwei Threads greifen auf denselben Speicher zu, jedoch nur zum Lesen. Die bedingungslose Einstellung flagauf truebedeutet, dass zwei verschiedene Threads in denselben Speicher schreiben. Dies ist nicht sicher, dies ist unsicher, selbst wenn die zu schreibenden Daten mit den bereits vorhandenen Daten identisch sind.


quelle
9
Ich denke, das ist ein Ergebnis der Bewerbung [intro.races]/21.
Griwes
10
Sehr interessant. Ich habe das so gelesen: Der Compiler darf niemals eine Schreiboperation "optimieren", bei der die abstrakte Maschine keine hätte.
Martin Ba
3
@ MartinBa Meistens. Wenn der Compiler jedoch nachweisen kann, dass dies keine Rolle spielt, beispielsweise weil er nachweisen kann, dass möglicherweise kein anderer Thread Zugriff auf diese bestimmte Variable hat, ist dies möglicherweise in Ordnung.
13
Dies ist nur dann unsicher, wenn das System, auf das der Compiler abzielt, es unsicher macht . Ich habe noch nie ein System entwickelt, bei dem das Schreiben 0x01in ein Byte bereits 0x01"unsicheres" Verhalten verursacht. Auf einem System mit Wort- oder Wortspeicherzugriff würde es; Der Optimierer sollte sich dessen jedoch bewusst sein. Auf einem modernen PC oder Telefonbetriebssystem tritt kein Problem auf. Das ist also kein triftiger Grund.
Yakk - Adam Nevraumont
4
@Yakk Eigentlich, wenn ich noch mehr nachdenke, denke ich, dass dies doch richtig ist, selbst für gewöhnliche Prozessoren. Ich denke, Sie haben Recht, wenn die CPU direkt in den Speicher schreiben kann, aber nehmen wir an, sie flagbefindet sich auf einer Copy-on-Write-Seite. Jetzt kann auf CPU-Ebene das Verhalten definiert werden (Seitenfehler, lassen Sie das Betriebssystem damit umgehen), aber auf Betriebssystemebene ist es möglicherweise immer noch undefiniert, oder?
13

Ich bin mir über das Verhalten von C ++ hier nicht sicher, aber in C kann sich der Speicher ändern, denn wenn der Speicher einen anderen Wert ungleich Null als 1 enthält, bleibt er bei der Prüfung unverändert, wird jedoch bei der Prüfung auf 1 geändert.

Da ich C ++ nicht sehr fließend beherrsche, weiß ich nicht, ob diese Situation überhaupt möglich ist.

glglgl
quelle
Wäre das noch wahr _Bool?
Ruslan
5
Wenn der Speicher in C einen Wert enthält, von dem der ABI nicht sagt, dass er für seinen Typ gültig ist, handelt es sich um eine Trap-Darstellung, und das Lesen einer Trap-Darstellung ist ein undefiniertes Verhalten. In C ++ kann dies nur passieren, wenn ein nicht initialisiertes Objekt gelesen wird und ein nicht initialisiertes Objekt, das UB ist. Wenn Sie jedoch einen ABI finden, der besagt, dass ein Wert ungleich Null für Typ bool/ gültig ist_Bool und Mittel ist true, dann haben Sie in diesem bestimmten ABI wahrscheinlich Recht.
1
@Ruslan Bei Compilern, die Itanium ABI verwenden, und auf ARM-Prozessoren sind C _Boolund C ++ boolentweder vom gleichen Typ oder kompatible Typen, die denselben Regeln folgen. Mit MSVC haben sie die gleiche Größe und Ausrichtung, aber es gibt keine offizielle Aussage darüber, ob sie die gleichen Regeln verwenden.
Justin Time - Stellen Sie Monica
1
@JustinTime: Cs <stdbool.h>enthält ein typedef _Bool bool; Und ja, auf x86 (zumindest im System V ABI) muss bool/ _Boolentweder 0 oder 1 sein, wobei die oberen Bits des Bytes gelöscht sind. Ich halte diese Erklärung nicht für plausibel.
Peter Cordes
1
@JustinTime: Das stimmt, ich hätte nur darauf hinweisen sollen, dass es definitiv die gleiche Semantik in allen x86-Varianten des System V ABI hat, worum es bei dieser Frage ging. (Ich kann sagen, dass das erste Argument funcin RDI übergeben wurde, während Windows RDX verwenden würde).
Peter Cordes