Warum kann der C-Compiler das Ändern des Werts eines konstanten Zeigers nicht optimieren, wenn davon ausgegangen wird, dass zwei Zeiger auf dieselbe Variable illegal/UB wären?

Lesezeit: 8 Minuten

Benutzeravatar von izlin
izlin

Kürzlich bin ich über einen Vergleich zwischen Rust und C gestolpert und sie verwenden den folgenden Code:

bool f(int* a, const int* b) {
  *a = 2;
  int ret = *b;
  *a = 3;
  return ret != 0;
}

In Rust (gleicher Code, aber mit Rust-Syntax) erzeugt es den folgenden Assembler-Code:

    cmp      dword ptr [rsi], 0 
    mov      dword ptr [rdi], 3 
    setne al                    
    ret

Während es mit gcc Folgendes erzeugt:

   mov      DWORD PTR [rdi], 2   
   mov      eax, DWORD PTR [rsi]
   mov      DWORD PTR [rdi], 3        
   test     eax, eax                  
   setne al                           
   ret

Der Text behauptet, dass die C-Funktion die erste Zeile nicht wegoptimieren kann, weil a und b könnte auf die gleiche Nummer zeigen. In Rust ist dies nicht erlaubt, damit der Compiler es wegoptimieren kann.

Nun zu meiner Frage:

Die Funktion dauert a const int* was ein Zeiger auf eine Konstante int ist. Ich habe diese Frage gelesen und sie besagt, dass das Ändern einer Konstante int mit einem Zeiger zu einer Compiler-Warnung und zur schlimmsten Besetzung in UB führen sollte.

Könnte diese Funktion zu einem UB führen, wenn ich sie mit zwei Zeigern auf dieselbe Ganzzahl aufrufe?

Warum kann der C-Compiler nicht die erste Zeile wegoptimieren, unter der Annahme, dass zwei Zeiger auf dieselbe Variable illegal/UB wären?

Link zu Godbolt

  • Erwägen int foo = 0; f(&foo, &foo);. Dies ist vollkommen legales C und es funktioniert wie erwartet, wenn Ihre Funktion zurückkehrt 1.

    – pmg

    1. Februar 2021 um 13:38 Uhr


  • Im Wesentlichen weiß der C-Compiler der Funktion nicht, dass die möglicherweise an ihn übergebene Zeigeradresse bis zur Laufzeit lautet, sodass Sie verwenden müssen restrict um ihm mitzuteilen, dass das Aktualisieren von a nicht den Wert aktualisiert, auf den b zeigt, der in den Vergleich mit 0 einbezogen werden muss, daher muss das Speichern in a, das vor dem Vergleich stattfindet, fortgesetzt werden, während in Rust die Standardannahme ist beschränken

    – Lewis Kelsey

    28. Februar 2021 um 17:21 Uhr


Benutzeravatar von Morten Jensen
Morten Jensen

Warum kann der C-Compiler nicht die erste Zeile wegoptimieren, unter der Annahme, dass zwei Zeiger auf dieselbe Variable illegal/UB wären?

Weil Sie den C-Compiler nicht dazu angewiesen haben, dass er diese Annahme machen darf.

C hat einen Typqualifizierer für genau diesen Namen restrict was ungefähr bedeutet: dieser Zeiger überschneidet sich nicht mit anderen Zeigern (nicht exaktaber spiel mit).

Die Assembly-Ausgabe für

bool f(int* restrict a, const int* b) {
  *a = 2;
  int ret = *b;
  *a = 3;
  return ret != 0;
}

ist

        mov     eax, DWORD PTR [rsi]
        mov     DWORD PTR [rdi], 3
        test    eax, eax
        setne   al
        ret

… was die Zuordnung entfernt/optimiert *a = 2

Aus https://en.wikipedia.org/wiki/Restrict

In der Programmiersprache C ist „restrict“ ein Schlüsselwort, das in Zeigerdeklarationen verwendet werden kann. Durch Hinzufügen dieses Typqualifizierers weist ein Programmierer den Compiler darauf hin, dass für die Lebensdauer des Zeigers nur der Zeiger selbst oder ein direkt davon abgeleiteter Wert (z. B. Zeiger + 1) verwendet wird, um auf das Objekt zuzugreifen, auf das er zeigt.

  • Genau genommen sind das alle Rust-Referenzen restrictsogar die gemeinsam genutzten — also das Äquivalent von Aiden4s Rust-Code wäre bool f(int * restrict a, const int* restrict b). Wikipedia unterlässt es, zwei Fakten darüber zu erwähnen restrict die dies funktionieren lassen: erstens, dass nur die Single-Pointer-Zugriffsregel gilt wenn das Objekt hinter dem Mauszeiger geändert wird (Also b kann beides sein restrict und Alias, weil *b wird nicht geändert); und zweitens, dass a const-qualifiziert restrict Zeiger darf nicht zeigen auf etwas, das geändert wird (im Gegensatz zu normalen const Zeiger). (C99 6.7.3.1p4)

    – Trient

    2. Februar 2021 um 21:02 Uhr

  • @trentcl: A const restrict Der Zeiger kann auf modifizierte Objekte zeigen, wenn er nicht verwendet wird, um auf einen der modifizierten Teile des Objekts zuzugreifen. Zum Beispiel gegeben void test(int * restrict p, int const * restrict q)könnte Code schreiben p[0] und beide lesen p[1] und q[1]sofern es nie gelesen wird q[0]noch (für clang oder gcc) testet einen eingeschränkt qualifizierten Zeiger auf Gleichheit mit etwas, das nicht davon abgeleitet ist.

    – Superkatze

    2. Februar 2021 um 22:45 Uhr


Benutzeravatar von pmg
pmg

Die Funktion int f(int *a, const int *b); verspricht, den Inhalt nicht zu ändern b durch diesen Zeiger… Es macht keine Versprechungen bezüglich des Zugriffs auf Variablen durch die a Zeiger.

Wenn a und b Zeigen Sie auf dasselbe Objekt und ändern Sie es durch a ist legal (vorausgesetzt natürlich, das zugrunde liegende Objekt ist modifizierbar).

Beispiel:

int val = 0;
f(&val, &val);

  • Vor allem ändern *b durch b (Nach dem Wegwerfen constNess) würde Auch legal sein C. const ist im Wesentlichen ein Fussel für den Programmierer, kein Hinweis für den Compiler – C darf Aufrufe nicht optimieren f unter der Annahme, dass es sich nicht ändert *b. restrict-qualifizierte Zeiger sind eine andere Sache (siehe Morten Jensens Antwort und meinen Kommentar dort).

    – Trient

    2. Februar 2021 um 21:07 Uhr

  • @trentcl: Ich bin mir nicht sicher, ob du „Hinweis“ oder tatsächlich „Flusen“ gemeint hast, aber es funktioniert so oder so.

    – Eric Postpischil

    3. Februar 2021 um 0:26 Uhr

Benutzeravatar von Aiden4
Aiden4

Während die anderen Antworten die C-Seite erwähnen, lohnt es sich dennoch, einen Blick auf die Rust-Seite zu werfen. Mit Rust ist der Code, den Sie haben, wahrscheinlich dieser:

fn f(a:&mut i32, b:&i32)->bool{
    *a = 2;
    let ret = *b;
    *a = 3;
    return ret != 0;
}

Die Funktion akzeptiert zwei Referenzen, eine änderbar, eine nicht. Referenzen sind Zeiger, die garantiert für Lesevorgänge gültig sind, und veränderliche Referenzen sind ebenfalls garantiert eindeutig, sodass sie optimiert werden

        cmp     dword ptr [rsi], 0
        mov     dword ptr [rdi], 3
        setne   al
        ret

Rust hat jedoch auch rohe Zeiger, die den Zeigern von C entsprechen und keine solchen Garantien geben. Die folgende Funktion, die Rohzeiger annimmt:

unsafe fn g(a:*mut i32, b:*const i32)->bool{
    *a = 2;
    let ret = *b;
    *a = 3;
    return ret != 0;
}

verpasst die Optimierung und kompiliert dazu:

        mov     dword ptr [rdi], 2
        cmp     dword ptr [rsi], 0
        mov     dword ptr [rdi], 3
        setne   al
        ret

Godbolt-Link

  • Sie vergleichen also Äpfel mit Birnen. Schön! +1 : D

    – Paul Evans

    3. Februar 2021 um 11:58 Uhr

  • @PaulEvans Um fair zu sein, als C-Fan ist es eine Schande, dass C keine Referenzen hat. Der Wechsel zu C++ und die Verwendung von Referenzen hat Teile meines Codes um nicht triviale Mengen beschleunigt, während er einfach zu lesen und zu argumentieren ist.

    – NervigC

    3. Februar 2021 um 16:11 Uhr

  • @AnnoyinC Ich bin hauptsächlich ein C ++ – Programmierer, verwende also offensichtlich ständig Referenzen. Aber es gibt nur syntaktischen Zucker für Zeiger, die an sich keine Geschwindigkeitsvorteile bieten. Was wirklich toll ist, sind intelligente Zeiger! Nicht für Schnelligkeit, sondern für Korrektheit. 😀

    – Paul Evans

    3. Februar 2021 um 17:00 Uhr


  • @PaulEvans Kein Sprachanwalt, daher kann ich tatsächlich falsch liegen, aber ich denke nicht, dass Referenzen syntaktischer Zucker für Zeiger sind.

    – Franko Leon Tokalić

    3. Februar 2021 um 20:11 Uhr

  • @FrankoLeonTokalić So ziemlich, ja. Sie können eine Referenz nicht neu setzen (wie bei einer Nicht-const Zeiger) und muss sich auf ein Objekt beziehen (im Gegensatz zu einem Zeiger, der auf gesetzt ist nullptr), aber unter der Haube ist es nur ein Zeiger, den Sie nicht dereferenzieren, um auf das zuzugreifen, worauf gezeigt wird.

    – Paul Evans

    3. Februar 2021 um 20:33 Uhr


Die Funktion dauert a const int* was ein Zeiger auf eine Konstante int ist.

Nein, const int* ist kein Zeiger auf eine Konstante int. Wer das sagt, täuscht sich.

  • int* ist ein Zeiger auf ein int, das definitiv nicht konstant ist.

  • const int* ist ein Zeiger auf ein Int mit unbekannter Konstanz.

  • Es gibt keine Möglichkeit, die Vorstellung eines Zeigers auf ein int auszudrücken, das definitiv eine Konstante ist.

Wenn C eine besser gestaltete Sprache wäre, dann const int * wäre ein Zeiger auf eine Konstante int, mutable int * (entlehnt ein Schlüsselwort aus C++) wäre ein Zeiger auf ein nicht konstantes int, und int * wäre ein Zeiger auf ein Int mit unbekannter Konstanz. Das Weglassen der Qualifizierer (dh etwas über den Typ, auf den gezeigt wird, zu vergessen) wäre ungefährlich – das Gegenteil von echtem C in which hinzufügen das const Qualifikation ist sicher. Ich habe Rust nicht verwendet, aber aus Beispielen in einer anderen Antwort geht hervor, dass es eine solche Syntax verwendet.

Bjarne Stroustrup, der vorstellte constnannte es ursprünglich readonlywas seiner eigentlichen Bedeutung viel näher kommt. int readonly* hätte deutlicher gemacht, dass der Zeiger schreibgeschützt ist, nicht das Objekt, auf das gezeigt wird. Die Umbenennung in const hat Generationen von Programmierern verwirrt.

Wenn ich die Wahl habe, schreibe ich immer foo const*nicht const foo*als das Nächstbeste readonly*.

Benutzeravatar von Lewis Kelsey
Lewis Kelsey

Es sei darauf hingewiesen, dass es bei dieser Frage um Optimierung geht -Ofast und wie es dort sogar der Fall ist.

Im Wesentlichen kennt der C-Compiler der Funktion nicht den vollständigen diskreten Satz von Adressen, die an ihn übergeben werden könnten, da dieser bis zur Verbindungszeit / Laufzeit nicht bekannt ist, da die Funktion von mehreren Übersetzungseinheiten aufgerufen werden kann, und daher Überlegungen anstellt die jede legale Adresse behandeln, die a und b darauf hindeuten könnte, und das schließt natürlich den Fall ein, in dem sie sich überschneiden.

Daher müssen Sie verwenden restrict um ihm diese Aktualisierung zu sagen a (was die Funktion erlaubt, weil es kein Zeiger auf const ist, aber selbst dann könnte die Funktion const ablegen) aktualisiert den Wert nicht b zeigt auf, was in den Vergleich mit 0 einbezogen werden muss, also das Speichern auf a Dies geschieht, bevor der Vergleich fortgesetzt werden muss, während bei Rust die Standardannahme restriktiv ist. Der Compiler der Funktion weiß das aber *a ist das gleiche wie *(a+1-1) und wird daher keine 2 separaten Geschäfte produzieren, weiß aber nicht, ob a oder b Überlappung.

1414610cookie-checkWarum kann der C-Compiler das Ändern des Werts eines konstanten Zeigers nicht optimieren, wenn davon ausgegangen wird, dass zwei Zeiger auf dieselbe Variable illegal/UB wären?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy