Considere o seguinte programa de computador muito simples:
for i = 1 to n:
y[i] = x[p[i]]
Aqui e y são n matrizes -element de bytes, e p é um n matriz -element de palavras. Aqui n é grande, por exemplo, n = 2 31 (para que apenas uma fração desprezível dos dados caiba em qualquer tipo de memória cache).
Suponha que consiste em números aleatórios , distribuídos uniformemente entre 1 e n .
Da perspectiva do hardware moderno, isso deve significar o seguinte:
- ler é barato (leitura seqüencial)
- ler é muito caro (leituras aleatórias; quase todas as leituras são falhas de cache; teremos que buscar cada byte individual da memória principal)
- escrever é barato (gravação seqüencial).
E é de fato o que estou observando. O programa é muito lento em comparação com um programa que faz apenas leituras e gravações sequenciais. Ótimo.
Agora vem a pergunta: quão bem esse programa é paralelo às modernas plataformas multinúcleo?
Minha hipótese era que esse programa não se compara bem. Afinal, o gargalo é a memória principal. Um único núcleo já está perdendo a maior parte do tempo apenas aguardando alguns dados da memória principal.
No entanto, não foi isso que observei quando comecei a experimentar alguns algoritmos em que o gargalo era esse tipo de operação!
Simplesmente substituí o loop for ingênuo por um loop for paralelo do OpenMP (em essência, ele apenas dividirá o intervalo em partes menores e executará essas partes em diferentes núcleos da CPU em paralelo).
Em computadores de gama baixa, as acelerações eram de fato menores. Mas em plataformas de ponta, fiquei surpreso por estar recebendo excelentes acelerações quase lineares. Alguns exemplos concretos (os horários exatos podem ser um pouco diferentes, há muitas variações aleatórias; foram apenas experiências rápidas):
2 x Xeon de 4 núcleos (no total 8 núcleos): fator 5-8 acelerações em comparação à versão single-threaded.
Xeon de 2 x 6 núcleos (no total 12 núcleos): acelerações de fator 8 a 14 em comparação com a versão single-threaded.
Agora isso foi totalmente inesperado. Questões:
Precisamente por que esse tipo de programa é tão paralelo ? O que acontece no hardware? (Meu palpite atual é algo assim: as leituras aleatórias de threads diferentes são "canalizadas" e a taxa média de obter respostas a essas perguntas é muito maior do que no caso de um único thread.)
Qual é o modelo teórico correto que poderíamos usar para analisar esse tipo de programa (e fazer previsões corretas do desempenho)?
Edit: Agora há alguns resultados de código-fonte e benchmark disponíveis aqui: https://github.com/suomela/parallel-random-read
- Aproximadamente. 42 ns por iteração (leitura aleatória) com um único encadeamento
- Aproximadamente. 5 ns por iteração (leitura aleatória) com 12 núcleos.
fonte
Decidi experimentar __builtin_prefetch (). Estou postando aqui como resposta, caso outros desejem testá-lo em suas máquinas. Os resultados estão próximos do que Jukka descreve: Cerca de uma diminuição de 20% no tempo de execução ao pré-buscar 20 elementos à frente versus pré-buscar 0 elementos à frente.
Resultados:
Código:
fonte
O acesso DDR3 é realmente canalizado. Os http://www.eng.utah.edu/~cs7810/pres/dram-cs7810-protocolx2.pdf, slides 20 e 24 mostram o que acontece no barramento de memória durante operações de leitura em pipeline.
(parcialmente errado, veja abaixo) Vários encadeamentos não serão necessários se a arquitetura da CPU suportar a pré-busca de cache. O x86 e o ARM modernos, assim como muitas outras arquiteturas, têm uma instrução de pré-busca explícita. Além disso, muitos tentam detectar padrões nos acessos à memória e fazem a pré-busca automaticamente. O suporte ao software é específico do compilador, por exemplo, o GCC e o Clang têm __builtin_prefech () intrínseco à pré-busca explícita.
O hyperthreading no estilo Intel parece funcionar muito bem em programas que passam a maior parte do tempo aguardando falhas no cache. Na minha experiência, na carga de trabalho intensiva em computação, a aceleração vai muito pouco acima do número de núcleos físicos.
EDIT: Eu estava errado no ponto 2. Parece que, enquanto a pré-busca pode otimizar o acesso à memória para um único núcleo, a largura de banda combinada da memória de vários núcleos é maior que a largura de banda do único núcleo. Quanto maior, depende da CPU.
O pré-buscador de hardware e outras otimizações juntas tornam o benchmarking muito complicado. É possível construir casos em que a pré-busca explícita tenha um efeito muito visível ou inexistente no desempenho, sendo este benchmark um dos últimos.
fonte