Em toda linguagem de programação, existem conjuntos de códigos de operação recomendados em detrimento de outros. Eu tentei listá-los aqui, em ordem de velocidade.
- Bit a bit
- Adição / Subtração Inteira
- Multiplicação / Divisão Inteira
- Comparação
- Controle de fluxo
- Adição / Subtração de Bóia
- Multiplicação / Divisão de Bóia
Onde você precisa de código de alto desempenho, o C ++ pode ser otimizado manualmente na montagem, para usar instruções SIMD ou fluxo de controle mais eficiente, tipos de dados, etc. Então, estou tentando entender se o tipo de dados (int32 / float32 / float64) ou a operação utilizado ( *
, +
, &
) afeta o desempenho no nível do CPU.
- Uma única multiplicação é mais lenta na CPU do que uma adição?
- Na teoria do MCU, você aprende que a velocidade dos códigos de operação é determinada pelo número de ciclos de CPU necessários para executar. Então, isso significa que multiplicar leva 4 ciclos e adicionar leva 2?
- Exatamente quais são as características de velocidade dos códigos de operação básicos de matemática e controle?
- Se dois opcodes levam o mesmo número de ciclos para serem executados, então ambos podem ser usados alternadamente sem nenhum ganho / perda de desempenho?
- Quaisquer outros detalhes técnicos que você possa compartilhar sobre o desempenho da CPU x86 são apreciados
c++
performance
optimization
Robinicks
fonte
fonte
Respostas:
Os guias de otimização da Agner Fog são excelentes. Ele tem guias, tabelas de tempos de instrução e documentos sobre a microarquitetura de todos os projetos recentes de CPU x86 (desde o Intel Pentium). Consulte também alguns outros recursos vinculados em /programming//tags/x86/info
Apenas por diversão, responderei algumas das perguntas (números de CPUs Intel recentes). A escolha de operações não é o principal fator na otimização do código (a menos que você possa evitar a divisão).
Sim (a menos que seja por uma potência de 2). (3-4x da latência, com apenas uma taxa de transferência por clock na Intel.) Não se esforce muito para evitá-la, pois ela é tão rápida quanto 2 ou 3 adiciona.
Consulte as tabelas de instruções e o guia de microarquitetura da Agner Fog para saber exatamente : P. Tenha cuidado com saltos condicionais. Saltos incondicionais (como chamadas de função) têm uma pequena sobrecarga, mas não muito.
Não, eles podem competir pela mesma porta de execução que outra coisa, ou não. Depende de quais outras cadeias de dependência a CPU pode estar trabalhando em paralelo. (Na prática, geralmente não há nenhuma decisão útil a ser tomada. Ocasionalmente, é possível usar um deslocamento de vetor ou um embaralhamento de vetor, que são executados em portas diferentes nas CPUs Intel. Porém, deslocamento por bytes de todo o registro (
PSLLDQ
etc.) é executado na unidade aleatória.)Os documentos de microarquitetura da Agner Fog descrevem os pipelines das CPUs Intel e AMD com detalhes suficientes para determinar exatamente quantos ciclos um loop deve levar por iteração e se o gargalo é a taxa de transferência, a cadeia de dependência ou a contenção de uma porta de execução. Veja algumas das minhas respostas no StackOverflow, como esta ou esta .
Além disso, http://www.realworldtech.com/haswell-cpu/ (e similar para projetos anteriores) é uma leitura divertida se você gosta de design de CPU.
Aqui está sua lista, classificada para uma CPU Haswell, com base nos meus melhores hóspedes. Esta não é realmente uma maneira útil de pensar sobre as coisas, mas para ajustar um loop asm. Os efeitos de previsão de cache / ramificação geralmente dominam; portanto, escreva seu código para ter bons padrões. Os números são muito ondulatórios e tentam explicar a alta latência, mesmo que a taxa de transferência não seja um problema, ou a geração de mais uops que entopem o tubo para que outras coisas aconteçam em paralelo. Esp. os números de cache / filial são muito inventados. A latência importa para dependências transportadas por loop, a taxa de transferência importa quando cada iteração é independente.
TL: DR esses números são criados com base no que estou visualizando para um caso de uso "típico", tanto quanto trocas entre latência, gargalos na porta de execução e taxa de transferência de front-end (ou paralisações para coisas como falhas de ramificação) ) Por favor, não use esses números para qualquer tipo de análise de desempenho séria .
shift and rotate (contagem de const em tempo de compilação) /
versões vetoriais de tudo isso (1 a 4 por taxa de transferência de ciclo, latência de 1 ciclo)
tmp += 7
um loop em vez detmp = i*7
)sum
variável. (Eu poderia ponderar isso e fp mul tão baixo quanto 1 ou tão alto quanto 5, dependendo do caso de uso)._mm_insert_epi8
, etc.)y = x ? a : b
, ouy = x >= 0
) (test / setcc
oucmov
)%
por uma constante em tempo de compilação (não potência de 2).PHADD
adicionando valores em um vetor)Eu totalmente inventei isso com base em suposições . Se algo parece errado, é porque eu estava pensando em um caso de uso diferente ou em um erro de edição.
O custo relativo das coisas nos processadores AMD será semelhante, exceto que eles têm shifters inteiros mais rápidos quando a contagem de turnos é variável. As CPUs da família AMD Bulldozer são obviamente mais lentas na maioria dos códigos, por vários motivos. (Ryzen é muito bom em muitas coisas).
Lembre-se de que é realmente impossível resumir as coisas a um custo unidimensional . Além de erros de cache e erros de ramificação, o gargalo em um bloco de código pode ser latência, taxa de transferência total de uop (front-end) ou taxa de transferência de uma porta específica (porta de execução).
Uma operação "lenta" como a divisão FP pode ser muito barata se o código circundante mantiver a CPU ocupada com outros trabalhos . (a div de vetor FP ou o sqrt são 1 uop cada, eles apenas apresentam baixa latência e taxa de transferência. Eles apenas bloqueiam a unidade de divisão, não toda a porta de execução em que está. Div inteiro é vários uops.) Portanto, se você tiver apenas uma divisão de FP para cada ~ 20 mul e acrescentar, e há outro trabalho para a CPU (por exemplo, uma iteração de loop independente), então o "custo" da divisão FP pode ser aproximadamente o mesmo que uma FP mul. Este é provavelmente o melhor exemplo de algo com baixa taxa de transferência quando tudo o que você está fazendo, mas combina muito bem com outro código (quando a latência não é um fator), por causa do baixo total de uops.
Observe que a divisão inteira não é tão amigável com o código circundante: no Haswell, são 9 uops, com um por taxa de transferência de 8 a 11c e latência de 22 a 29c. (A divisão de 64 bits é muito mais lenta, mesmo na Skylake.) Portanto, os números de latência e taxa de transferência são um pouco semelhantes à divisão FP, mas a divisão FP é apenas um uop.
Para exemplos de análise de uma curta sequência de insns para taxa de transferência, latência e total de Uops, consulte algumas das minhas respostas de SO:
sum += x[i] * y[i]
desenrolando-se com vários acumuladores de vetores para ocultar a latência de FMA. É bastante técnico e de baixo nível, mas mostra o tipo de saída em linguagem assembly que você deseja que seu compilador faça e por que isso importa.IDK se outras pessoas escreverem respostas SO, incluindo esse tipo de análise. É muito mais fácil encontrar o meu, porque sei que vou a esse detalhe com frequência e me lembro do que escrevi.
fonte
Depende da CPU em questão, mas para uma CPU moderna a lista é algo como isto:
Dependendo da CPU, pode haver um custo considerável para trabalhar com tipos de dados de 64 bits.
Suas perguntas:
if
que você pode razoavelmente fazer com aritmética.E finalmente, se você estiver fazendo um jogo, não se preocupe muito com tudo isso, concentre-se melhor em fazer um bom jogo do que cortar os ciclos da CPU.
fonte
Fiz um teste sobre operação inteira que fez um loop um milhão de vezes em x64_64, chegou a uma breve conclusão como abaixo,
adicionar --- 116 microssegundos
sub ---- 116 microssegundos
mul ---- 1036 microssegundos
div ---- 13037 microssegundos
os dados acima já reduziram a sobrecarga induzida pelo loop,
fonte
Os manuais do processador intel podem ser baixados gratuitamente em seu site. Eles são bastante grandes, mas tecnicamente podem responder à sua pergunta. O manual de otimização, em particular, é o que você procura, mas o manual de instruções também possui os tempos e latências para a maioria das principais linhas de CPU para obter instruções simd, pois elas variam de chip para chip.
Em geral, eu consideraria ramificações completas, bem como a busca por ponteiros (traverals da lista de links, chamando funções virtuais) como os melhores para os perf killers, mas os cpus x86 / x64 são muito bons em ambos, em comparação com outras arquiteturas. Se você mover para outra plataforma, verá o quanto de um problema pode ser se estiver escrevendo um código de alto desempenho.
fonte