2009-12-27 13 views
40

Hat jemand Zahlen/Analysen darüber gesehen, ob die Verwendung des Schlüsselworts C/C++ restrict in gcc/g ++ tatsächlich einen signifikanten Leistungsschub in der Realität bietet (und nicht nur in Theorie)?Bietet das Schlüsselwort restrict signifikante Vorteile in gcc/g ++

Ich habe verschiedene Artikel gelesen, die es empfehlen/abwerten, aber ich bin nicht über irgendwelche wirklichen Zahlen gestolpert, die praktisch beide Seitenargumente demonstrieren.

EDIT

Ich weiß, dass restrict nicht offiziell Teil von C++, aber es wird von einigen Compilern unterstützt und ich habe ein Papier von Christer Ericson lesen, die es Nutzung empfiehlt dringend.

+9

Aliasing Probleme sind wird häufig als der Hauptgrund angesehen, warum C/C++ bei vielen Rechenaufgaben weniger effizient ist als Fortran. Ich würde also sagen, dass jede Funktion, die Aliasing vermeidet, einen großen Unterschied machen kann. – jalf

+0

mögliche Duplikate von [Realistische Verwendung des Schlüsselwortes C99 'restrict'?] (Http://stackoverflow.com/questions/745870/realistic-usage-of-the-c99-restrict-keyword) –

Antwort

41

Das Schlüsselwort restrict macht einen Unterschied.

Ich habe in einigen Situationen Verbesserungen von Faktor 2 und mehr gesehen (Bildverarbeitung). Die meiste Zeit ist der Unterschied jedoch nicht so groß. Ungefähr 10%.

Hier ist ein kleines Beispiel, das den Unterschied veranschaulicht. Ich habe eine sehr einfache 4x4-Vektor * -Matrix-Transformation als Test geschrieben. Beachten Sie, dass ich die Funktion erzwingen muss, nicht inline zu sein. Andernfalls erkennt GCC, dass in meinem Benchmark-Code keine Aliasing-Zeiger enthalten sind, und restrict würde aufgrund von Inlining keinen Unterschied machen.

Ich könnte die Transformationsfunktion auch in eine andere Datei verschoben haben.

#include <math.h> 

#ifdef USE_RESTRICT 
#else 
#define __restrict 
#endif 


void transform (float * __restrict dest, float * __restrict src, 
       float * __restrict matrix, int n) __attribute__ ((noinline)); 

void transform (float * __restrict dest, float * __restrict src, 
       float * __restrict matrix, int n) 
{ 
    int i; 

    // simple transform loop. 

    // written with aliasing in mind. dest, src and matrix 
    // are potentially aliasing, so the compiler is forced to reload 
    // the values of matrix and src for each iteration. 

    for (i=0; i<n; i++) 
    { 
    dest[0] = src[0] * matrix[0] + src[1] * matrix[1] + 
       src[2] * matrix[2] + src[3] * matrix[3]; 

    dest[1] = src[0] * matrix[4] + src[1] * matrix[5] + 
       src[2] * matrix[6] + src[3] * matrix[7]; 

    dest[2] = src[0] * matrix[8] + src[1] * matrix[9] + 
       src[2] * matrix[10] + src[3] * matrix[11]; 

    dest[3] = src[0] * matrix[12] + src[1] * matrix[13] + 
       src[2] * matrix[14] + src[3] * matrix[15]; 

    src += 4; 
    dest += 4; 
    } 
} 

float srcdata[4*10000]; 
float dstdata[4*10000]; 

int main (int argc, char**args) 
{ 
    int i,j; 
    float matrix[16]; 

    // init all source-data, so we don't get NANs 
    for (i=0; i<16; i++) matrix[i] = 1; 
    for (i=0; i<4*10000; i++) srcdata[i] = i; 

    // do a bunch of tests for benchmarking. 
    for (j=0; j<10000; j++) 
    transform (dstdata, srcdata, matrix, 10000); 
} 

Ergebnisse: (auf meinem 2 Ghz Core Duo)

[email protected]:~$ gcc -O3 test.c 
[email protected]:~$ time ./a.out 

real 0m2.517s 
user 0m2.516s 
sys  0m0.004s 

[email protected]:~$ gcc -O3 -DUSE_RESTRICT test.c 
[email protected]:~$ time ./a.out 

real 0m2.034s 
user 0m2.028s 
sys  0m0.000s 

über den Daumen 20% Ausführung schneller, auf dass System.

Um zu zeigen, wie viel sie von der Architektur hängt ich die gleichen Code läuft auf einem Cortex-A8 CPU eingebettet lassen habe (bereinigt die Schleife ein wenig zählen, weil ich warten will nicht so lange):

[email protected]:~# gcc -O3 -mcpu=cortex-a8 -mfpu=neon -mfloat-abi=softfp test.c 
[email protected]:~# time ./a.out 

real 0m 7.64s 
user 0m 7.62s 
sys  0m 0.00s 

[email protected]:~# gcc -O3 -mcpu=cortex-a8 -mfpu=neon -mfloat-abi=softfp -DUSE_RESTRICT test.c 
[email protected]:~# time ./a.out 

real 0m 7.00s 
user 0m 6.98s 
sys  0m 0.00s 

Hier ist der Unterschied liegt nur 9% (gleicher Compiler btw.)

+2

Gute Arbeit. Es gibt einen Artikel über die Verwendung von restrict auf einem Cell-Prozessor hier: http://cellperformance.beyond3d.com/articles/2006/05/demystifying-the-restrict-keyword.html, das für die Diskussion Architektur spezifische Vorteile relevant sein kann . – Clifford

+0

@Nils Pipenbrinck: Warum musst du das Inlining für die Funktion deaktivieren? Es scheint eine furchtbar große Funktion zu sein, damit der Compiler automatisch inline arbeitet. –

+2

@Nils Pipenbrinck: Übrigens hat Ulrich Drepper im Rahmen seiner Diskussion über die Optimierung von Cache- und Speichernutzung mehrfach Code für eine superoptimierte Matrix gepostet. Es ist hier: http://lwn.net/Articles/258188/. Seine Diskussion über jeden Schritt, den er durchführte, um zu dieser Lösung zu gelangen, ist hier: http://lwn.net/Articles/255364/. Er konnte die Ausführungszeit um 90% gegenüber einem Standard-MM reduzieren. –

0

Ich testete this C-Programm. Ohne restrict dauerte es 12.640 Sekunden, mit restrict 12.516 abzuschließen. Sieht aus wie es kann speichern einige Zeit.

+23

Diese Art von Unterschied ist in das Messrauschen ... –

+0

Dieser Unterschied ist fast sicher unbedeutend, aber Sie sollten auch c als eingeschränkt deklarieren, da jedes Mal, wenn c geschrieben wird, der Compiler in Betracht zieht, dass * a * b und * inc möglicherweise gewesen sind geändert. – James

+0

In Ihrem Beispiel kann das Optimierungsprogramm erkennen, dass die Parameter kein Aliasing haben. Versuchen Sie das Inlining zu deaktivieren und Sie werden einen größeren Unterschied sehen. –

0

Beachten Sie, dass C++ - Compiler, die das Schlüsselwort restrict zulassen, es trotzdem ignorieren können. Das ist zum Beispiel der Fall here.

+0

Eigentlich, wenn Sie die Seite lesen, werden Sie bemerken, dass, während in C++ restricted wegen eines möglichen Konflikts mit Benutzervariablen des gleichen Namens ignoriert wird, '__restrict__' für C++ unterstützt wird. –

+1

@Robert: Und ignoriert. Der Unterschied besteht lediglich darin, dass Bezeichner mit doppeltem Unterstrich für die Systemnutzung reserviert sind. Daher sollte ein \ _ \ _ restrict \ _ \ _ nicht mit den vom Benutzer deklarierten Bezeichnern kollidieren. –

+0

@Martin: Woher weißt du, dass es ignoriert wird? Es ist nicht ganz klar aus der Dokumentation - scheint so, als könnte man es lesen. –

6

der Artikel Demystifying The Restrict Keyword auf das Papier Why Programmer-specified Aliasing is a Bad Idea (pdf) bezieht, die sagt, dass es im allgemeinen nicht und liefert Messungen hilft dies untermauern.

+0

Es gibt viele Arten von Code, wo es wenig Nutzen bietet, aber es gibt einige, wo es einen großen Vorteil bietet. Sind Ihnen irgendwelche Papiere bekannt, die zeigen würden, dass die vernünftige Verwendung von "restrict" keine Vorteile bietet, die größer sind, als diese Compiler durch typenbasiertes Aliasing realisieren können? – supercat

3

Bietet das Schlüsselwort restrict signifikante Vorteile in gcc/g ++?

Es kann die Anzahl von Befehlen zu reduzieren, wie unten am Beispiel gezeigt, so benutzen, wann immer möglich.

GCC 4.8 Linux x86-64

Eingang exmample:

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

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

Compile und decompile:

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

Mit -O0, sie sind die gleichen.

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

Für die Laien der calling convention ist:

  • rdi = erste Parameter
  • rsi = zweite Parameter
  • rdx = dritte Parameter

Fazit: 3 Anweisungen statt 4.

Natürlich, Anweisungen can have different latencies, aber das ist eine gute Idee.

Warum konnte GCC das optimieren?

Der obige Code stammt aus der Wikipedia example, die sehr leuchtet.

Pseudo Anordnung für f:

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 a may be equal to x. 
load R1 ← *x 
load R2 ← *b 
add R2 += R1 
set R2 → *b 

Für fr:

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 
load R2 ← *b 
add R2 += R1 
set R2 → *b 

Ist es wirklich schneller?

ermmm ... nicht für diesen einfachen Test:

.text 
    .global _start 
    _start: 
     mov $0x10000000, %rbx 
     mov $x, %rdx 
     mov $x, %rdi 
     mov $x, %rsi 
    loop: 
     # START of interesting block 
     mov (%rdx),%eax 
     add %eax,(%rdi) 
     mov (%rdx),%eax # Comment out this line. 
     add %eax,(%rsi) 
     # END ------------------------ 
     dec %rbx 
     cmp $0, %rbx 
     jnz loop 
     mov $60, %rax 
     mov $0, %rdi 
     syscall 
.data 
    x: 
     .int 0 

Und dann:

as -o a.o a.S && ld a.o && time ./a.out 

auf Ubuntu 14.04 AMD64 CPU Intel i5-3210M.

Ich gestehe, dass ich immer noch moderne CPUs nicht verstehe.Lassen Sie uns wissen, wenn Sie:

  • einen Fehler in meiner Methode gefunden
  • Assembler Testfall gefunden, wo es viel schneller
  • wird verstehen, warum es keinen Unterschied
  • war
Verwandte Themen