Jak powiedzieli inni, jeśli nic nie znaczy od C ++ 14 , rozważmy __restrict__
rozszerzenie GCC, które robi to samo co C99 restrict
.
C99
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ększą optymalizację 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.
Możliwa optymalizacja
Przykład Wikipedia jest bardzo pouczające.
Wyraźnie pokazuje, jak pozwala 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?
g++
4.8 Linux x86-64:
g++ -g -std=gnu++98 -O0 -c main.cpp
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 pojedynczych instrukcji, ale jeśli wskaźnik reprezentuje tablice, które mają być zapętlone, co jest powszechnym przypadkiem użycia, wówczas można zapisać kilka instrukcji, o czym wspominają supercat i michael .
Rozważ na przykład:
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;
}
}
Z tego powodu restrict
inteligentny kompilator (lub człowiek) może zoptymalizować to w celu:
memset(p1, 4, size);
memset(p2, 9, size);
Który jest potencjalnie znacznie bardziej wydajny, ponieważ może być zoptymalizowany pod kątem montażu w porządnej implementacji libc (takiej jak glibc) Czy lepiej jest używać std :: memcpy () lub std :: copy () pod względem wydajności? , ewentualnie z instrukcjami SIMD .
Bez ograniczenia ta optymalizacja nie byłaby możliwa, np. Rozważ:
char p1[4];
char *p2 = &p1[1];
f(p1, p2, 3);
Następnie for
wersja tworzy:
p1 == {4, 4, 4, 9}
podczas gdy memset
wersja zapewnia:
p1 == {4, 9, 9, 9}
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.
Ś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?
Czy to działa w odniesieniu do referencji?
Zgodnie z dokumentacją GCC robi to: https://gcc.gnu.org/onlinedocs/gcc-5.1.0/gcc/Restricted-Pointers.html ze składnią:
int &__restrict__ rref
Istnieje nawet wersja this
funkcji członkowskich:
void T::fn () __restrict__
restrict
to słowo kluczowe c99. Tak, Rpbert S. Barnes, wiem, że większość kompilatorów obsługuje__restrict__
. Zauważysz, że wszystko z podwójnymi podkreśleniami jest z definicji specyficzne dla implementacji, a więc NIE C ++ , ale jest wersją specyficzną dla kompilatora.