Warum optimiert der Rust-Compiler den Code nicht unter der Annahme, dass zwei veränderbare Referenzen keinen Alias ​​haben können?

301

Soweit ich weiß, kann Referenz- / Zeiger-Aliasing die Fähigkeit des Compilers beeinträchtigen, optimierten Code zu generieren, da sie sicherstellen müssen, dass sich die generierte Binärdatei korrekt verhält, wenn die beiden Referenzen / Zeiger tatsächlich Alias ​​sind. Zum Beispiel im folgenden C-Code:

void adds(int  *a, int *b) {
    *a += *b;
    *a += *b;
}

Wenn es clang version 6.0.0-1ubuntu2 (tags/RELEASE_600/final)mit dem -O3Flag kompiliert wird , wird es ausgegeben

0000000000000000 <adds>:
   0:    8b 07                    mov    (%rdi),%eax
   2:    03 06                    add    (%rsi),%eax
   4:    89 07                    mov    %eax,(%rdi)  # The first time
   6:    03 06                    add    (%rsi),%eax
   8:    89 07                    mov    %eax,(%rdi)  # The second time
   a:    c3                       retq

Hier speichert der Code (%rdi)zweimal in case int *aund int *balias.

Wenn wir dem Compiler ausdrücklich mitteilen, dass diese beiden Zeiger keinen Alias ​​mit dem restrictSchlüsselwort haben können:

void adds(int * restrict a, int * restrict b) {
    *a += *b;
    *a += *b;
}

Dann wird Clang eine optimierte Version des Binärcodes ausgeben:

0000000000000000 <adds>:
   0:    8b 06                    mov    (%rsi),%eax
   2:    01 c0                    add    %eax,%eax
   4:    01 07                    add    %eax,(%rdi)
   6:    c3                       retq

Da Rust (außer in unsicherem Code) sicherstellt, dass zwei veränderbare Referenzen keinen Alias ​​haben können, würde ich denken, dass der Compiler in der Lage sein sollte, die optimierte Version des Codes auszugeben.

Als ich mit dem Code testen unten und kompilieren Sie es mit rustc 1.35.0mit -C opt-level=3 --emit obj,

#![crate_type = "staticlib"]
#[no_mangle]
fn adds(a: &mut i32, b: &mut i32) {
    *a += *b;
    *a += *b;
}

es erzeugt:

0000000000000000 <adds>:
   0:    8b 07                    mov    (%rdi),%eax
   2:    03 06                    add    (%rsi),%eax
   4:    89 07                    mov    %eax,(%rdi)
   6:    03 06                    add    (%rsi),%eax
   8:    89 07                    mov    %eax,(%rdi)
   a:    c3                       retq

Dies nutzt die Garantie nicht aus aund bkann nicht alias.

Liegt das daran, dass sich der aktuelle Rust-Compiler noch in der Entwicklung befindet und noch keine Alias-Analyse für die Optimierung integriert hat?

Liegt das daran, dass es immer noch eine Chance gibt aund bAlias ​​auch in sicherem Rust sein könnte?

Zhiyao
quelle
3
godbolt.org/z/aEDINX , seltsam
Stargateur
76
Nebenbemerkung: " Da Rust (außer in unsicherem Code) sicherstellt, dass zwei veränderbare Referenzen keinen Alias ​​haben " - ist es erwähnenswert, dass das unsafeAliasing veränderlicher Verweise auch im Code nicht zulässig ist und zu undefiniertem Verhalten führt. Sie können Aliasing-Rohzeiger verwenden, aber mit unsafeCode können Sie die Rust-Standardregeln nicht ignorieren. Es ist nur ein weit verbreitetes Missverständnis und daher erwähnenswert.
Lukas Kalbertodt
6
Es hat eine Weile gedauert, bis ich herausgefunden habe, worum es in dem Beispiel geht, weil ich nicht in der Lage bin, asm zu lesen. Falls es also jemand anderem hilft: Es läuft darauf hinaus, ob die beiden +=Operationen im Körper von addsals neu interpretiert werden können *a = *a + *b + *b. Wenn die Zeiger keinen Alias ​​haben, können Sie sogar b* + *bin der zweiten ASM-Liste sehen, was sie bedeuten : 2: 01 c0 add %eax,%eax. Wenn sie jedoch einen Alias ​​verwenden, können sie dies nicht, da er *bzum zweiten Mal einen anderen Wert enthält als beim ersten Mal (den Wert, den Sie online 4:in der ersten ASM-Liste speichern ).
dlukes

Antworten:

364

Rust ursprünglich tat das LLVM ermöglichen noaliasAttribut, aber dieses verursacht miscompiled Code . Wenn alle unterstützten LLVM-Versionen den Code nicht mehr falsch kompilieren, wird er wieder aktiviert .

Wenn Sie -Zmutable-noalias=yesden Compileroptionen hinzufügen , erhalten Sie die erwartete Assembly:

adds:
        mov     eax, dword ptr [rsi]
        add     eax, eax
        add     dword ptr [rdi], eax
        ret

Einfach ausgedrückt, Rust hat das Äquivalent des restrictSchlüsselworts von C überall platziert , weitaus häufiger als jedes übliche C-Programm. Dies übte Eckfälle von LLVM mehr aus, als es richtig handhaben konnte. Es stellt sich heraus, dass C- und C ++ - Programmierer einfach nicht restrictso häufig verwenden wie &mutin Rust.

Dies ist mehrfach passiert .

  • Rost 1.0 bis 1.7 - noaliasaktiviert
  • Rost 1.8 bis 1.27 - noaliasdeaktiviert
  • Rost 1.28 bis 1.29 - noaliasaktiviert
  • Rost 1.30 bis ??? - noaliasdeaktiviert

Verwandte Rostprobleme

Shepmaster
quelle
12
Das ist nicht überraschend. Ungeachtet seiner weitreichenden Behauptungen der Mehrsprachigkeit wurde LLVM speziell als C ++ - Backend entwickelt und hatte immer eine starke Tendenz, an Dingen zu ersticken, die nicht genug wie C ++ aussehen.
Mason Wheeler
47
@MasonWheeler Wenn Sie sich durch einige der Probleme klicken, finden Sie C-Codebeispiele, die restrictsowohl Clang als auch GCC verwenden und falsch kompilieren. Es ist nicht auf Sprachen beschränkt, die nicht „C ++ genug“ sind, es sei denn, Sie zählen C ++ selbst in dieser Gruppe .
Shepmaster
6
@MasonWheeler: Ich glaube nicht, dass LLVM wirklich nach den Regeln von C oder C ++ entwickelt wurde, sondern nach den Regeln von LLVM. Es werden Annahmen getroffen, die normalerweise für C- oder C ++ - Code gelten, aber soweit ich das beurteilen kann, basiert das Design auf einem statischen Datenabhängigkeitsmodell, das keine schwierigen Eckfälle verarbeiten kann. Das wäre in Ordnung, wenn pessimistisch Datenabhängigkeiten angenommen würden, die nicht widerlegt werden können, sondern als No-Ops-Aktionen behandelt werden, die Speicher mit demselben Bitmuster wie sie schreiben würden und die potenzielle, aber nicht nachweisbare Datenabhängigkeiten von der haben lesen und schreiben.
Supercat
8
@supercat Ich habe Ihre Kommentare ein paar Mal gelesen, aber ich gebe zu, dass ich ratlos bin - ich habe keine Ahnung, was sie mit dieser Frage oder Antwort zu tun haben. Undefiniertes Verhalten kommt hier nicht ins Spiel, dies ist "nur" ein Fall von mehreren Optimierungsdurchläufen, die schlecht miteinander interagieren.
Shepmaster
2
@avl_sweden zu wiederholen, es ist nur ein Fehler . Der Optimierungsschritt zum Abrollen der Schleife hat (nicht?) noaliasZeiger bei der Ausführung nicht vollständig berücksichtigt. Es wurden neue Zeiger basierend auf Eingabezeigern erstellt, wobei das noaliasAttribut nicht ordnungsgemäß kopiert wurde , obwohl die neuen Zeiger einen Alias ​​hatten.
Shepmaster