Wie andere sagten, wenn ab C ++ 14 nichts bedeutet , betrachten wir die __restrict__
GCC-Erweiterung, die dasselbe tut wie die C99 restrict
.
C99
restrict
sagt, dass zwei Zeiger nicht auf überlappende Speicherbereiche zeigen können. Die häufigste Verwendung sind Funktionsargumente.
Dies schränkt den Aufruf der Funktion ein, ermöglicht jedoch weitere Kompilierungsoptimierungen.
Wenn der Anrufer dem restrict
Vertrag nicht folgt , undefiniertes Verhalten.
Der C99 N1256 Entwurf 6.7.3 / 7 "Typqualifizierer" sagt:
Die beabsichtigte Verwendung des Einschränkungsqualifizierers (wie der Registerspeicherklasse) besteht darin, die Optimierung zu fördern, und das Löschen aller Instanzen des Qualifizierers aus allen vorverarbeitenden Übersetzungseinheiten, aus denen ein konformes Programm besteht, ändert seine Bedeutung nicht (dh das beobachtbare Verhalten).
und 6.7.3.1 "Formale Definition von Beschränkung" gibt die blutigen Details an.
Eine mögliche Optimierung
Das Wikipedia-Beispiel ist sehr aufschlussreich.
Es zeigt deutlich, wie eine Montageanweisung gespeichert werden kann .
Ohne Einschränkung:
void f(int *a, int *b, int *x) {
*a += *x;
*b += *x;
}
Pseudo-Assemblierung:
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 a may be equal to x.
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-Assemblierung:
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
load R2 ← *b
add R2 += R1
set R2 → *b
Macht GCC das wirklich?
g++
4.8 Linux x86-64:
g++ -g -std=gnu++98 -O0 -c main.cpp
objdump -S main.o
Mit -O0
sind sie 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 die Uneingeweihten lautet die aufrufende Konvention :
rdi
= erster Parameter
rsi
= zweiter Parameter
rdx
= dritter Parameter
Die GCC-Ausgabe war noch deutlicher als der Wiki-Artikel: 4 Anweisungen gegen 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önnte eine Reihe von Anweisungen gespeichert werden, wie von Supercat und Michael erwähnt .
Betrachten Sie zum Beispiel:
void f(char *restrict p1, char *restrict p2, size_t size) {
for (size_t i = 0; i < size; i++) {
p1[i] = 4;
p2[i] = 9;
}
}
Wegen restrict
, ein Smart - Compiler (oder Menschen), könnten diese optimieren:
memset(p1, 4, size);
memset(p2, 9, size);
Was ist möglicherweise viel effizienter, da es für eine anständige libc-Implementierung (wie glibc) Assembly-optimiert werden kann ? Ist es in Bezug auf die Leistung besser, std :: memcpy () oder std :: copy () zu verwenden? , möglicherweise mit SIMD-Anweisungen .
Ohne Einschränkung könnte diese Optimierung nicht durchgeführt werden, z. B. berücksichtigen Sie:
char p1[4];
char *p2 = &p1[1];
f(p1, p2, 3);
Dann for
macht die Version:
p1 == {4, 4, 4, 9}
während die memset
Version macht:
p1 == {4, 9, 9, 9}
Macht 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 -O0
sind beide gleich.
Mit -O3
:
mit einschränken:
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
Anrufe wie erwartet.
ohne Einschränkung: keine stdlib-Aufrufe, nur eine 16 Iterationen breite Schleife, die ich hier nicht reproduzieren möchte :-)
Ich hatte nicht die Geduld, sie zu vergleichen, aber ich glaube, dass die eingeschränkte Version schneller sein wird.
Strikte Aliasing-Regel
Das restrict
Schlüsselwort wirkt sich nur auf Zeiger kompatibler Typen aus (z. B. zwei int*
), da die strengen Aliasing-Regeln besagen, dass das Aliasing inkompatibler Typen standardmäßig ein undefiniertes Verhalten ist. Compiler können daher davon ausgehen, dass dies nicht der Fall ist, und optimieren.
Siehe: Was ist die strenge Aliasing-Regel?
Funktioniert es für Referenzen?
Laut den GCC-Dokumenten gilt Folgendes: https://gcc.gnu.org/onlinedocs/gcc-5.1.0/gcc/Restricted-Pointers.html mit Syntax:
int &__restrict__ rref
Es gibt sogar eine Version für this
Mitgliedsfunktionen:
void T::fn () __restrict__
restrict
ist ein c99-Schlüsselwort. Ja, Rpbert S. Barnes, ich weiß, dass die meisten Compiler dies unterstützen__restrict__
. Sie werden feststellen, dass alles mit doppelten Unterstrichen per Definition implementierungsspezifisch und somit NICHT C ++ ist , sondern eine compilerspezifische Version davon.