Utilizzo realistico della parola chiave "limit" di C99?


183

Stavo sfogliando alcuni documenti e domande / risposte e l'ho visto menzionato. Ho letto una breve descrizione, affermando che sarebbe fondamentalmente una promessa del programmatore che il puntatore non verrà utilizzato per indicare altrove.

Qualcuno può offrire alcuni casi realistici in cui vale la pena usarlo?


4
memcpyvs memmoveè un esempio canonico.
Alexandre C.,

@AlexandreC .: Non penso che sia particolarmente applicabile, poiché la mancanza di un qualificatore "restringente" non implica che la logica del programma funzionerà con il sovraccarico di origine e destinazione, né la presenza di tale qualificatore impedirebbe a un metodo chiamato di determinare se l'origine e la destinazione si sovrappongono e, in tal caso, sostituire dest con src + (dest-src) che, essendo derivato da src, avrebbe il permesso di alias.
supercat,

@supercat: ecco perché l'ho messo come commento. Tuttavia, 1) restrict-qualificare gli argomenti per memcpyconsentire in linea di principio un'attuazione ingenua da ottimizzare in modo aggressivo, e 2) la semplice chiamata memcpyconsente al compilatore di supporre che gli argomenti forniti non siano alias, il che potrebbe consentire una certa ottimizzazione attorno alla memcpychiamata.
Alexandre C.,

@AlexandreC .: Sarebbe molto difficile per un compilatore sulla maggior parte delle piattaforme ottimizzare un ingenuo memcpy - anche con "restringimento" - per essere quasi altrettanto efficiente della versione adattata al target. Le ottimizzazioni sul lato chiamata non richiederebbero la parola chiave "restringi" e, in alcuni casi, gli sforzi per facilitare quelli potrebbero essere controproducenti. Ad esempio, molte implementazioni di memcpy potrebbero, a costo zero, essere considerate memcpy(anything, anything, 0);no-op e garantire che se pè un puntatore ad almeno nbyte scrivibili memcpy(p,p,n),; non avrà effetti collaterali negativi. Possono verificarsi casi del genere ...
supercat

... naturalmente in alcuni tipi di codice dell'applicazione (ad es. una sorta di routine che scambia un oggetto con se stesso) e in implementazioni in cui non hanno effetti collaterali negativi, lasciare che tali casi siano gestiti dal codice del caso generale può essere più efficiente che avere per aggiungere test per casi speciali. Sfortunatamente, alcuni autori di compilatori sembrano pensare che sia meglio richiedere che i programmatori aggiungano codice che il compilatore potrebbe non essere in grado di ottimizzare, in modo da facilitare le "opportunità di ottimizzazione" che i compilatori raramente sfrutterebbero comunque.
supercat

Risposte:


182

restrictdice che il puntatore è l'unica cosa che accede all'oggetto sottostante. Elimina il potenziale per l'aliasing del puntatore, consentendo una migliore ottimizzazione da parte del compilatore.

Ad esempio, supponiamo di avere una macchina con istruzioni specializzate in grado di moltiplicare i vettori di numeri in memoria e di avere il seguente codice:

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

Il compilatore deve gestire correttamente se dest, src1e si src2sovrappongono, il che significa che deve eseguire una moltiplicazione alla volta, dall'inizio alla fine. Avendo restrict, il compilatore è libero di ottimizzare questo codice usando le istruzioni vettoriali.

Wikipedia ha una voce su restrict, con un altro esempio, qui .


3
@Michael - Se non sbaglio, il problema sarebbe solo quando destsi sovrappone uno dei vettori di origine. Perché dovrebbe esserci un problema se src1e si src2sovrappongono?

1
restringere normalmente ha un effetto solo quando si punta su un oggetto che viene modificato, nel qual caso afferma che non è necessario prendere in considerazione effetti collaterali nascosti. Molti compilatori lo usano per facilitare la vettorializzazione. Msvc utilizza il controllo del tempo di esecuzione per la sovrapposizione dei dati a tale scopo.
tim18

L'aggiunta della parola chiave register alla variabile for loop rende anche più veloce oltre all'aggiunta di limit.

2
In realtà, la parola chiave register è solo consultiva. E nei compilatori dal 2000 circa, i (e n per il confronto) nell'esempio saranno ottimizzati in un registro indipendentemente dal fatto che si usi o meno la parola chiave register.
Mark Fischler

154

L' esempio di Wikipedia è molto illuminante.

Mostra chiaramente come consente di salvare un'istruzione di assemblaggio .

Senza restrizioni:

void f(int *a, int *b, int *x) {
  *a += *x;
  *b += *x;
}

Pseudo assemblaggio:

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

Con limitazione:

void fr(int *restrict a, int *restrict b, int *restrict x);

Pseudo assemblaggio:

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

GCC lo fa davvero?

GCC 4.8 Linux x86-64:

gcc -g -std=c99 -O0 -c main.c
objdump -S main.o

Con -O0, sono gli stessi.

Con -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) 

Per i non iniziati, la convenzione di chiamata è:

  • rdi = primo parametro
  • rsi = secondo parametro
  • rdx = terzo parametro

L'output di GCC è stato persino più chiaro dell'articolo wiki: 4 istruzioni contro 3 istruzioni.

Array

Finora abbiamo risparmi con una singola istruzione, ma se il puntatore rappresenta array da sottoporre a loop, un caso d'uso comune, allora un gruppo di istruzioni potrebbe essere salvato, come menzionato da supercat .

Considera ad esempio:

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

A causa di restrict, un compilatore intelligente (o umano), potrebbe ottimizzarlo per:

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

che è potenzialmente molto più efficiente in quanto può essere assemblato ottimizzato su un'implementazione libc decente (come glibc): è meglio usare std :: memcpy () o std :: copy () in termini di prestazioni?

GCC lo fa davvero?

GCC 5.2.1. Linux x86-64 Ubuntu 15.10:

gcc -g -std=c99 -O0 -c main.c
objdump -dr main.o

Con -O0, entrambi sono uguali.

Con -O3:

  • con limitazione:

    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

    Due memsetchiamate come previsto.

  • senza restrizioni: nessuna chiamata stdlib, solo un srotolamento a ciclo continuo di 16 iterazioni che non intendo riprodurre qui :-)

Non ho avuto la pazienza di confrontarli, ma credo che la versione con restrizioni sarà più veloce.

C99

Diamo un'occhiata allo standard per completezza.

restrictafferma che due puntatori non possono puntare a aree di memoria sovrapposte. L'uso più comune è per gli argomenti delle funzioni.

Ciò limita il modo in cui è possibile chiamare la funzione, ma consente ulteriori ottimizzazioni in fase di compilazione.

Se il chiamante non segue il restrictcontratto, comportamento indefinito.

Il progetto C99 N1256 6.7.3 / 7 "Qualificatori di tipo" dice:

L'uso previsto del qualificatore di limitazione (come la classe di archiviazione dei registri) è quello di promuovere l'ottimizzazione e l'eliminazione di tutte le istanze del qualificatore da tutte le unità di traduzione di preelaborazione che compongono un programma conforme non ne modifica il significato (vale a dire comportamento osservabile).

e 6.7.3.1 "Definizione formale di restringimento" fornisce i dettagli cruenti.

Regola aliasing rigorosa

La restrictparola chiave influenza solo i puntatori di tipi compatibili (ad esempio due int*) perché le regole di aliasing rigoroso indicano che l'alias di tipi incompatibili è un comportamento indefinito per impostazione predefinita, e quindi i compilatori possono presumere che non accada e ottimizzare via.

Vedi: Qual è la regola di aliasing rigorosa?

Guarda anche


9
Il qualificatore "restringimento" può effettivamente consentire risparmi molto maggiori. Ad esempio, dato che void zap(char *restrict p1, char *restrict p2) { for (int i=0; i<50; i++) { p1[i] = 4; p2[i] = 9; } }i qualificatori di limitazione consentirebbero al compilatore di riscrivere il codice come "memset (p1,4,50); memset (p2,9,50);". Limitare è di gran lunga superiore all'aliasing basato sul tipo; è un peccato che i compilatori si concentrino maggiormente su quest'ultimo.
supercat

@supercat ottimo esempio, aggiunto per rispondere.
Ciro Santilli 9 冠状 病 六四 事件 法轮功

2
@ tim18: la parola chiave "restringi" può consentire molte ottimizzazioni che nemmeno l'ottimizzazione basata sul tipo aggressivo può fare. Inoltre, l'esistenza di "restringi" nel linguaggio - a differenza dell'aliasing basato sul tipo aggressivo - non rende mai impossibile eseguire compiti nel modo più efficiente possibile in loro assenza (poiché il codice che verrebbe violato da "restringi" può semplicemente non usarlo, mentre il codice che è rotto dal TBAA aggressivo deve essere spesso riscritto in modo meno efficiente).
supercat,

2
@ tim18: circonda le cose che contengono doppie sottolineature nei backtick, come in __restrict. Altrimenti le doppie sottolineature potrebbero essere interpretate erroneamente come un'indicazione che stai gridando.
supercat

1
Più importante del non gridare è che i caratteri di sottolineatura hanno un significato direttamente pertinente al punto che stai cercando di sottolineare.
ricicla il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.