Concorrência massiva e de hardware são os tópicos mais importantes do século XXI. Existem várias boas razões para isso, e uma bastante triste.
Duas boas razões: uma combinação de excelente desempenho da GPU nos jogos e, ao mesmo tempo, seu inesperado uso lateral em treinamento profundo em IA, uma vez que o paralelismo maciço é implementado no nível de hardware. A triste razão é que a velocidade dos sistemas uniprocessadores repousa contra as leis da física desde 2006. Os problemas atuais com vazamentos e falhas térmicas limitam bastante o aumento da frequência do relógio, e a queda de tensão clássica agora enfrenta sérios problemas com ruído quântico.
Competindo pela atenção do público, os fabricantes de processadores estão tentando inserir cada vez mais núcleos em cada chip, divulgando o desempenho geral teórico. Os esforços de transporte e métodos de execução especulativos, que usam multithreading sob o capô, também estão crescendo rapidamente, de modo que um único processador visível para o programador possa processar as instruções mais rapidamente.
A verdade inconveniente é que muitas de nossas tarefas de computação menos fascinantes simplesmente não podem
usar muito bem o multithreading visível. Existem várias razões para isso, que têm consequências diferentes para o programador, e há muita confusão. Neste artigo, quero esclarecer um pouco a situação.
Primeiro, você precisa entender claramente onde o paralelismo de hardware funciona melhor e por quê. Vejamos os cálculos para gráficos, redes neurais, processamento de sinal e mineração de bitcoin. Existe um padrão: os algoritmos de paralelização funcionam melhor em equipamentos que são (a) especialmente projetados para executá-los; (b) não pode fazer mais nada!
Também vemos que a entrada para os algoritmos paralelos mais bem-sucedidos (classificação, correspondência de cadeias, transformação rápida de Fourier, operações de matriz, quantização inversa de imagens etc.) parece bastante semelhante. Como regra, eles têm uma estrutura métrica e a diferença entre dados “próximos” e “distantes” é implícita, o que nos permite dividi-los em partes, uma vez que a conexão entre os elementos distantes é insignificante.
Em termos do último artigo sobre localidade semântica, podemos dizer que métodos paralelos são principalmente aplicáveis onde os dados têm boa localidade. E eles funcionam melhor em equipamentos que suportam apenas conexões de "curto alcance", como a matriz sistólica no coração da GPU.
Por outro lado, é muito difícil escrever um software que produza efetivamente essa seção para dados de entrada com baixa localidade em computadores de uso geral (arquitetura von Neumann).
Como resultado, podemos formular uma heurística simples: as
chances de usar a computação paralela são inversamente proporcionais ao grau de não localidade semântica irredutível nos dados de entrada.Outra limitação da computação paralela é que alguns algoritmos importantes não podem ser paralelados de maneira alguma - mesmo teoricamente. Quando discuti esse tópico no meu blog, criei o termo “algoritmo doente”, em que SICK significa “Serial, intrinsecalmente - lide, garoto!” Exemplos significativos incluem: algoritmo de Dijkstra para encontrar o caminho mais curto; detecção de ciclos em gráficos direcionados (usando 3-SAT em solucionadores); pesquisa profunda; computar o enésimo membro na cadeia criptográfica de hash; otimização do fluxo de rede ... e esta não é uma lista completa.
A má localização dos dados de entrada também desempenha um papel aqui, especialmente nos contextos do gráfico e da estrutura em árvore. Cadeias de hash criptográficas não podem ser paralelizadas, porque os registros são calculados em ordem estrita - essa é realmente uma regra importante para proteger a cadeia de falsificações.
E aqui entra a trava: você não pode paralelizar nada enquanto o algoritmo SICK está funcionando.
Nós não terminamos. Existem pelo menos duas classes de obstáculos, e os mais comuns.
Em primeiro lugar, não há ferramentas necessárias. A maioria dos idiomas não suporta nada além de um mutex e semáforos. Isso é conveniente, as primitivas são fáceis de implementar, mas essa situação causa terríveis explosões de complexidade na cabeça: é quase impossível compreender a escala de mais de quatro bloqueios em interação.
Se você tiver sorte, receberá um conjunto de primitivas mais complacentes, como canais Go (também conhecidos como Processos Sequenciais de Comunicação) ou o sistema Rust de propriedade / envio / sincronização. Mas, na verdade, não sabemos qual é a linguagem "correta" das primitivas para a implementação do paralelismo na arquitetura von Neumann. Talvez não haja sequer um conjunto correto de primitivos. Talvez dois ou três conjuntos diferentes sejam adequados para diferentes áreas problemáticas, mas são incomensuráveis como uma unidade e uma raiz quadrada de dois. Até o momento, em 2018, ninguém realmente sabe.
E a última limitação, mas não menos importante, é o cérebro humano. Mesmo em um algoritmo claro, com boa localidade dos dados e ferramentas eficientes, a programação paralela é simplesmente
difícil para as pessoas, mesmo que o algoritmo seja aplicado de maneira simples. Nosso cérebro não modela muito bem os espaços de estado mais simples de programas puramente seqüenciais, e especialmente os paralelos.
Sabemos disso porque há muitas evidências reais de que a depuração de código paralelo é mais do que difícil. Isso é prejudicado por condições de corrida, impasses, bloqueios autodestrutivos, corrupção de dados traiçoeira devido a uma ordem de instruções um pouco insegura.
Penso que a compreensão dessas limitações se torna mais importante após o colapso da
lei de escalar Dennard . Devido a todos esses gargalos na programação, parte dos sistemas com vários núcleos sempre executa software que não é capaz de carregar equipamentos com 100% da capacidade de computação. Se você olhar do outro lado, temos excesso de ferro para as tarefas atuais. Quanto dinheiro e esforço estamos desperdiçando?
Os fabricantes de processadores desejam que você superestime os benefícios funcionais de novos chips inteligentes com ainda mais núcleos. De que outra forma eles podem arrecadar dinheiro para cobrir custos de produção gigantescos, mantendo-se lucrativos? O marketing está fazendo o melhor possível para que você nunca se pergunte quais tarefas são realmente benéficas para esse multithreading.
Honestamente, existem essas tarefas. Servidores em datacenters que processam centenas de milhares de transações simultâneas por segundo provavelmente distribuirão a carga razoavelmente bem entre os núcleos. Smartphones ou sistemas embarcados também - em ambos os casos, são feitos esforços significativos para minimizar o custo e o consumo de energia, o que dificulta o consumo de energia em excesso.
Mas para usuários comuns de desktop e laptop? Duvidas vagas me atormentam. É difícil entender a situação aqui, porque o aumento real da produtividade vem de outros fatores, como a transição do HDD para o SSD. Tais conquistas são facilmente confundidas com o efeito de acelerar a CPU, se você não realizar um perfil completo.
Aqui estão as razões para tais suspeitas:
- A computação paralela séria em computadores desktop / laptop ocorre apenas na GPU.
- Mais de dois núcleos em um processador geralmente são inúteis. Os sistemas operacionais podem distribuir os fluxos de aplicativos, mas o software típico não é capaz de usar o paralelismo, e a maioria dos usuários raramente consegue iniciar simultaneamente um grande número de aplicativos diferentes que consomem muitos recursos da CPU para carregar totalmente seus equipamentos.
- Consequentemente, a maioria dos sistemas quad-core na maioria das vezes não faz nada além de gerar calor.
Entre meus leitores, há muitas pessoas que provavelmente poderão comentar razoavelmente essa hipótese. É interessante ver o que eles dizem.
ATUALIZAÇÃO O comentarista do G + apontou um benefício interessante dos processadores com vários núcleos: eles compilam o código muito rapidamente. O código fonte de idiomas como C possui boa localidade: aqui, unidades bem separadas (arquivos de origem) são compiladas em arquivos de objeto, que o vinculador combina.