Abaixo estão dois programas que são quase idênticos, exceto que eu mudei as variáveis i
e j
. Ambos correm em diferentes quantidades de tempo. Alguém poderia explicar por que isso acontece?
Versão 1
#include <stdio.h>
#include <stdlib.h>
main () {
int i,j;
static int x[4000][4000];
for (i = 0; i < 4000; i++) {
for (j = 0; j < 4000; j++) {
x[j][i] = i + j; }
}
}
Versão 2
#include <stdio.h>
#include <stdlib.h>
main () {
int i,j;
static int x[4000][4000];
for (j = 0; j < 4000; j++) {
for (i = 0; i < 4000; i++) {
x[j][i] = i + j; }
}
}
Respostas:
Como já foi dito, a questão é a loja para o local de memória na matriz:
x[i][j]
. Aqui está um pouco do porquê:Você tem uma matriz bidimensional, mas a memória do computador é inerentemente unidimensional. Então, enquanto você imagina sua matriz assim:
O seu computador armazena-o na memória como uma única linha:
No segundo exemplo, você acessa a matriz fazendo um loop sobre o segundo número primeiro, ou seja:
Significando que você está acertando todos eles em ordem. Agora olhe para a 1ª versão. Voce esta fazendo:
Devido à maneira como C organizou a matriz 2-d na memória, você está pedindo que ela salte por todo o lado. Mas agora para o kicker: Por que isso importa? Todos os acessos à memória são iguais, certo?
Não: por causa dos caches. Os dados da sua memória são trazidos para a CPU em pequenos pedaços (chamados de 'linhas de cache'), normalmente de 64 bytes. Se você tem números inteiros de 4 bytes, significa que você está obtendo 16 números inteiros consecutivos em um pequeno pacote. Na verdade, é bastante lento buscar esses pedaços de memória; sua CPU pode fazer muito trabalho no tempo necessário para carregar uma única linha de cache.
Agora, olhe novamente para a ordem dos acessos: O segundo exemplo é (1) pegar um pedaço de 16 polegadas, (2) modificar todos eles, (3) repetir 4000 * 4000/16 vezes. Isso é agradável e rápido, e a CPU sempre tem algo para trabalhar.
O primeiro exemplo é (1) pegue um pedaço de 16 polegadas, (2) modifique apenas um deles, (3) repita 4000 * 4000 vezes. Isso exigirá 16 vezes o número de "buscas" da memória. Na verdade, sua CPU terá que gastar um tempo esperando que a memória apareça e, enquanto estiver sentado, você estará perdendo um tempo valioso.
Nota importante:
Agora que você tem a resposta, eis uma observação interessante: não há razão inerente para que seu segundo exemplo seja o mais rápido. Por exemplo, no Fortran, o primeiro exemplo seria rápido e o segundo lento. Isso ocorre porque, em vez de expandir as coisas em "linhas" conceituais, como C faz, o Fortran se expande em "colunas", ou seja:
O layout de C é chamado de 'linha principal' e o de Fortran é chamado de 'coluna principal'. Como você pode ver, é muito importante saber se a sua linguagem de programação é de linhas principais ou de colunas! Aqui está um link para mais informações: http://en.wikipedia.org/wiki/Row-major_order
fonte
Nada a ver com montagem. Isso ocorre devido a falhas de cache .
As matrizes multidimensionais C são armazenadas com a última dimensão como a mais rápida. Portanto, a primeira versão perderá o cache em todas as iterações, enquanto a segunda versão não. Portanto, a segunda versão deve ser substancialmente mais rápida.
Veja também: http://en.wikipedia.org/wiki/Loop_interchange .
fonte
A versão 2 será executada muito mais rapidamente porque usa o cache do computador melhor que a versão 1. Se você pensar bem, as matrizes são apenas áreas contíguas da memória. Quando você solicita um elemento em uma matriz, seu sistema operacional provavelmente trará uma página de memória para o cache que contém esse elemento. No entanto, como os próximos elementos também estão nessa página (por serem contíguos), o próximo acesso já estará em cache! É isso que a versão 2 está fazendo para acelerar sua velocidade.
A versão 1, por outro lado, está acessando elementos em colunas, e não em linhas. Esse tipo de acesso não é contíguo no nível da memória; portanto, o programa não pode aproveitar tanto o cache do SO.
fonte
O motivo é o acesso a dados em cache local. No segundo programa, você está digitalizando linearmente a memória, beneficiando do armazenamento em cache e da pré-busca. O padrão de uso de memória do seu primeiro programa é muito mais espalhado e, portanto, apresenta um comportamento de cache pior.
fonte
Além das outras excelentes respostas sobre os acertos do cache, também há uma possível diferença de otimização. Seu segundo loop provavelmente será otimizado pelo compilador em algo equivalente a:
Isso é menos provável para o primeiro loop, porque seria necessário incrementar o ponteiro "p" com 4000 a cada vez.
EDIT:
p++
e até*p++ = ..
pode ser compilado em uma única instrução de CPU na maioria das CPUs.*p = ..; p += 4000
não pode, portanto, há menos benefícios em otimizá-lo. Também é mais difícil, porque o compilador precisa conhecer e usar o tamanho da matriz interna. E não ocorre com frequência no loop interno no código normal (ocorre apenas para matrizes multidimensionais, em que o último índice é mantido constante no loop e o penúltimo no último é escalado), portanto a otimização é menos prioritária .fonte
p += 4000
isop++
i
já é incrementado por um valor não unitário, dado que é um incremento de ponteiro.int *f(int *p) { *p++ = 10; return p; } int *g(int *p) { *p = 10; p += 4000; return p; }
em gcc.godbolt.org . Os dois parecem compilar basicamente o mesmo.Esta linha o culpado:
A segunda versão usa memória contínua, portanto, será substancialmente mais rápida.
Eu tentei com
e o tempo de execução é 13s para a versão1 versus 0,6s para a versão2.
fonte
Eu tento dar uma resposta genérica.
Porque
i[y][x]
é uma abreviação para*(i + y*array_width + x)
C (experimente o eleganteint P[3]; 0[P] = 0xBEEF;
).À medida que você repete
y
, você repete sobre pedaços de tamanhoarray_width * sizeof(array_element)
. Se você tiver isso em seu loop interno, teráarray_width * array_height
iterações sobre esses blocos.Ao inverter a ordem, você terá apenas
array_height
iterações de partes e entre qualquer iteração de partes, você teráarray_width
apenas iteraçõessizeof(array_element)
.Enquanto em CPUs x86 realmente antigas isso não importava muito, hoje em dia o x86 faz muita pré-busca e armazenamento em cache de dados. Você provavelmente produz muitas falhas de cache na sua ordem de iteração mais lenta.
fonte