Isso me deixou imaginando a importância do Multithreading no cenário atual do setor.
Em campos críticos de desempenho, nos quais o desempenho não é proveniente de códigos de terceiros que realizam trabalhos pesados, mas sim os nossos, então eu tenderia a considerar as coisas nesta ordem de importância da perspectiva da CPU (GPU é um curinga que ganhei entrar):
- Eficiência de memória (ex: localidade de referência).
- Algorítmico
- Multithreading
- SIMD
- Outras otimizações (dicas de previsão de ramificação estática, por exemplo)
Observe que esta lista não se baseia apenas na importância, mas em muitas outras dinâmicas, como o impacto que elas causam na manutenção, o quanto elas são diretas (se não, vale a pena considerar com mais antecedência), suas interações com outras pessoas da lista etc.
Eficiência de memória
A maioria pode se surpreender com a minha escolha de eficiência de memória em vez de algorítmica. Isso ocorre porque a eficiência da memória interage com todos os outros 4 itens desta lista, e é porque a consideração está frequentemente na categoria "design" e não na categoria "implementação". É certo que existe um problema de galinha ou ovo aqui, pois entender a eficiência da memória geralmente requer considerar todos os 4 itens da lista, enquanto todos os outros 4 itens também exigem considerar a eficiência da memória. No entanto, está no coração de tudo.
Por exemplo, se precisarmos de uma estrutura de dados que ofereça acesso seqüencial em tempo linear e inserções em tempo constante na parte traseira e nada mais para pequenos elementos, a ingênua opção aqui a ser alcançada seria uma lista vinculada. Isso desconsidera a eficiência da memória. Quando consideramos a eficiência da memória no mix, acabamos escolhendo estruturas mais contíguas nesse cenário, como estruturas baseadas em matrizes crescíveis ou nós mais contíguos (por exemplo: um que armazena 128 elementos em um nó) vinculados ou, no mínimo, uma lista vinculada apoiada por um alocador de pool. Eles têm uma vantagem dramática, apesar de terem a mesma complexidade algorítmica. Da mesma forma, geralmente escolhemos a classificação rápida de uma matriz e a classificação de mesclagem, apesar de uma complexidade algorítmica inferior, simplesmente devido à eficiência da memória.
Da mesma forma, não podemos ter multithreading eficiente se nossos padrões de acesso à memória são tão granulares e dispersos por natureza que acabamos maximizando a quantidade de compartilhamento falso enquanto bloqueamos nos níveis mais granulares do código. Portanto, a eficiência da memória multiplica a multithreading de eficiência. É um pré-requisito para aproveitar ao máximo os threads.
Cada item acima na lista tem uma interação complexa com os dados, e o foco na forma como os dados são representados é, em última análise, o objetivo da eficiência da memória. Cada um dos itens acima pode ter gargalos com uma maneira inadequada de representar ou acessar dados.
Outra eficiência da memória razão é tão importante é que ele pode aplicar em toda a toda a base de código. Geralmente, quando as pessoas imaginam que as ineficiências se acumulam a partir de pequenas seções de trabalho aqui e ali, é um sinal de que elas precisam pegar um criador de perfil. No entanto, os campos de baixa latência ou aqueles que lidam com hardware muito limitado encontrarão, mesmo após a criação de perfil, sessões que não indicam pontos de acesso claros (apenas algumas vezes dispersas por todo o lugar) em uma base de código que é flagrantemente ineficiente na maneira como aloca, copia e acessando a memória. Normalmente, essa é a única vez que uma base de código inteira pode ser suscetível a uma preocupação de desempenho que pode levar a um novo conjunto de padrões aplicados em toda a base de código, e a eficiência da memória geralmente está no centro dela.
Algorítmico
Esse é praticamente um dado, pois a escolha em um algoritmo de classificação pode fazer a diferença entre uma entrada massiva que leva meses para classificar versus segundos para classificar. Causa o maior impacto de todos, se a escolha for entre, digamos, algoritmos quadráticos ou cúbicos realmente abaixo do par e um linearitmico, ou entre linear e logarítmico ou constante, pelo menos até termos mais de 1.000.000 de máquinas principais (nesse caso, memória eficiência se tornaria ainda mais importante).
No entanto, ele não está no topo da minha lista pessoal, já que alguém competente em seu campo saberia usar uma estrutura de aceleração para a seleção de frustum, por exemplo, estamos saturados de conhecimento algorítmico e saber coisas como usar uma variante de um trie como uma árvore de raiz para pesquisas baseadas em prefixos é coisa de bebê. Na falta desse tipo de conhecimento básico do campo em que estamos trabalhando, a eficiência algorítmica certamente chegaria ao topo, mas muitas vezes a eficiência algorítmica é trivial.
Também inventar novos algoritmos pode ser uma necessidade em alguns campos (por exemplo: no processamento de malha, tive que inventar centenas, pois eles não existiam antes ou as implementações de recursos semelhantes em outros produtos eram segredos de propriedade, não publicados em um artigo ) No entanto, uma vez que passamos pela parte da solução de problemas e encontramos uma maneira de obter os resultados corretos, e uma vez que a eficiência se torna o objetivo, a única maneira de realmente obtê-la é considerar como estamos interagindo com os dados (memória). Sem entender a eficiência da memória, o novo algoritmo pode se tornar desnecessariamente complexo com esforços fúteis para torná-lo mais rápido, quando a única coisa necessária era uma consideração um pouco mais da eficiência da memória para gerar um algoritmo mais simples e elegante.
Por fim, os algoritmos tendem a estar mais na categoria "implementação" do que na eficiência da memória. Em geral, é mais fácil melhorar em retrospectiva, mesmo com um algoritmo abaixo do ideal usado inicialmente. Por exemplo, um algoritmo inferior de processamento de imagem geralmente é implementado em um local local na base de código. Pode ser trocado por um melhor mais tarde. No entanto, se todos os algoritmos de processamento de imagem estiverem vinculados a uma Pixel
interface que tenha uma representação de memória abaixo do ideal, mas a única maneira de corrigi-lo é alterar a maneira como vários pixels são representados (e não um único), então geralmente SOL e terá que reescrever completamente a base de código para umImage
interface. O mesmo tipo de coisa vale para substituir um algoritmo de classificação - geralmente é um detalhe de implementação, enquanto uma alteração completa na representação subjacente dos dados que estão sendo classificados ou na maneira como eles passam pelas mensagens pode exigir que as interfaces sejam redesenhadas.
Multithreading
O multithreading é difícil no contexto do desempenho, pois é uma otimização em nível micro que atende às características do hardware, mas nosso hardware está realmente escalando nessa direção. Já tenho colegas que têm 32 núcleos (só tenho 4).
No entanto, o multithreading está entre as micro-otimizações mais perigosas, provavelmente conhecidas por um profissional, se o objetivo for usado para acelerar o software. A condição de corrida é praticamente o bug mais mortal possível, pois é de natureza tão indeterminista (talvez apenas aparecendo uma vez a cada poucos meses na máquina de um desenvolvedor no momento mais inconveniente fora do contexto de depuração, se houver). Portanto, tem sem dúvida a degradação mais negativa na capacidade de manutenção e potencial correção de código entre todas elas, especialmente porque os erros relacionados ao multithreading podem facilmente voar sob o radar, mesmo nos testes mais cuidadosos.
No entanto, está se tornando tão importante. Embora nem sempre possa superar algo como a eficiência da memória (que às vezes pode tornar as coisas cem vezes mais rápidas), dado o número de núcleos que temos agora, estamos vendo cada vez mais núcleos. É claro que, mesmo em máquinas com 100 núcleos, eu ainda colocaria a eficiência da memória no topo da lista, pois a eficiência do encadeamento é geralmente impossível sem ela. Um programa pode usar uma centena de threads em uma máquina assim e ainda ser lento, sem representação eficiente da memória e padrões de acesso (que se vincularão aos padrões de bloqueio).
SIMD
O SIMD também é um pouco estranho, pois os registros estão realmente ficando mais amplos, com planos de se tornar ainda mais amplos. Originalmente, vimos registros MMX de 64 bits seguidos por registros XMM de 128 bits, capazes de realizar 4 operações SPFP em paralelo. Agora estamos vendo registros YMM de 256 bits com capacidade para 8 em paralelo. E já existem planos para registros de 512 bits que permitiriam 16 em paralelo.
Eles interagem e se multiplicam com a eficiência do multithreading. No entanto, o SIMD pode degradar a capacidade de manutenção tanto quanto o multithreading. Embora os bugs relacionados a eles não sejam necessariamente tão difíceis de reproduzir e corrigir como uma condição de impasse ou corrida, a portabilidade é incômoda e garantir que o código possa ser executado na máquina de todos (e usar as instruções apropriadas com base em seus recursos de hardware) é estranho.
Outra coisa é que, embora os compiladores hoje em dia geralmente não batam o código SIMD escrito por especialistas, eles vencem tentativas ingênuas facilmente. Eles podem melhorar até o ponto em que não precisamos mais fazê-lo manualmente, ou pelo menos sem sermos tão manuais a ponto de escrever códigos intrínsecos ou de montagem direta (talvez apenas um pouco de orientação humana).
Novamente, porém, sem um layout de memória eficiente para o processamento vetorizado, o SIMD é inútil. Acabaremos apenas carregando um campo escalar em um registro amplo apenas para fazer uma operação nele. No centro de todos esses itens, há uma dependência dos layouts de memória para ser realmente eficiente.
Outras otimizações
Isso é frequentemente o que eu sugeriria que começássemos a chamar de "micro" hoje em dia se a palavra sugerir não apenas ir além do foco algorítmico, mas também em direção a mudanças que tenham um impacto minúsculo no desempenho.
Muitas vezes, tentar otimizar a previsão de ramificação requer uma alteração na eficiência do algoritmo ou da memória, por exemplo, se isso for tentado apenas através de dicas e reorganização do código para previsão estática, isso só tende a melhorar a execução inicial desse código, tornando os efeitos questionáveis se nem sempre é insignificante.
Voltar para Multithreading for Performance
Enfim, qual a importância da multithreading de um contexto de desempenho? Na minha máquina de 4 núcleos, o ideal é tornar as coisas cerca de 5 vezes mais rápidas (o que posso obter com o hyperthreading). Seria consideravelmente mais importante para o meu colega que tem 32 núcleos. E isso se tornará cada vez mais importante nos próximos anos.
Então é muito importante. Mas é inútil apenas lançar um monte de threads no problema se a eficiência da memória não existir para permitir que os bloqueios sejam usados com moderação, para reduzir o compartilhamento falso etc.
Multithreading fora do desempenho
Multithreading nem sempre é sobre puro desempenho, em um sentido direto da taxa de transferência. Às vezes, é usado para equilibrar uma carga, mesmo com o custo possível da taxa de transferência, para melhorar a capacidade de resposta ao usuário ou permitir que o usuário faça mais multitarefas sem esperar que as coisas terminem (por exemplo: continue navegando enquanto faz o download de um arquivo).
Nesses casos, eu sugiro que o multithreading suba ainda mais para o topo (possivelmente até acima da eficiência da memória), já que trata-se de design do usuário final, e não de tirar o máximo proveito do hardware. Muitas vezes, ele domina os designs de interface e a maneira como estruturamos toda a nossa base de código em tais cenários.
Quando não estamos simplesmente paralelizando um loop restrito acessando uma estrutura de dados massiva, o multithreading vai para a categoria "design" realmente incondicional, e o design sempre supera a implementação.
Portanto, nesses casos, eu diria que considerar o multithreading inicial é absolutamente crítico, ainda mais do que a representação e o acesso à memória.