Realistische Verwendung des C99-Schlüsselworts „restrict“?

Lesezeit: 10 Minuten

Benutzeravatar von user90052
Benutzer90052

Ich habe einige Dokumentationen und Fragen/Antworten durchgesehen und gesehen, dass es erwähnt wurde. Ich habe eine kurze Beschreibung gelesen, die besagt, dass es im Grunde ein Versprechen des Programmierers wäre, dass der Zeiger nicht verwendet wird, um auf etwas anderes zu zeigen.

Kann jemand einige realistische Fälle anbieten, in denen es sich lohnt, dies tatsächlich zu verwenden?

  • memcpy vs memmove ist ein kanonisches Beispiel.

    – Alexander C.

    19. April 2016 um 18:22 Uhr

  • @AlexandreC.: Ich denke nicht, dass es besonders anwendbar ist, da das Fehlen eines “Einschränken” -Qualifizierers nicht bedeutet, dass die Programmlogik mit dem Überladen von Quelle und Ziel funktioniert, und das Vorhandensein eines solchen Qualifizierers eine aufgerufene Methode nicht verhindern würde Bestimmen, ob sich Quelle und Ziel überschneiden, und wenn ja, Ersetzen von dest durch src+(dest-src), das, da es von src abgeleitet ist, es als Alias ​​verwenden könnte.

    – Superkatze

    19. April 2016 um 22:25 Uhr

  • @supercat: Deshalb habe ich es als Kommentar geschrieben. Allerdings 1) restrict-Qualifizierende Argumente zu memcpy ermöglicht im Prinzip eine aggressive Optimierung einer naiven Implementierung und 2) ein bloßes Aufrufen memcpy ermöglicht es dem Compiler anzunehmen, dass die ihm übergebenen Argumente keinen Alias ​​haben, was eine gewisse Optimierung um die herum ermöglichen könnte memcpy Anruf.

    – Alexander C.

    20. April 2016 um 6:50 Uhr

  • @AlexandreC.: Es wäre für einen Compiler auf den meisten Plattformen sehr schwierig, ein naives Memcpy – selbst mit “restrict” – so zu optimieren, dass es annähernd so effizient ist wie eine auf das Ziel zugeschnittene Version. Aufrufseitige Optimierungen würden das Schlüsselwort „restrict“ nicht erfordern, und in einigen Fällen können Bemühungen, diese zu erleichtern, kontraproduktiv sein. Beispielsweise könnten viele Implementierungen von memcpy ohne zusätzliche Kosten betrachtet werden memcpy(anything, anything, 0); als No-Op, und stellen Sie sicher, dass wenn p ist ein Hinweis auf zumindest n beschreibbare Bytes, memcpy(p,p,n); wird keine nachteiligen Nebenwirkungen haben. Solche Fälle können vorkommen…

    – Superkatze

    20. April 2016 um 15:28 Uhr


  • … natürlich in bestimmten Arten von Anwendungscode (z. B. eine Sortierroutine, die ein Element mit sich selbst austauscht) und in Implementierungen, in denen sie keine nachteiligen Nebenwirkungen haben, kann es effizienter sein, diese Fälle vom Code für allgemeine Fälle behandeln zu lassen, als es zu tun Sonderfalltests hinzuzufügen. Leider scheinen einige Compiler-Autoren zu denken, dass es besser ist, von Programmierern Code hinzuzufügen, den der Compiler möglicherweise nicht optimieren kann, um “Optimierungsmöglichkeiten” zu erleichtern, die Compiler ohnehin sehr selten ausnutzen würden.

    – Superkatze

    20. April 2016 um 15:33 Uhr

Michaels Benutzeravatar
Michael

restrict besagt, dass der Zeiger das einzige ist, was auf das zugrunde liegende Objekt zugreift. Es eliminiert das Potenzial für Zeiger-Aliasing und ermöglicht eine bessere Optimierung durch den Compiler.

Angenommen, ich habe eine Maschine mit speziellen Anweisungen, die Vektoren von Zahlen im Speicher multiplizieren kann, und ich habe den folgenden Code:

void MultiplyArrays(int* dest, int* src1, int* src2, int n)
{
    for(int i = 0; i < n; i++)
    {
        dest[i] = src1[i]*src2[i];
    }
}

Der Compiler muss if richtig handhaben dest, src1und src2 sich überlappen, was bedeutet, dass von Anfang bis Ende jeweils eine Multiplikation durchgeführt werden muss. Indem restrictkann der Compiler diesen Code mithilfe der Vektoranweisungen optimieren.

Wikipedia hat einen Eintrag auf restrictmit einem anderen Beispiel, hier.

  • @Michael – Wenn ich mich nicht irre, dann wäre das Problem nur wann dest einen der Quellvektoren überlappt. Warum sollte es ein Problem geben, wenn src1 und src2 Überlappung?

    – jap

    25. März 2015 um 17:53 Uhr

  • Einschränken hat normalerweise nur eine Wirkung, wenn es auf ein modifiziertes Objekt zeigt, in welchem ​​Fall es behauptet, dass keine versteckten Seiteneffekte berücksichtigt werden müssen. Die meisten Compiler verwenden es, um die Vektorisierung zu erleichtern. Msvc verwendet zu diesem Zweck eine Laufzeitprüfung auf Datenüberlappung.

    – Tim18

    21. Mai 2016 um 18:24 Uhr

  • Eigentlich ist das Schlüsselwort register nur beratend. Und in Compilern seit etwa dem Jahr 2000 wird das i (und das n für den Vergleich) im Beispiel in ein Register optimiert, unabhängig davon, ob Sie das Schlüsselwort register verwenden oder nicht.

    – Markus Fischler

    7. März 2018 um 19:47 Uhr

  • Für alle anderen wie mich, die sich fragen, was in aller Welt “Zeiger-Aliasing” bedeutet, schreibt die Cornell University hier darüber: cvw.cac.cornell.edu/vector/coding_aliasing.

    – Gabriel Staples

    24. Juni 2021 um 20:10 Uhr


  • @MarkFischler: Zumindest beim Targeting von ARM-Zielen nutzt GCC immer noch die register Schlüsselwort bei der Verwendung -O0. In einigen Fällen ermöglicht dieses Schlüsselwort dem Compiler, Code zu generieren -O0 Dies wäre genauso effizient (manchmal effizienter!) Wie auf einer höheren Optimierungsstufe, wenn Dinge wie das Auf- und Abrüsten von Stack-Frames bei Blattfunktionen weggelassen würden, die den Stack nicht verwenden.

    – Superkatze

    8. September 2021 um 16:33 Uhr

Ciro Santilli Benutzeravatar von OurBigBook.com
Ciro Santilli OurBigBook.com

Das Wikipedia-Beispiel ist sehr erhellend.

Es zeigt deutlich wie Es ermöglicht das Speichern einer Montageanleitung.

Ohne Einschränkung:

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

Pseudo-Assembly:

load R1 ← *x    ; Load the value of x pointer
load R2 ← *a    ; Load the value of a pointer
add R2 += R1    ; Perform Addition
set R2 → *a     ; Update the value of a pointer
; Similarly for b, note that x is loaded twice,
; because x may point to a (a aliased by x) thus 
; the value of x will change when the value of a
; changes.
load R1 ← *x
load R2 ← *b
add R2 += R1
set R2 → *b

Mit Einschränkung:

void fr(int *restrict a, int *restrict b, int *restrict x);

Pseudo-Assembly:

load R1 ← *x
load R2 ← *a
add R2 += R1
set R2 → *a
; Note that x is not reloaded,
; because the compiler knows it is unchanged
; "load R1 ← *x" is no longer needed.
load R2 ← *b
add R2 += R1
set R2 → *b

Tut GCC das wirklich?

GCC 4.8 Linux x86-64:

gcc -g -std=c99 -O0 -c main.c
objdump -S main.o

Mit -O0Sie sind gleich.

Mit -O3:

void f(int *a, int *b, int *x) {
    *a += *x;
   0:   8b 02                   mov    (%rdx),%eax
   2:   01 07                   add    %eax,(%rdi)
    *b += *x;
   4:   8b 02                   mov    (%rdx),%eax
   6:   01 06                   add    %eax,(%rsi)  

void fr(int *restrict a, int *restrict b, int *restrict x) {
    *a += *x;
  10:   8b 02                   mov    (%rdx),%eax
  12:   01 07                   add    %eax,(%rdi)
    *b += *x;
  14:   01 06                   add    %eax,(%rsi) 

Für den Uneingeweihten, die Berufungskonvention ist:

  • rdi = erster Parameter
  • rsi = zweiter Parameter
  • rdx = dritter Parameter

Die GCC-Ausgabe war noch klarer als der Wiki-Artikel: 4 Anweisungen vs. 3 Anweisungen.

Arrays

Bisher haben wir Einsparungen bei einzelnen Anweisungen, aber wenn Zeiger Arrays darstellen, die durchlaufen werden sollen, ein häufiger Anwendungsfall, dann könnten eine Reihe von Anweisungen eingespart werden, wie von Supercat erwähnt.

Betrachten Sie zum Beispiel:

void f(char *restrict p1, char *restrict p2) {
    for (int i = 0; i < 50; i++) {
        p1[i] = 4;
        p2[i] = 9;
    }
}

Durch restrictein intelligenter Compiler (oder Mensch), könnte dies optimieren zu:

memset(p1, 4, 50);
memset(p2, 9, 50);

was potenziell viel effizienter ist, da es auf einer anständigen libc-Implementierung (wie glibc) für die Assemblierung optimiert werden kann: Ist es in Bezug auf die Leistung besser, std::memcpy() oder std::copy() zu verwenden?

Tut GCC das wirklich?

GCC 5.2.1.Linux x86-64 Ubuntu 15.10:

gcc -g -std=c99 -O0 -c main.c
objdump -dr main.o

Mit -O0beide sind gleich.

Mit -O3:

  • mit Einschränkung:

    3f0:   48 85 d2                test   %rdx,%rdx
    3f3:   74 33                   je     428 <fr+0x38>
    3f5:   55                      push   %rbp
    3f6:   53                      push   %rbx
    3f7:   48 89 f5                mov    %rsi,%rbp
    3fa:   be 04 00 00 00          mov    $0x4,%esi
    3ff:   48 89 d3                mov    %rdx,%rbx
    402:   48 83 ec 08             sub    $0x8,%rsp
    406:   e8 00 00 00 00          callq  40b <fr+0x1b>
                            407: R_X86_64_PC32      memset-0x4
    40b:   48 83 c4 08             add    $0x8,%rsp
    40f:   48 89 da                mov    %rbx,%rdx
    412:   48 89 ef                mov    %rbp,%rdi
    415:   5b                      pop    %rbx
    416:   5d                      pop    %rbp
    417:   be 09 00 00 00          mov    $0x9,%esi
    41c:   e9 00 00 00 00          jmpq   421 <fr+0x31>
                            41d: R_X86_64_PC32      memset-0x4
    421:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)
    428:   f3 c3                   repz retq
    

    Zwei memset ruft wie erwartet an.

  • ohne Einschränkung: keine stdlib-Aufrufe, nur 16 Iterationen breit Schleife abrollen was ich hier nicht wiedergeben möchte 🙂

Ich hatte nicht die Geduld, sie zu benchmarken, aber ich glaube, dass die eingeschränkte Version schneller sein wird.

C99

Schauen wir uns der Vollständigkeit halber den Standard an.

restrict besagt, dass zwei Zeiger nicht auf überlappende Speicherbereiche zeigen können. Die häufigste Verwendung ist für Funktionsargumente.

Dies schränkt ein, wie die Funktion aufgerufen werden kann, ermöglicht jedoch weitere Optimierungen zur Kompilierzeit.

Wenn der Anrufer dem nicht folgt restrict Vertrag, undefiniertes Verhalten.

Das C99 N1256-Entwurf 6.7.3/7 “Type Qualifiers” sagt:

Die beabsichtigte Verwendung des Einschränkungskennzeichners (wie der Registerspeicherklasse) besteht darin, die Optimierung zu fördern, und das Löschen aller Instanzen des Kennzeichners aus allen vorverarbeitenden Übersetzungseinheiten, die ein konformes Programm bilden, ändert seine Bedeutung (dh beobachtbares Verhalten) nicht.

und 6.7.3.1 „Formale Definition von Beschränkung“ liefert die blutigen Details.

Strenge Aliasing-Regel

Das restrict Das Schlüsselwort wirkt sich nur auf Zeiger kompatibler Typen aus (z. B. two int*), da die strengen Aliasing-Regeln besagen, dass das Aliasing inkompatibler Typen standardmäßig ein undefiniertes Verhalten ist, sodass Compiler davon ausgehen können, dass dies nicht der Fall ist, und wegoptimieren.

Siehe: Was ist die strikte Aliasing-Regel?

Siehe auch

  • C++14 hat noch kein Analogon für restrictaber GCC hat __restrict__ als Erweiterung: Was bedeutet das Schlüsselwort „restrict“ in C++?
  • Viele Fragen, die sich stellen: Laut den blutigen Details, ist dieser Code UB oder nicht?
    • Restriktionskennzeichner anhand von Beispielen verstehen
    • Eingeschränkte Zeigerfragen
    • Ist es legal, einen eingeschränkten Zeiger einem anderen Zeiger zuzuweisen und den zweiten Zeiger zu verwenden, um den Wert zu ändern?
  • Eine “Wann zu verwenden”-Frage: Wann ist die Verwendung einzuschränken und wann nicht
  • Der zugehörige GCC __attribute__((malloc))was besagt, dass der Rückgabewert einer Funktion keinem Alias ​​zugeordnet ist: GCC: __attribute__((malloc))

  • Der Qualifizierer „Einschränken“ kann tatsächlich viel größere Einsparungen ermöglichen. Zum Beispiel gegeben void zap(char *restrict p1, char *restrict p2) { for (int i=0; i<50; i++) { p1[i] = 4; p2[i] = 9; } }, würden die Einschränkungsqualifizierer den Compiler den Code als “memset(p1,4,50); memset(p2,9,50);” neu schreiben lassen. Restrict ist dem typbasierten Aliasing weit überlegen; Es ist eine Schande, dass sich Compiler mehr auf letzteres konzentrieren.

    – Superkatze

    9. März 2016 um 0:14 Uhr

  • @tim18: Das Schlüsselwort „restrict“ kann viele Optimierungen ermöglichen, die selbst aggressive typbasierte Optimierungen nicht können. Darüber hinaus macht es das Vorhandensein von „restrict“ in der Sprache – im Gegensatz zu aggressivem typenbasiertem Aliasing – niemals unmöglich, Aufgaben so effizient zu erledigen, wie es ohne sie möglich wäre (da Code, der durch „restrict“ gebrochen würde, einfach nicht verwenden, während Code, der durch aggressives TBAA beschädigt wurde, oft auf weniger effiziente Weise umgeschrieben werden muss).

    – Superkatze

    23. Mai 2016 um 22:28 Uhr

  • @tim18: Umgeben Sie Dinge, die doppelte Unterstreichungen enthalten, in Backticks, wie in __restrict. Andernfalls könnten die doppelten Unterstreichungen als Hinweis darauf missverstanden werden, dass Sie schreien.

    – Superkatze

    6. Juni 2016 um 17:09 Uhr

  • Wichtiger als nicht zu schreien ist, dass die Unterstriche eine Bedeutung haben, die direkt für den Punkt relevant ist, den Sie ansprechen möchten.

    – Recycling

    6. August 2018 um 4:11 Uhr

  • GCC 7 und Clang 9 – bei jeder Optimierung (-O1, -Os) __restrict__ da keine Bedeutung mehr, da der Compiler gleich optimiert. Erinnert mich an die register Stichwort. Stimmen Sie immer noch für die epische Antwort ab. Gut gemacht!

    – elcuco

    26. August 2020 um 8:20 Uhr

Benutzeravatar von Michel
Michel

Der folgende C99-Code gibt je nach entweder 0 oder 1 zurück beschränken Qualifikation :

__attribute__((noinline))
int process(const int * restrict const a, int * const b) {
    *b /= (*a + 1) ;
    return *a + *b ;
}

int main(void) {
    int data[2] = {1, 2};
    return process(&data[0], &data[0]);
}

Mit dem Snippet können Sie realistische Beispiele erstellen, insbesondere wenn *a ist eine Schleifenbedingung.

Kompilieren mit gcc -std=c99 -Wall -pedantic -O3 main.c.

1425980cookie-checkRealistische Verwendung des C99-Schlüsselworts „restrict“?

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

Privacy policy