Spunti di ottimizzazione del codice (versione 3/2007)
Elenco di benchmark e idee su come massimizzare le performance senza
dover scendere in linguaggio assembly su compilatori GCC.
I Test sono stati eseguiti sotto Debian Linux con programmi compilati
usando gcc 3.4.6, gcc 4.0.4, gcc 4.1.2, gcc 4.2.1 su un Pentium4
HT a 3.2Ghz, mentre sotto Gentoo per l'icc
9.1.
Salvo dove indicato sono state usate le seguenti opzioni di
compilazione:
gcc 3.4, gcc 4.0, gcc 4.1,
gcc 4.2:
-march=pentium4 -O2
gcc 4.0, gcc 4.1, gcc 4.2 + autovettorizzazione: -march=pentium4 -O2
-ftree-vectorize
-ftree-vectorizer-verbose=5
icc 9.1 : -march=pentium4 -O2
Nota: esiste anche -ftree-vectorizer-verbose=7 :)
ATTENZIONE:
Molti dei pattern che venivano vettorizzati sul gcc4.1.0 , ora con il
gcc 4.1.2 non hanno dato buon fine. Non
vengono vettorizzati perche':
note: not vectorized: can't determine dependence between *D.11390_42
and *D.11388_40
per paura di aliasing (i puntatori di input e di output si
sovrappongono) il codice ora non viene vettorizzato?
Eppure nel gcc
4.1.0 tale problematica non c'era:
[Precedente versione del documento per
GCC4.0 e 4.1.0]
Nelle prossime versioni del gcc probabilmente verra' introdotta la
parola chiave __restricted__ per risolvere tale problema. Esempio:
typedef int aint __attribute__ ((__aligned__(16)));
foo (int n, aint * __restricted__ p, aint * __restricted q) {
/* feature: support for (aligned) pointer accesses. */
while (n--){
*p++ = *q++;
}
}
----
Il simbolo * indica che e' stato vettorizzato il codice nel
test.
1. Introduzione
E' vero che un algoritmo di complessità di ordine inferiore
risulta più veloce di un algoritmo di ordine superiore. E'
altresì vero che un algoritmo di complessità inferiore
ben scritto è più veloce di un algoritmo di
complessità inferiore scritto male.
2 Confronto tra i compilatori
Per settare un buffer di memoria sono possibili diversi costrutti. Con
il GCC 4.0 tali costrutti tuttavia sono finalmente equivalenti (dal
punto di vista delle prestazioni, ma non del codice generato).
2.1 settaggio della memoria
Vediamo ora un primo test per confrontare i diversi compilatori su un
buffer di memoria fisso (65536 bytes)
Codice
|
gcc 3.4
|
gcc 4.0/gcc 4.1
|
gcc 4.0/gcc 4.1 +
autovettorizzazione
|
icc 9.1
|
memset(dst, x, size);
|
7.2 us
|
7.2 us
|
7.2 us
|
|
for(i=0;i<size;i++) dst[i] = x;
|
44 us
|
41 us
|
5.9 us * (4.1.1-21
|
*
|
while(size--) *dst++ = x;
|
43 us
|
38 us
|
5.8 us *
|
*
|
Faccio notare che il gcc 4.0 (e il 4.1) hanno prestazioni simili ora al
gcc 3.3 (se non si auto vettorizza) mentre era il gcc3.4 ad avere
alcuni problemi. La memset non puo' cambiare visto che viene usata la
libreria di sistema che non viene ovviamente ricompilata di volta in
volta. Sia il gcc 4.0 che il 4.1 mostrano la diminuzione dei tempi di
un ordine di grandezza se viene usata l'autovettorizzazione del codice.
La memset e' di diversi ordini di grandezza inferiore, soprattutto per
grandi numeri di byte spostati perche' internamente ha un codice
scritto in assembly che massimizza tale operazione, che solo il codice
autovettorizzato riesce a uguagliare.
Non e' chiaro se viene sfruttato o meno il fatto se la memoria e'
allineata a 16 byte o meno.
2.2 Copia della memoria
Anche se il codice usato negli esempi 2.2 e' simile a quello degli
esempi 2.1 il fatto di usare due buffer sorgenti, distrugge la
possibilita' di vettorizzare il codice.
Vediamo ora un primo test per confrontare i diversi compilatori su un
buffer di memoria fisso (65536 bytes)
Codice
|
gcc 3.4
|
gcc 4.0/gcc 4.1/gcc 4.2
|
gcc 4.1 + autovettorizzazione |
icc 9.1
|
memcpy(dst, src, size);
|
22 us
|
23 us
|
=
|
|
for(i=0;i<size;i++) dst[i] = src[i];
|
78 us
|
78 us
|
4.1.2: 85us / 4.1.0: 10.6us *
|
|
do { *dst++ = *src++; } while(--size)
|
79 us
|
78 us
|
4.1.2: 85us / 4.1.0: 10.6us * |
|
Nel caso della copia di memoria il limite piu che di codice generato e'
un limite fisico del bus e della comunicazione tra cpu e memoria. In
questo caso l'autovettorizzazione sul gcc4.0 non e' andata a buon
termine, mentre sui nuovi pattern del gcc4.1 si, portando a un
significativo aumento delle prestazioni.
2.3 copia di strutture
Nei casi precedenti si trattava di spostare memoria byte per
byte. Diverso discorso va fatto in caso di strutture di dati.
2.4 Pattern per l'autovettorizzazione
su unsigned char
Codice
|
gcc 3.4/gcc 4.0/gcc 4.1
|
gcc 4.0 + autovettorizzazione
|
gcc 4.1 + autovettorizzazione |
a[i] = b[i] + c[i]
|
101us
|
103us
|
102 us
|
a[i] = b[i] * c[i]
|
111us
|
102us
|
115 us
|
a[i] = max(b[i],c[i])
|
155us
|
160us
|
152 us
|
su int
Codice
|
gcc 3.4/gcc 4.0/gcc 4.1
|
gcc 4.0 + autovettorizzazione
|
gcc 4.1 + autovettorizzazione |
a[i] = b[i] + c[i]
|
145us
|
172
|
132 us
|
a[i] = b[i] * c[i]
|
146us
|
174
|
129 us
|
a[i] = max(b[i],c[i])
|
171us
|
181
|
166 us
|
Nessuno di questi pattern ora, anche su gcc4.1.2 ha dato buon fine. Non
vengono vettorizzati perche':
note: not vectorized: can't determine dependence between *D.11390_42
and *D.11388_40
per paura di aliasing (i puntatori di input e di output si
sovrappongono) il codice ora non viene vettorizzato?
Il passaggio da unsigned char a int mostra comunque che buona parte dei
tempi sono dovuti a spostamento di memoria.
I tempi si riferiscono
comunque a 65536 elementi (65536 byte e 65536 int rispettivamente,
pertanto 4 volte piu
memoria per gli interi).
3 Vector vs Map
Tempi di accessi a questi due contenitori STL. Ovviamente per Random
Access Memory non ci sono paragoni: il vector vince sempre e in ogni
caso.
Esaminiamo invece il caso di analisi di ricerca di elementi
Aggiungo anche il caso di list, solo per completezza.
Number Of elements>
|
8
|
16
|
64
|
256
|
1024
|
4096
|
vector
|
0.6us
|
1.0us
|
4.3us
|
28.9us
|
355us
|
5621.2 us
|
sorted vector
(binary search)
|
0.7us
|
0.9us
|
3.4us
|
16.1us
|
64.6us
|
289.4 us
|
map
|
0.7us
|
1.2us
|
4.9us
|
23.2us
|
106us
|
421.7 us
|
list
|
0.6us
|
0.9us
|
5.4us
|
59.9us
|
892us
|
26790 us
|
I numeri rappresentano il tempi necessari per trovare <n> volte
gli elementi richiesti con inserimento di tali valori random.
std::set e std::map hanno prestazioni uguali.
I tempi di creazione delle tabelle non sono considerati, perche' si fa
l'ipotesi di tante ricerche e percio' il tempo di creazione e'
trascurabile.
Nota: l'autovettorizzazione non e' stata possibile.
Le conclusioni sono che le STL sono utili, ma con un po' di sano uso di
tali strutture c'e' un guadagno di prestazioni.
La differenza tra `map' e 'sorted vector' e' nel codice della ricerca:
map usando std::map::find mentre per il binary search del codice
scritto da me. Da notae infatti che
l'ordine di grandezza e' lo stesso,
ma con un fattore moltiplicativo di circa 2 tra i due sistemi.
4 Conclusioni
Tra il GCC3.4 e il GCC4.0 non ci sono significative differenze di
prestazioni, tranne che quest'ultimo puo' autovettorizzare il codice
generato.
Il GCC4.1 in piu riconosce piu pattern e permette di migliorare
ulteriormente le performance sul codice generato.
Esistono altri siti di benchmark fatti sicuramente meglio:
GCC Code-Size
Benchmark Environment (CSiBE)
PHP
/ GCC / ICC Benchmark
[Precedente versione del documento per
GCC3.3 e 3.4]