As regras básicas são realmente bastante simples. O problema é como eles se aplicam ao seu código.
O cache funciona em dois princípios: localidade temporal e local espacial. A primeira é a ideia de que, se você usou recentemente um determinado pedaço de dados, provavelmente precisará deles novamente em breve. O último significa que, se você usou recentemente os dados no endereço X, provavelmente precisará em breve do endereço X + 1.
O cache tenta acomodar isso lembrando os pedaços de dados usados mais recentemente. Ele opera com linhas de cache, geralmente com tamanho de 128 bytes, aproximadamente, portanto, mesmo que você precise apenas de um byte, toda a linha de cache que a contém é puxada para o cache. Portanto, se você precisar do seguinte byte depois, ele já estará no cache.
E isso significa que você sempre desejará que seu próprio código explore essas duas formas de localidade o máximo possível. Não pule toda a memória. Faça o máximo de trabalho possível em uma pequena área e, em seguida, passe para a próxima, e faça o máximo de trabalho possível.
Um exemplo simples é o percurso da matriz 2D que a resposta de 1800 mostrou. Se você percorrer uma linha de cada vez, estará lendo a memória sequencialmente. Se você fizer isso em colunas, lerá uma entrada e depois pulará para um local completamente diferente (o início da próxima linha), lerá uma entrada e pulará novamente. E quando você finalmente voltar à primeira linha, ela não estará mais no cache.
O mesmo se aplica ao código. Saltos ou ramificações significam um uso menos eficiente do cache (porque você não está lendo as instruções sequencialmente, mas pulando para um endereço diferente). É claro que pequenas instruções if provavelmente não mudarão nada (você está pulando apenas alguns bytes, portanto ainda vai acabar dentro da região em cache), mas as chamadas de função normalmente implicam que você está pulando para uma posição completamente diferente. endereço que não pode ser armazenado em cache. A menos que tenha sido chamado recentemente.
O uso do cache de instruções geralmente é bem menos problemático. Em geral, você precisa se preocupar com o cache de dados.
Em uma estrutura ou classe, todos os membros são dispostos de forma contígua, o que é bom. Em uma matriz, todas as entradas também são dispostas de forma contígua. Nas listas vinculadas, cada nó é alocado em um local completamente diferente, o que é ruim. Os ponteiros em geral tendem a apontar para endereços não relacionados, o que provavelmente resultará em uma falta de cache se você o derereçar.
E se você quiser explorar vários núcleos, pode ser realmente interessante, como normalmente, apenas uma CPU pode ter um endereço específico no cache L1 de cada vez. Portanto, se os dois núcleos acessarem constantemente o mesmo endereço, isso resultará em constantes falhas de cache, pois eles estão brigando pelo endereço.