Por que a adição de arrays é mais rápida em dois loops do que em um: análise de erros C++

Treinador Online de Python para Iniciantes

Aprenda Python facilmente sem sobrecarga teórica. Resolva tarefas práticas com verificação automática e escreva código diretamente no navegador.

Start Course

Por que a adição de arrays é mais rápida em dois loops do que em um? Análise do erro clássico do C++

\n\n

Você já escreveu um código que parecia intuitivamente correto, mas funcionava devagar? E então, ao dividir um loop em dois, obteve uma aceleração de 2 a 3 vezes? Uma dor familiar. No Stack Overflow, há uma pergunta lendária que acumulou milhares de visualizações e centenas de comentários: «Why are elementwise additions much faster in separate loops than in a combined loop?». Hoje vamos analisar esse fenômeno de forma simples, com exemplos de código em C++ e sem teorias complicadas.

\n\n

Isso não é mágica. São cache misses e a forma como o processador realmente acessa a memória. Se você programa em C++ e quer que seu código voe — leia com atenção. No final do artigo, há ações concretas que podem ser aplicadas agora mesmo.

\n\n

O cerne do problema: dois loops vs um

\n\n

Imagine que temos dois grandes arrays a e b, e queremos realizar a adição elemento a elemento e, em seguida, a multiplicação de cada elemento. A intuição sugere: «Vamos fazer um único loop para não ficar alternando o processador!».

\n\n

Veja como fica o loop combinado:

\n\n
const int N = 1000000;\nfloat a[N], b[N];\n\n// Um loop — faz tudo de uma vez\nfor (int i = 0; i < N; ++i) {\n    a[i] = a[i] + b[i];  // adição\n    a[i] = a[i] * 2.0f;  // multiplicação\n}
\n\n

E agora dois loops separados:

\n\n
// Primeiro loop — apenas adição\nfor (int i = 0; i < N; ++i) {\n    a[i] = a[i] + b[i];\n}\n\n// Segundo loop — apenas multiplicação\nfor (int i = 0; i < N; ++i) {\n    a[i] = a[i] * 2.0f;\n}
\n\n

É lógico supor que a primeira opção é mais rápida: menos custos indiretos na organização do loop. Mas na prática, é o contrário — a segunda opção geralmente é 2 a 3 vezes mais rápida. Por quê? Bem-vindo ao mundo da hierarquia de cache.

\n\n

Cache misses: o principal inimigo do desempenho

\n\n

Processadores modernos não acessam a memória RAM diretamente toda vez. Eles usam cache — uma memória rápida localizada no próprio chip. O cache tem vários níveis (L1, L2, L3), e cada nível seguinte é mais lento, mas maior.

\n\n

Quando seu código lê ou escreve dados, o processador carrega um bloco inteiro (cache line) no cache — geralmente 64 bytes. Se a próxima requisição cair no mesmo bloco, é um hit (acerto). Se não, é um miss (erro), e o processador precisa esperar os dados da RAM lenta. Um único miss pode custar centenas de ciclos.

\n\n

Agora, voltemos ao nosso exemplo. No loop combinado, em cada iteração acessamos a[i] e b[i] duas vezes: primeiro para leitura, depois para escrita. Enquanto trabalhamos com a[0] e b[0], o cache carrega suas cache lines. Mas na próxima iteração, vamos para a[1] e b[1] — pode ocorrer um miss se os dados estiverem em linhas de cache diferentes. Como resultado, ficamos constantemente «puxando» a memória, forçando o cache a recarregar.

\n\n

Nos dois loops separados, é diferente. O primeiro loop lê sequencialmente a[i] e b[i] e escreve o resultado de volta. Os dados são lineares — o cache é previsível e funciona perfeitamente (localidade espacial). O segundo loop percorre novamente o mesmo array a — os dados já estão quentes no cache L1. Praticamente não há misses.

\n\n

Principais erros que matam o desempenho

\n\n

Erro nº 1 — misturar operações com diferentes arrays em um único loop. Se você puder dividir a lógica em duas passagens (especialmente com grandes volumes de dados) — faça isso. O cache agradecerá.

\n\n

Erro nº 2 — ignorar o tamanho da cache line. 64 bytes equivalem a aproximadamente 16 floats ou 8 doubles. Se seus arrays têm passos estranhos ou você salta pelos índices de forma não sequencial, você garante cache misses.

\n\n

Erro nº 3

Blogs

Book Recommendations