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?
GCC 4.8 Linux x86-64:
gcc -g -std=c99 -O0 -c main.c
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 Befehlen, aber wenn Zeiger Arrays darstellen, die durchlaufen werden sollen, ein häufiger Anwendungsfall, dann könnte eine Reihe von Befehlen gespeichert 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;
}
}
Wegen restrict
, ein Smart - Compiler (oder Menschen), könnten diese optimieren:
memset(p1, 4, 50);
memset(p2, 9, 50);
Dies ist möglicherweise viel effizienter, da es für eine anständige libc-Implementierung (wie glibc) für die Assembly optimiert werden kann: Ist es in Bezug auf die Leistung besser, std :: memcpy () oder std :: copy () zu verwenden?
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.
C99
Schauen wir uns der Vollständigkeit halber den Standard an.
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 mehr Optimierungen zur Kompilierungszeit.
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.
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?
Siehe auch
memcpy
vsmemmove
ist ein kanonisches Beispiel.