Przykład Wikipedia jest bardzo pouczające.
Wyraźnie pokazuje, w jaki sposób można zapisać jedną instrukcję montażu .
Bez ograniczeń:
void f(int *a, int *b, int *x) {
*a += *x;
*b += *x;
}
Pseudo-montaż:
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
Z ograniczeniem:
void fr(int *restrict a, int *restrict b, int *restrict x);
Pseudo-montaż:
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
Czy GCC naprawdę to robi?
GCC 4.8 Linux x86-64:
gcc -g -std=c99 -O0 -c main.c
objdump -S main.o
Z -O0
są takie same.
Z -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)
Dla niewtajemniczonych konwencja wywoływania to:
rdi
= pierwszy parametr
rsi
= drugi parametr
rdx
= trzeci parametr
Wynik GCC był jeszcze wyraźniejszy niż artykuł na wiki: 4 instrukcje vs 3 instrukcje.
Tablice
Do tej pory mamy oszczędności na pojedynczych instrukcjach, ale jeśli wskaźnik reprezentuje tablice, które mają być zapętlone, co jest częstym przypadkiem użycia, wówczas można zapisać kilka instrukcji, jak wspomniano w supercat .
Rozważ na przykład:
void f(char *restrict p1, char *restrict p2) {
for (int i = 0; i < 50; i++) {
p1[i] = 4;
p2[i] = 9;
}
}
Z tego powodu restrict
inteligentny kompilator (lub człowiek) może zoptymalizować to w celu:
memset(p1, 4, 50);
memset(p2, 9, 50);
który jest potencjalnie znacznie bardziej wydajny, ponieważ może być zoptymalizowany pod kątem montażu w przyzwoitej implementacji libc (jak glibc): czy lepiej jest używać std :: memcpy () lub std :: copy () pod względem wydajności?
Czy GCC naprawdę to robi?
GCC 5.2.1.Linux x86-64 Ubuntu 15.10:
gcc -g -std=c99 -O0 -c main.c
objdump -dr main.o
Z -O0
, oba są takie same.
Z -O3
:
z ograniczeniem:
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
Dwa memset
połączenia zgodnie z oczekiwaniami.
bez ograniczeń: brak wywołań stdlib, tylko rozwijanie pętli o szerokości 16 iteracji, których nie zamierzam tu odtwarzać :-)
Nie miałem cierpliwości, by je testować, ale wierzę, że wersja z ograniczeniami będzie szybsza.
C99
Spójrzmy na standard dla kompletności.
restrict
mówi, że dwa wskaźniki nie mogą wskazywać na nakładające się obszary pamięci. Najczęstszym zastosowaniem są argumenty funkcji.
Ogranicza to sposób wywoływania funkcji, ale pozwala na więcej optymalizacji czasu kompilacji.
Jeśli dzwoniący nie przestrzega restrict
umowy, niezdefiniowane zachowanie.
Projekt C99 N1256 6.7.3 / 7 „Kwalifikatory typu” mówi:
Zamierzonym zastosowaniem kwalifikatora ograniczającego (takiego jak klasa pamięci rejestru) jest promowanie optymalizacji, a usunięcie wszystkich instancji kwalifikatora ze wszystkich jednostek tłumaczenia wstępnego tworzących zgodny program nie zmienia jego znaczenia (tj. Obserwowalnego zachowania).
oraz 6.7.3.1 „Formalna definicja ograniczenia” podaje krwawe szczegóły.
Ścisła zasada aliasingu
Słowo restrict
kluczowe wpływa tylko na wskaźniki kompatybilnych typów (np. Dwa int*
), ponieważ surowe reguły aliasingu mówią, że aliasing niezgodnych typów jest domyślnie niezdefiniowanym zachowaniem, więc kompilatory mogą założyć, że tak się nie dzieje i zoptymalizować.
Zobacz: jaka jest ścisła zasada aliasingu?
Zobacz też
memcpy
vsmemmove
jest jednym kanonicznym przykładem.