Frage

Hat jemand jemals Zahlen/Analysen darüber gesehen, ob die Verwendung des C/C ++ verwendet wird oder nicht restrict Das Schlüsselwort in GCC/G ++ bietet tatsächlich einen signifikanten Leistungsschub in der Realität (und nicht nur in der Theorie)?

Ich habe verschiedene Artikel gelesen, in denen die Verwendung empfohlen / abfällt, aber ich habe keine realen Zahlen auf die beiden Seiten der Argumente nachgewiesen.

BEARBEITEN

ich weiß das restrict ist nicht offiziell Teil von C ++, aber es wird von einigen Compilern unterstützt und ich habe ein Papier von gelesen Christer Ericson was dringend seine Verwendung empfiehlt.

War es hilfreich?

Lösung

Das einschränkende Schlüsselwort macht einen Unterschied.

Ich habe in einigen Situationen Verbesserungen von Faktor 2 und mehr gesehen (Bildverarbeitung). Meistens 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 zwingen muss, nicht eingebaut zu werden. Andernfalls erkennt GCC, dass es in meinem Benchmark -Code keine Aliasing -Zeiger gibt, und einschränken würden aufgrund des Einbindung keinen Unterschied machen.

Ich hätte die Transformationsfunktion auch in eine andere Datei verschieben können.

#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 -Kern -Duo)

nils@doofnase:~$ gcc -O3 test.c
nils@doofnase:~$ time ./a.out

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

nils@doofnase:~$ gcc -O3 -DUSE_RESTRICT test.c
nils@doofnase:~$ time ./a.out

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

Über den Daumen um 20% schneller Ausführung, auf das System.

Um zu zeigen, wie sehr es von der Architektur abhängt, habe ich denselben Code auf einer Cortex-A8-eingebetteten CPU ausgeführt (angepasst die Schleifenzahl ein wenig, weil ich nicht so lange warten möchte):

root@beagleboard:~# gcc -O3 -mcpu=cortex-a8 -mfpu=neon -mfloat-abi=softfp test.c
root@beagleboard:~# time ./a.out

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

root@beagleboard:~# gcc -O3 -mcpu=cortex-a8 -mfpu=neon -mfloat-abi=softfp -DUSE_RESTRICT test.c 
root@beagleboard:~# time ./a.out

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

Hier beträgt der Unterschied nur 9% (der gleiche Compiler übrigens).

Andere Tipps

Bietet das einschränkende Schlüsselwort erhebliche Vorteile bei GCC / G ++?

Es kann Reduzieren Sie die Anzahl der Anweisungen, wie im folgenden Beispiel gezeigt. Verwenden Sie sie also nach Möglichkeit.

GCC 4.8 Linux x86-64 Beispielbeispiel

Eingang:

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;
}

Kompilieren und zersetzen:

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

Mit -O0, Sie sind gleich.

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 Uneingeweihten die Konvention anrufen ist:

  • rdi = erster Parameter
  • rsi = Zweiter Parameter
  • rdx = dritter Parameter

Fazit: 3 Anweisungen statt 4.

Natürlich Anweisungen kann unterschiedliche Latenzen haben, aber das gibt eine gute Idee.

Warum konnte GCC das optimieren?

Der obige Code wurde aus dem entnommen Wikipedia -Beispiel welches ist sehr aufschlussreich.

Pseudo -Montage 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

Zum 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?

Ähmmm ... 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 gebe zu, dass ich moderne CPUs immer noch nicht verstehe. Lass es mich wissen wenn du:

  • Ich habe einen Fehler in meiner Methode gefunden
  • fand einen Assembler -Testfall, in dem es viel schneller wird
  • Verstehe, warum es keinen Unterschied gab

Der Artikel Entmystifizierung des eingeschränkten Schlüsselworts bezieht sich auf das Papier Warum Programmierer-spezifiziertes Aliasing eine schlechte Idee ist (PDF), was besagt, dass es im Allgemeinen nicht hilft und Messungen bereitstellt, um dies zu sichern.

Beachten Sie, dass C ++ - Compiler, die das zulassen restrict Schlüsselwort kann es möglicherweise noch ignorieren. Das ist zum Beispiel der Fall hier.

Ich habe getestet Dies C-Programm. Ohne restrict Es dauerte 12,640 Sekunden, bis es fertiggestellt war, mit restrict 12.516. Schaut so aus kann sparen etwas Zeit.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top