Il limitare parola chiave fornire vantaggi significativi in termini di gcc/g++?
-
21-09-2019 - |
Domanda
Qualcuno ha mai visto i numeri e l'analisi di se o di non utilizzare il C/C++ restrict
parola chiave per gcc/g++ reale, fornisce un significativo aumento di prestazioni in realtà (e non solo in teoria)?
Ho letto vari articoli consigliare / denigrare il suo utilizzo, ma non ho incontrato alcun numeri reali praticamente dimostrando entrambi i lati argomenti.
MODIFICA
So che restrict
non è ufficialmente parte del C++, ma è supportato da alcuni compilatori e ho letto un articolo di Christer Ericson che consiglia il suo utilizzo.
Soluzione
La parola chiave limitare fa la differenza.
Ho visto miglioramenti di fattore di 2 e più in alcune situazioni (elaborazione di immagini). Il più delle volte la differenza non è così grande però. Circa 10%.
Ecco un piccolo esempio che illustrano la differenza. Ho scritto un 4x4 vettore * matrice molto semplice trasformare come un test. Si noti che devo forzare la funzione di non essere inline. In caso contrario, GCC rileva che non ci sono indicazioni di aliasing nel mio codice di riferimento e limitano non sarebbe fare la differenza a causa di inline.
Ho potuto spostare la funzione di trasformazione in un file diverso pure.
#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);
}
Risultati: (sul mio 2 Ghz Core 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
Nel corso del pollice di esecuzione del 20% più veloce, il che sistema.
Per mostrare quanto dipende l'architettura ho lasciato lo stesso codice eseguito su una CPU incorporato Cortex-A8 (aggiustato contare il ciclo un po 'perché io non voglio aspettare così a lungo):
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
Qui la differenza è solo il 9% (lo stesso compilatore btw.)
Altri suggerimenti
Il limitare parola chiave fornire vantaggi significativi in termini di gcc / g++ ?
Si può ridurre il numero di istruzioni, come illustrato nell'esempio riportato di seguito, in modo da utilizzare ogni volta che è possibile.
GCC 4.8 Linux x86-64 exmample
Ingresso:
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;
}
Compilare e decompilare:
gcc -g -std=c99 -O0 -c main.c
objdump -S main.o
Con -O0
, sono le stesse.
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 chi non lo sapesse, il convenzione di chiamata è:
rdi
= primo parametrorsi
= secondo parametrordx
= terzo parametro
Conclusione: 3 istruzioni invece di 4.
Naturalmente, istruzioni può avere diverse latenze, ma questo dà una buona idea.
Perché GCC è stato in grado di ottimizzare?
Il codice di cui sopra è stata scattata dall' Wikipedia esempio che è molto illuminante.
Pseudo assemblea per 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
Per 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
È davvero più veloce?
Ermmm...non per questo semplice 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
E poi:
as -o a.o a.S && ld a.o && time ./a.out
su Ubuntu 14.04 AMD64 CPU Intel i5-3210M.
Confesso che non ho ancora capito moderne Cpu.Fatemi sapere se avete:
- trovare un difetto a mio metodo
- trovato un assemblatore di test case in cui diventa molto più veloce
- capire perché non c'era una differenza
L'articolo Demystifying Limita Chiave si riferisce alla carta Perché programmatore-specificato Aliasing è un Bad Idea ( pdf), che dice che in genere non aiuta e fornisce misurazioni per eseguire questa funzione.
Si noti che i compilatori C ++ che consentono la parola restrict
può ancora ignorarlo. Questo è il caso, ad esempio qui .
questo C-Programma. Senza restrict
ci sono voluti 12.640 secondi per completare, con restrict
12,516. Sembra che possono risparmiare alcuni di tempo.