Le regole di base sono in realtà abbastanza semplici. Dove diventa difficile è come si applicano al tuo codice.
La cache funziona su due principi: località temporale e località spaziale. La prima è l'idea che se di recente hai usato un certo blocco di dati, probabilmente ne avrai bisogno presto. Quest'ultimo significa che se hai usato di recente i dati all'indirizzo X, probabilmente avrai presto bisogno dell'indirizzo X + 1.
La cache cerca di soddisfare questo problema ricordando i blocchi di dati utilizzati più di recente. Funziona con linee di cache, in genere di dimensioni pari a circa 128 byte, quindi anche se è necessario un solo byte, l'intera riga di cache che lo contiene viene inserita nella cache. Quindi, se in seguito hai bisogno del seguente byte, sarà già nella cache.
E questo significa che vorrai sempre che il tuo codice sfrutti il più possibile queste due forme di località. Non saltare tutta la memoria. Fai più lavoro che puoi su una piccola area, quindi passa a quello successivo e fai più lavoro lì che puoi.
Un semplice esempio è l'attraversamento di array 2D mostrato dalla risposta del 1800. Se lo attraversi una riga alla volta, stai leggendo la memoria in sequenza. Se lo fai in base alla colonna, leggerai una voce, quindi salti in una posizione completamente diversa (l'inizio della riga successiva), leggi una voce e salta di nuovo. E quando finalmente torni alla prima riga, non sarà più nella cache.
Lo stesso vale per il codice. Salti o rami significano un utilizzo della cache meno efficiente (perché non stai leggendo le istruzioni in sequenza, ma stai saltando a un indirizzo diverso). Ovviamente, le piccole istruzioni if probabilmente non cambieranno nulla (stai saltando solo pochi byte, quindi finirai ancora all'interno della regione cache), ma le chiamate di funzione in genere implicano che stai saltando in un modo completamente diverso indirizzo che non può essere memorizzato nella cache. A meno che non sia stato chiamato di recente.
L'utilizzo della cache delle istruzioni di solito è tuttavia molto meno problematico. Di solito è necessario preoccuparsi della cache dei dati.
In una struttura o classe, tutti i membri sono disposti in modo contiguo, il che è positivo. In un array, anche tutte le voci sono disposte in modo contiguo. Negli elenchi collegati, ogni nodo è allocato in una posizione completamente diversa, il che è negativo. I puntatori in generale tendono a puntare a indirizzi non correlati, il che probabilmente causerà un errore nella cache se lo si differenzia.
E se vuoi sfruttare più core, può diventare davvero interessante, come al solito, solo una CPU può avere un dato indirizzo nella sua cache L1 alla volta. Quindi, se entrambi i core accedono costantemente allo stesso indirizzo, si tradurranno in costanti errori nella cache, poiché combattono per l'indirizzo.