Seperti yang dikatakan orang lain, jika tidak ada artinya pada C ++ 14 , jadi mari kita pertimbangkan __restrict__
ekstensi GCC yang melakukan hal yang sama dengan C99 restrict
.
C99
restrict
mengatakan bahwa dua petunjuk tidak dapat menunjuk ke wilayah memori yang tumpang tindih. Penggunaan paling umum adalah untuk argumen fungsi.
Ini membatasi bagaimana fungsi dapat dipanggil, tetapi memungkinkan untuk lebih kompilasi optimasi.
Jika penelepon tidak mengikuti restrict
kontrak, perilaku tidak terdefinisi.
The C99 N1256 rancangan 6.7.3 / 7 "Jenis kualifikasi" mengatakan:
Tujuan penggunaan pembatasan kualifikasi (seperti kelas penyimpanan register) adalah untuk mempromosikan optimasi, dan menghapus semua instance kualifikasi dari semua unit terjemahan preprocessing yang menyusun program yang sesuai tidak mengubah artinya (yaitu, perilaku yang dapat diamati).
dan 6.7.3.1 "Definisi formal pembatasan" memberikan detail yang mengerikan.
Kemungkinan pengoptimalan
The Wikipedia Contoh yang sangat mencerahkan.
Ini jelas menunjukkan bagaimana memungkinkan untuk menyimpan satu instruksi perakitan .
Tanpa batasan:
void f(int *a, int *b, int *x) {
*a += *x;
*b += *x;
}
Perakitan semu:
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
Dengan batasan:
void fr(int *restrict a, int *restrict b, int *restrict x);
Perakitan semu:
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
Apakah GCC benar-benar melakukannya?
g++
4.8 Linux x86-64:
g++ -g -std=gnu++98 -O0 -c main.cpp
objdump -S main.o
Dengan -O0
, mereka sama.
Dengan -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)
Untuk yang belum tahu, konvensi pemanggilan adalah:
rdi
= parameter pertama
rsi
= parameter kedua
rdx
= parameter ketiga
Output GCC bahkan lebih jelas daripada artikel wiki: 4 instruksi vs 3 instruksi.
Array
Sejauh ini kami memiliki penghematan instruksi tunggal, tetapi jika pointer mewakili array yang akan dilewati, kasus penggunaan umum, maka banyak instruksi dapat disimpan, seperti yang disebutkan oleh supercat dan michael .
Pertimbangkan misalnya:
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;
}
}
Karena itu restrict
, kompiler pintar (atau manusia), dapat mengoptimalkannya untuk:
memset(p1, 4, size);
memset(p2, 9, size);
Yang berpotensi jauh lebih efisien karena dapat dioptimalkan perakitan pada implementasi libc yang layak (seperti glibc) Apakah lebih baik menggunakan std :: memcpy () atau std :: copy () dalam hal kinerja? , mungkin dengan instruksi SIMD .
Tanpa, batasi, optimasi ini tidak dapat dilakukan, misalnya pertimbangkan:
char p1[4];
char *p2 = &p1[1];
f(p1, p2, 3);
Kemudian for
versi membuat:
p1 == {4, 4, 4, 9}
sedangkan memset
versi membuat:
p1 == {4, 9, 9, 9}
Apakah GCC benar-benar melakukannya?
GCC 5.2.1.Linux x86-64 Ubuntu 15.10:
gcc -g -std=c99 -O0 -c main.c
objdump -dr main.o
Dengan -O0
, keduanya sama.
Dengan -O3
:
dengan batasan:
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
Dua memset
panggilan seperti yang diharapkan.
tanpa batasan: tidak ada panggilan stdlib, hanya loop lebar iterasi 16 terbuka yang saya tidak ingin mereproduksi di sini :-)
Saya belum memiliki kesabaran untuk membandingkan mereka, tetapi saya percaya bahwa versi pembatasan akan lebih cepat.
Aturan aliasing yang ketat
Kata restrict
kunci hanya memengaruhi pointer dari tipe yang kompatibel (misalnya dua int*
) karena aturan aliasing yang ketat mengatakan bahwa aliasing tipe yang tidak kompatibel adalah perilaku yang tidak terdefinisi secara default, sehingga kompiler dapat menganggap itu tidak terjadi dan mengoptimalkannya.
Lihat: Apa aturan aliasing yang ketat?
Apakah ini berfungsi untuk referensi?
Menurut dokumen GCC, ia melakukannya: https://gcc.gnu.org/onlinedocs/gcc-5.1.0/gcc/Restricted-Pointers.html dengan sintaks:
int &__restrict__ rref
Bahkan ada versi untuk this
fungsi anggota:
void T::fn () __restrict__
restrict
adalah kata kunci c99. Ya, Rpbert S. Barnes, saya tahu sebagian besar penyusun mendukung__restrict__
. Anda akan mencatat bahwa apa pun dengan garis bawah ganda adalah, menurut definisi, implementasi spesifik dan karenanya BUKAN C ++ , tetapi merupakan versi spesifik kompilator.