Realistyczne użycie słowa kluczowego „99 ”C99?

183

Przeglądałem dokumentację i pytania / odpowiedzi i widziałem o tym wspomniane. Przeczytałem krótki opis, stwierdzając, że programista byłby w zasadzie obietnicą, że wskaźnik nie zostanie użyty do wskazania w innym miejscu.

Czy ktoś może zaoferować realistyczne przypadki, w których warto z tego skorzystać?

użytkownik90052
źródło
4
memcpyvs memmovejest jednym kanonicznym przykładem.
Alexandre C.
@AlexandreC .: Nie sądzę, aby było to szczególnie odpowiednie, ponieważ brak kwalifikatora „ograniczającego” nie oznacza, że ​​logika programu będzie działać z przeciążeniem źródła i miejsca docelowego, ani obecność takiego kwalifikatora nie zapobiegnie wywołaniu wywoływanej metody określenie, czy źródło i cel nakładają się, a jeśli tak, zastąpienie dest src + (dest-src), który, ponieważ pochodzi od src, miałby możliwość aliasu.
supercat 19.04.16
@ superupat: Właśnie dlatego umieszczam to jako komentarz. Jednak 1) restrict-kwalifikowanie argumentów w celu memcpyumożliwienia w zasadzie agresywnej optymalizacji naiwnej implementacji oraz 2) samo wywołanie memcpypozwala kompilatorowi założyć, że podane mu argumenty nie są aliasami, co może pozwolić na pewną optymalizację wokół memcpywywołania.
Alexandre C.
@AlexandreC .: Kompilatorowi na większości platform byłoby bardzo trudno zoptymalizować naiwny memcpy - nawet z „ograniczeniem” - aby był tak blisko wydajności jak wersja dostosowana do celu. Optymalizacje strony wywołującej nie wymagałyby słowa kluczowego „ogranicz”, a w niektórych przypadkach wysiłki na rzecz ułatwienia tych działań mogą przynieść efekt przeciwny do zamierzonego. Na przykład wiele implementacji memcpy może, przy zerowym koszcie dodatkowym, uznać memcpy(anything, anything, 0);za brak operacji i zapewnić, że jeśli pjest wskaźnikiem do co najmniej nzapisywalnych bajtów memcpy(p,p,n); nie będzie miał żadnych niepożądanych skutków ubocznych. Takie przypadki mogą powstać ...
supercat 20.04.16
... oczywiście w niektórych rodzajach kodu aplikacji (np. procedura sortowania zamieniająca element na siebie) oraz we wdrożeniach, w których nie wywołują żadnych niepożądanych skutków ubocznych, pozwalając na rozpatrywanie tych przypadków za pomocą ogólnego kodu spraw może być bardziej wydajne niż mieć aby dodać testy przypadków specjalnych. Niestety, niektórzy pisarze kompilatorów uważają, że lepiej jest wymagać od programistów dodania kodu, który kompilator może nie być w stanie zoptymalizować, aby ułatwić „możliwości optymalizacji”, które i tak rzadko byłyby wykorzystywane przez kompilatory.
supercat

Odpowiedzi:

182

restrictmówi, że wskaźnik jest jedyną rzeczą, która uzyskuje dostęp do obiektu leżącego pod spodem. Eliminuje to możliwość aliasingu wskaźnika, umożliwiając lepszą optymalizację przez kompilator.

Załóżmy na przykład, że mam maszynę ze specjalistycznymi instrukcjami, która może pomnożyć wektory wektorów liczb w pamięci, i mam następujący kod:

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

Potrzeby kompilatora prawidłowo obsługiwać razie dest, src1i src2nakładania, co oznacza, że musi wykonać jedną mnożenia na raz, od początku do końca. Poprzez restrictkompilator jest wolna, aby zoptymalizować ten kod za pomocą instrukcji wektorowych.

Wikipedia ma wpis na restrictinnym przykładzie tutaj .

Michael
źródło
3
@Michael - Jeśli się nie mylę, problem będzie występował tylko wtedy, gdy destnakłada się na którykolwiek z wektorów źródłowych. Dlaczego miałby nie być problem, jeśli src1i src2nakładanie?
ysap,
1
Ograniczenie zwykle działa tylko wtedy, gdy wskazuje na obiekt, który jest modyfikowany, w którym to przypadku nie wymaga uwzględnienia ukrytych efektów ubocznych. Większość kompilatorów używa go do ułatwienia wektoryzacji. Msvc używa do tego celu sprawdzania czasu wykonywania, aby dane się pokrywały.
tim18
Dodanie słowa kluczowego register do zmiennej for loop również przyspiesza oprócz dodawania ograniczenia.
2
W rzeczywistości słowo kluczowe rejestru ma jedynie charakter doradczy. A w kompilatorach od około 2000 roku i (i n dla porównania) w tym przykładzie zostaną zoptymalizowane do rejestru, niezależnie od tego, czy użyjesz słowa kluczowego register.
Mark Fischler,
154

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 -O0są 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 restrictinteligentny 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 memsetpołą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.

restrictmó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 restrictumowy, 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 restrictkluczowe 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ż

Ciro Santilli
źródło
9
Kwalifikator „ograniczenie” może faktycznie pozwolić na znacznie większe oszczędności. Na przykład, biorąc pod uwagę void zap(char *restrict p1, char *restrict p2) { for (int i=0; i<50; i++) { p1[i] = 4; p2[i] = 9; } }, kwalifikatory ograniczające pozwolą kompilatorowi przepisać kod jako „memset (p1,4,50); memset (p2,9,50);”. Ograniczenie znacznie przewyższa aliasing oparty na typach; Szkoda, że ​​kompilatory koncentrują się bardziej na tym drugim.
supercat,
@supercat świetny przykład, dodany do odpowiedzi.
Ciro Santilli 法轮功 冠状 病 六四 事件 法轮功
2
@ tim18: Słowo kluczowe „ograniczenie” może umożliwić wiele optymalizacji, których nawet agresywna optymalizacja oparta na typie nie jest w stanie. Co więcej, istnienie „ograniczania” w języku - w przeciwieństwie do agresywnego aliasingu opartego na typach - nigdy nie uniemożliwia wykonywania zadań tak skutecznie, jak to możliwe przy ich braku (ponieważ kod, który zostałby złamany przez „ograniczenie”, może po prostu nie używaj go, podczas gdy kod, który jest łamany przez agresywną TBAA, musi być często przepisywany w mniej wydajny sposób).
supercat
2
@ tim18: Otaczaj rzeczy, które zawierają podwójne podkreślenia w backticks, jak w __restrict. W przeciwnym razie podwójne podkreślenia mogą zostać źle zinterpretowane jako wskazówka, że ​​krzyczysz.
supercat
1
Ważniejsze niż nie krzyczenie jest to, że podkreślenia mają znaczenie bezpośrednio związane z punktem, który próbujesz przedstawić.
motocykle