Penggunaan realistis kata kunci 'pembatasan' C99?

183

Saya sedang menelusuri beberapa dokumentasi dan pertanyaan / jawaban dan melihatnya disebutkan. Saya membaca deskripsi singkat, menyatakan bahwa itu pada dasarnya adalah janji dari programmer bahwa pointer tidak akan digunakan untuk menunjuk ke tempat lain.

Adakah yang bisa menawarkan beberapa kasus realistis di mana nilainya benar-benar menggunakan ini?

user90052
sumber
4
memcpyvs memmoveadalah salah satu contoh kanonik.
Alexandre C.
@AlexandreC.: Saya tidak berpikir itu yang sangat berlaku, karena kurangnya kualifikasi "membatasi" tidak menyiratkan bahwa logika program akan bekerja dengan sumber dan tujuan yang berlebihan, juga tidak akan adanya kualifikasi seperti itu mencegah metode yang dipanggil dari menentukan apakah sumber dan tujuan tumpang tindih dan, jika demikian, mengganti dest dengan src + (dest-src) yang, karena berasal dari src, akan diizinkan untuk alias itu.
supercat
@supercat: Itu sebabnya saya berikan sebagai komentar. Namun, 1) restrict-kualifikasi argumen untuk memcpymemungkinkan pada prinsipnya implementasi naif untuk dioptimalkan secara agresif, dan 2) hanya memanggil memcpymemungkinkan kompiler untuk mengasumsikan bahwa argumen yang diberikan kepadanya tidak alias, yang dapat memungkinkan beberapa optimasi di sekitar memcpypanggilan.
Alexandre C.
@AlexandreC .: Akan sangat sulit bagi kompiler di sebagian besar platform untuk mengoptimalkan memcpy naif - bahkan dengan "batasan" - untuk berada di dekat seefisien versi yang disesuaikan dengan target. Optimalisasi sisi panggilan tidak memerlukan kata kunci "batasi", dan dalam beberapa kasus upaya untuk memfasilitasi kata kunci tersebut mungkin tidak produktif. Sebagai contoh, banyak implementasi memcpy bisa, nol biaya tambahan, hal memcpy(anything, anything, 0);sebagai no-op, dan memastikan bahwa jika padalah pointer untuk setidaknya nbyte ditulis, memcpy(p,p,n); tidak akan memiliki efek samping yang merugikan. Kasus-kasus seperti itu dapat muncul ...
supercat
... secara alami dalam beberapa jenis kode aplikasi (misalnya semacam rutin menukar item dengan dirinya sendiri), dan dalam implementasi di mana mereka tidak memiliki efek samping, membiarkan kasus-kasus tersebut ditangani oleh kode kasus umum mungkin lebih efisien daripada memiliki untuk menambahkan tes kasus khusus. Sayangnya, beberapa penulis kompiler tampaknya berpikir lebih baik meminta pemrogram menambahkan kode yang tidak dapat dioptimalkan oleh kompiler, sehingga untuk memfasilitasi "peluang pengoptimalan" yang kompiler jarang akan mengeksploitasi.
supercat

Jawaban:

182

restrictmengatakan bahwa pointer adalah satu-satunya hal yang mengakses objek yang mendasarinya. Ini menghilangkan potensi aliasing pointer, memungkinkan optimasi yang lebih baik oleh kompiler.

Sebagai contoh, misalkan saya memiliki mesin dengan instruksi khusus yang dapat mengalikan vektor angka dalam memori, dan saya memiliki kode berikut:

void MultiplyArrays(int* dest, int* src1, int* src2, int n)
{
    for(int i = 0; i < n; i++)
    {
        dest[i] = src1[i]*src2[i];
    }
}

Kompilator perlu menangani dengan benar jika dest,, src1dan src2tumpang tindih, artinya harus melakukan satu perkalian pada satu waktu, dari awal hingga akhir. Dengan memiliki restrict, kompiler bebas untuk mengoptimalkan kode ini dengan menggunakan instruksi vektor.

Wikipedia memiliki entri aktif restrict, dengan contoh lain, di sini .

Michael
sumber
3
@Michael - Jika saya tidak salah, maka masalahnya hanya ketika desttumpang tindih salah satu vektor sumber. Mengapa akan ada masalah jika src1dan src2tumpang tindih?
ysap
1
Pembatasan biasanya hanya memiliki efek ketika menunjuk ke objek yang dimodifikasi, dalam hal ini menegaskan tidak ada efek samping tersembunyi yang perlu diperhitungkan. Sebagian besar kompiler menggunakannya untuk memfasilitasi vektorisasi. Msvc menggunakan run time check untuk tumpang tindih data untuk tujuan itu.
tim18
Menambahkan kata kunci register ke variabel for loop juga membuatnya lebih cepat selain menambah batasan.
2
Sebenarnya, kata kunci daftar hanya berupa saran. Dan dalam kompiler sejak sekitar tahun 2000, i (dan n untuk perbandingan) dalam contoh akan dioptimalkan menjadi register apakah Anda menggunakan kata kunci register atau tidak.
Mark Fischler
154

The Wikipedia Contoh adalah sangat mencerahkan.

Itu jelas menunjukkan bagaimana hal itu 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?

GCC 4.8 Linux x86-64:

gcc -g -std=c99 -O0 -c main.c
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 .

Pertimbangkan misalnya:

void f(char *restrict p1, char *restrict p2) {
    for (int i = 0; i < 50; i++) {
        p1[i] = 4;
        p2[i] = 9;
    }
}

Karena itu restrict, kompiler pintar (atau manusia), dapat mengoptimalkannya untuk:

memset(p1, 4, 50);
memset(p2, 9, 50);

yang berpotensi jauh lebih efisien karena perakitan dapat dioptimalkan pada implementasi libc yang layak (seperti glibc): Apakah lebih baik menggunakan std :: memcpy () atau std :: copy () dalam hal kinerja?

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 memsetpanggilan 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.

C99

Mari kita lihat standar demi kelengkapan.

restrictmengatakan 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 banyak waktu kompilasi optimasi.

Jika penelepon tidak mengikuti restrictkontrak, 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.

Aturan aliasing yang ketat

Kata restrictkunci 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?

Lihat juga

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
sumber
9
Kualifikasi "batasan" sebenarnya dapat memungkinkan penghematan yang jauh lebih besar. Sebagai contoh, diberikan void zap(char *restrict p1, char *restrict p2) { for (int i=0; i<50; i++) { p1[i] = 4; p2[i] = 9; } }, batasan kualifikasi akan membiarkan kompiler menulis ulang kode sebagai "memset (p1,4,50); memset (p2,9,50);". Pembatasan jauh lebih unggul dari alias berbasis tipe; itu memalukan kompiler lebih fokus pada yang terakhir.
supercat
@supercat contoh yang bagus, ditambahkan ke jawaban.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
2
@ tim18: Kata kunci "batasi" dapat mengaktifkan banyak optimasi yang bahkan optimasi berbasis tipe yang agresif pun tidak bisa. Lebih jauh, keberadaan "pembatasan" dalam bahasa - tidak seperti aliasing berbasis tipe agresif - tidak pernah membuat mustahil untuk menyelesaikan tugas seefisien mungkin tanpa adanya mereka (karena kode yang akan dipecah oleh "batasan" dapat dengan mudah tidak menggunakannya, sedangkan kode yang dipecah oleh TBAA agresif harus sering ditulis ulang dengan cara yang kurang efisien).
supercat
2
@ tim18: Mengelilingi hal-hal yang berisi garis bawah dua kali di backticks, seperti pada __restrict. Kalau tidak, double-underline mungkin disalahartikan sebagai indikasi bahwa Anda berteriak.
supercat
1
Lebih penting daripada tidak berteriak adalah bahwa garis bawah memiliki makna yang relevan langsung dengan poin yang Anda coba buat.
remikes