Perché il mio programma è lento quando ricopro esattamente 8192 elementi?


755

Ecco l'estratto del programma in questione. La matrice img[][]ha le dimensioni SIZE × SIZE ed è inizializzata a:

img[j][i] = 2 * j + i

Quindi, crei una matrice res[][]e ogni campo qui creato diventa la media dei 9 campi circostanti nella matrice img. Il bordo è lasciato a 0 per semplicità.

for(i=1;i<SIZE-1;i++) 
    for(j=1;j<SIZE-1;j++) {
        res[j][i]=0;
        for(k=-1;k<2;k++) 
            for(l=-1;l<2;l++) 
                res[j][i] += img[j+l][i+k];
        res[j][i] /= 9;
}

Questo è tutto ciò che c'è nel programma. Per completezza, ecco cosa viene prima. Nessun codice viene dopo. Come puoi vedere, è solo un'inizializzazione.

#define SIZE 8192
float img[SIZE][SIZE]; // input image
float res[SIZE][SIZE]; //result of mean filter
int i,j,k,l;
for(i=0;i<SIZE;i++) 
    for(j=0;j<SIZE;j++) 
        img[j][i] = (2*j+i)%8196;

Fondamentalmente, questo programma è lento quando SIZE è un multiplo di 2048, ad esempio i tempi di esecuzione:

SIZE = 8191: 3.44 secs
SIZE = 8192: 7.20 secs
SIZE = 8193: 3.18 secs

Il compilatore è GCC. Da quello che so, questo è a causa della gestione della memoria, ma non so davvero molto su quell'argomento, motivo per cui sto chiedendo qui.

Anche come risolvere questo sarebbe bello, ma se qualcuno potesse spiegare questi tempi di esecuzione sarei già abbastanza felice.

Conosco già malloc / free, ma il problema non è la quantità di memoria utilizzata, è solo un tempo di esecuzione, quindi non so come sarebbe di aiuto.


67
@bokan succede quando la dimensione è un multiplo del passo critico della cache.
Luchian Grigore,

5
@Mysticial, non importa, espone lo stesso identico problema; il codice può essere diverso, ma fondamentalmente entrambe le domande si pongono allo stesso tempo (ei loro titoli sono decisamente simili).
Griwes,

33
Non è necessario elaborare l'immagine utilizzando un array di 2 dimensioni se si desidera prestazioni elevate. Considera tutti i pixel che si trovano in un raw e li elabora come un array a una dimensione. Fai questa sfocatura in due passaggi. Per prima cosa aggiungi il valore dei pixel circostanti usando una somma scorrevole di 3 pixel: slideSum + = src [i + 1] -src [i-1]; dest [i] = slideSum ;. Quindi fai lo stesso in verticale e dividi allo stesso tempo: dest [i] = (src [i-width] + src [i] + src [i + width]) / 9. www-personal.engin.umd.umich.edu/~jwvm/ece581/18_RankedF.pdf
bokan

8
In realtà ci sono due cose che stanno succedendo qui. Non è solo un super allineamento.
Mistico

7
(Solo un piccolo pignolo sulla tua risposta. Per il primo segmento di codice, sarebbe bello se tutti i tuoi loop for avessero delle parentesi graffe.)
Trevor Boyd Smith

Risposte:


954

La differenza è causata dallo stesso problema di superallineamento dalle seguenti domande correlate:

Ma è solo perché c'è un altro problema con il codice.

A partire dal loop originale:

for(i=1;i<SIZE-1;i++) 
    for(j=1;j<SIZE-1;j++) {
        res[j][i]=0;
        for(k=-1;k<2;k++) 
            for(l=-1;l<2;l++) 
                res[j][i] += img[j+l][i+k];
        res[j][i] /= 9;
}

In primo luogo notare che i due anelli interni sono banali. Possono essere srotolati come segue:

for(i=1;i<SIZE-1;i++) {
    for(j=1;j<SIZE-1;j++) {
        res[j][i]=0;
        res[j][i] += img[j-1][i-1];
        res[j][i] += img[j  ][i-1];
        res[j][i] += img[j+1][i-1];
        res[j][i] += img[j-1][i  ];
        res[j][i] += img[j  ][i  ];
        res[j][i] += img[j+1][i  ];
        res[j][i] += img[j-1][i+1];
        res[j][i] += img[j  ][i+1];
        res[j][i] += img[j+1][i+1];
        res[j][i] /= 9;
    }
}

Ciò lascia i due anelli esterni a cui siamo interessati.

Ora possiamo vedere che il problema è lo stesso in questa domanda: perché l'ordine dei loop influenza le prestazioni quando si scorre su un array 2D?

Stai iterando la matrice per colonna invece che per riga.


Per risolvere questo problema, è necessario scambiare i due loop.

for(j=1;j<SIZE-1;j++) {
    for(i=1;i<SIZE-1;i++) {
        res[j][i]=0;
        res[j][i] += img[j-1][i-1];
        res[j][i] += img[j  ][i-1];
        res[j][i] += img[j+1][i-1];
        res[j][i] += img[j-1][i  ];
        res[j][i] += img[j  ][i  ];
        res[j][i] += img[j+1][i  ];
        res[j][i] += img[j-1][i+1];
        res[j][i] += img[j  ][i+1];
        res[j][i] += img[j+1][i+1];
        res[j][i] /= 9;
    }
}

Questo elimina completamente tutti gli accessi non sequenziali in modo da non avere più rallentamenti casuali su grandi potenze di due.


Core i7 920 a 3,5 GHz

Codice originale:

8191: 1.499 seconds
8192: 2.122 seconds
8193: 1.582 seconds

Anelli esterni scambiati:

8191: 0.376 seconds
8192: 0.357 seconds
8193: 0.351 seconds

217
Noterò anche che srotolare i circuiti interni non ha alcun effetto sulle prestazioni. Il compilatore probabilmente lo fa automaticamente. Li ho srotolati al solo scopo di sbarazzarmi di loro per rendere più facile individuare il problema con i circuiti esterni.
Mistico il

29
E puoi velocizzare questo codice di un altro fattore tre memorizzando nella cache le somme lungo ciascuna riga. Ma questa e altre ottimizzazioni non rientrano nell'ambito della domanda originale.
Eric Postpischil,

34
@ClickUpvote Questo è in realtà un problema hardware (memorizzazione nella cache). Non ha nulla a che fare con la lingua. Se lo provassi in qualsiasi altra lingua che compila o JIT in codice nativo, probabilmente vedresti gli stessi effetti.
Mistico

19
@ClickUpvote: sembri piuttosto fuorviato. Quel "secondo anello" era solo Mistico che svolgeva a mano gli anelli interni. Questo è qualcosa che il tuo compilatore farà quasi sicuramente comunque, e Mystical lo ha fatto solo per rendere più evidente il problema con i loop esterni. Non è assolutamente qualcosa che dovresti preoccuparti di fare da solo.
Lily Ballard,

154
QUESTO è un esempio perfetto di una buona risposta su SO: fa riferimento a domande simili, spiega passo passo come lo hai affrontato, spiega il problema, spiega come FIX il problema, ha un'ottima formattazione e persino un esempio del codice in esecuzione sulla tua macchina. Grazie per il tuo contributo.
MattSayar,

57

I seguenti test sono stati eseguiti con il compilatore Visual C ++ in quanto viene utilizzato dall'installazione predefinita di Qt Creator (suppongo senza flag di ottimizzazione). Quando si utilizza GCC, non c'è alcuna grande differenza tra la versione di Mystical e il mio codice "ottimizzato". Quindi la conclusione è che le ottimizzazioni del compilatore si prendono cura della micro ottimizzazione meglio degli umani (finalmente io). Lascio il resto della mia risposta come riferimento.


Non è efficiente elaborare le immagini in questo modo. È meglio usare matrici a dimensione singola. L'elaborazione di tutti i pixel avviene in un ciclo. L'accesso casuale ai punti potrebbe essere fatto usando:

pointer + (x + y*width)*(sizeOfOnePixel)

In questo caso particolare, è meglio calcolare e memorizzare nella cache la somma di tre gruppi di pixel in orizzontale perché vengono utilizzati tre volte ciascuno.

Ho fatto alcuni test e penso che valga la pena condividerli. Ogni risultato è una media di cinque test.

Codice originale per utente1615209:

8193: 4392 ms
8192: 9570 ms

La versione di Mystical:

8193: 2393 ms
8192: 2190 ms

Due passaggi utilizzando un array 1D: primo passaggio per somme orizzontali, secondo per somma verticale e media. Indirizzamento a due passaggi con tre puntatori e solo incrementi come questo:

imgPointer1 = &avg1[0][0];
imgPointer2 = &avg1[0][SIZE];
imgPointer3 = &avg1[0][SIZE+SIZE];

for(i=SIZE;i<totalSize-SIZE;i++){
    resPointer[i]=(*(imgPointer1++)+*(imgPointer2++)+*(imgPointer3++))/9;
}

8193: 938 ms
8192: 974 ms

Due passaggi usando un array 1D e indirizzando in questo modo:

for(i=SIZE;i<totalSize-SIZE;i++){
    resPointer[i]=(hsumPointer[i-SIZE]+hsumPointer[i]+hsumPointer[i+SIZE])/9;
}

8193: 932 ms
8192: 925 ms

Un passaggio memorizza nella cache le somme orizzontali solo una riga in avanti in modo da rimanere nella cache:

// Horizontal sums for the first two lines
for(i=1;i<SIZE*2;i++){
    hsumPointer[i]=imgPointer[i-1]+imgPointer[i]+imgPointer[i+1];
}
// Rest of the computation
for(;i<totalSize;i++){
    // Compute horizontal sum for next line
    hsumPointer[i]=imgPointer[i-1]+imgPointer[i]+imgPointer[i+1];
    // Final result
    resPointer[i-SIZE]=(hsumPointer[i-SIZE-SIZE]+hsumPointer[i-SIZE]+hsumPointer[i])/9;
}

8193: 599 ms
8192: 652 ms

Conclusione:

  • Nessun vantaggio derivante dall'utilizzo di diversi puntatori e solo incrementi (ho pensato che sarebbe stato più veloce)
  • La memorizzazione nella cache delle somme orizzontali è meglio che calcolarle più volte.
  • Il doppio passaggio non è tre volte più veloce, solo due volte.
  • È possibile ottenere 3,6 volte più velocemente utilizzando sia un singolo passaggio sia la memorizzazione nella cache di un risultato intermedio

Sono sicuro che è possibile fare molto meglio.

NOTA Si noti che ho scritto questa risposta per indirizzare i problemi di prestazioni generali anziché il problema di cache spiegato nella risposta eccellente di Mystical. All'inizio era solo uno pseudo codice. Mi è stato chiesto di fare dei test nei commenti ... Ecco una versione completamente refactored con i test.


9
"Penso che sia almeno 3 volte più veloce": ti va di sostenere tale affermazione con alcune metriche o citazioni?
Adam Rosenfield,

8
@AdamRosenfield "Penso" = supposizione! = "È" = reclamo. Non ho metriche per questo e vorrei vedere un test. Ma il mio richiede 7 incrementi, 2 secondari, 2 add e un div per pixel. Ogni loop utilizza meno var locale di quanto ci sia registro nella CPU. L'altro richiede 7 incrementi, 6 decrementi, 1 div e tra 10 e 20 mul per l'indirizzamento a seconda dell'ottimizzazione del compilatore. Inoltre, ogni istruzione nel loop richiede il risultato dell'istruzione precedente, questo scartare i vantaggi dell'architettura super scalare di Pentiums. Quindi deve essere più veloce.
bokan,

3
La risposta alla domanda originale riguarda gli effetti di memoria e cache. Il motivo per cui il codice di OP è così lento è che il suo modello di accesso alla memoria passa per colonne anziché per righe, che ha una localizzazione di riferimento della cache molto scarsa. È particolarmente negativo a 8192 perché quindi le righe consecutive finiscono per usare le stesse linee di cache in una cache a mappatura diretta o cache con bassa associatività, quindi la percentuale di errori della cache è ancora più alta. L'interscambio dei loop fornisce un enorme incremento delle prestazioni aumentando notevolmente la localizzazione della cache.
Adam Rosenfield,

1
Ben fatto, questi sono alcuni numeri impressionanti. Come hai scoperto, tutto dipende dalle prestazioni della memoria: l'utilizzo di diversi puntatori con incrementi non ha apportato alcun vantaggio.
Adam Rosenfield,

2
@AdamRosenfield Questa mattina ero piuttosto preoccupato perché non riuscivo a riprodurre i test. Sembra che l'aumento delle prestazioni sia solo con il compilatore Visual C ++. Usando gcc, c'è solo una piccola differenza.
bokan,
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.