Instrução INC vs ADD 1: Isso importa?

Da resposta de Ira Baxter, por que as instruções INC e DEC não afetam a Carry Flag (CF)?

Principalmente, eu fico longe do INC e DEC agora, porque eles fazem atualizações de código de condição parcial, e isso pode causar estranhas barracas no pipeline, e ADD / SUB não. Então, onde não importa (a maioria dos lugares), eu uso ADD / SUB para evitar as barracas. Eu uso INC / DEC apenas quando o código é pequeno, por exemplo, encheckboxndo em uma linha de cache onde o tamanho de uma ou duas instruções faz diferença suficiente para importar. Isto é provavelmente sem sentido nano [literalmente!] – otimização, mas eu sou bastante old-school nos meus hábitos de codificação.

E eu gostaria de perguntar por que isso pode causar barracas no pipeline enquanto add não? Afinal, tanto o ADD quanto o INC atualizam os registros de sinalizadores. A única diferença é que INC não atualiza CF Mas por que isso importa?

Em CPUs modernas, o add nunca é mais lento que o inc (exceto para efeitos indiretos de tamanho de código / decodificação), mas geralmente não é mais rápido, então você deve preferir o inc por razões de tamanho de código . Especialmente se esta escolha é repetida muitas vezes no mesmo binário (por exemplo, se você é um compilador-escritor).

inc economiza 1 byte (modo de 64 bits) ou 2 bytes (opcodes 0x40..F inc r32 / dec r32 forma curta no modo de 32 bits, adaptado como o prefixo REX para x86-64). Isso faz uma pequena diferença percentual no tamanho total do código. Isso ajuda as taxas de acertos do cache de instruções, a taxa de acertos do iTLB e o número de páginas que precisam ser carregadas do disco.

Vantagens do inc :

  • tamanho do código diretamente
  • Não usar um efeito imediato pode ter efeitos de cache uop na família Sandybridge, o que poderia compensar a melhor microfusão do add . (Veja a tabela 9.1 de Agner Fog na seção Sandybridge de seu guia microarquivo .) Os contadores de desempenho podem medir facilmente os uops em estágio de problema, mas é mais difícil medir como as coisas se encheckboxm no cache uop e os efeitos de largura de banda de leitura de cache uop.
  • Deixar CF não modificado é uma vantagem em alguns casos, em CPUs onde você pode ler CF após inc sem uma parada. (Não no Nehalem e antes.)

Há uma exceção entre as CPUs modernas: Silvermont / Goldmont / Knight’s Landing decodifica inc / dec eficientemente como 1 uop, mas se expande para 2 no estágio de atribuição / renomeação (também conhecido como issue). O extra uop mescla sinalizadores parciais. inc taxa de transferência inc é de apenas 1 por clock, contra 0.5c (ou 0.33c Goldmont) para add r32, imm8 independente add r32, imm8 devido à cadeia dep criada pelos uops de mesclagem de flag.

Ao contrário de P4, o resultado do registro não tem um falso-dep em sinalizadores (veja abaixo), portanto a execução fora de ordem leva a união de sinalizadores do caminho crítico de latência quando nada usa o resultado do sinalizador. (Mas a janela do OOO é muito menor do que os processadores mainstream como Haswell ou Ryzen.) A execução de inc em dois uops separados é provavelmente uma vitória para Silvermont na maioria dos casos; a maioria das instruções x86 escreve todas as bandeiras sem lê-las, quebrando essas cadeias de dependência de sinalizadores.

O SMont / KNL tem uma fila entre decodificar e alocar / renomear (veja o manual de otimização da Intel, figura 16-2 ), então expandir para 2 uops durante o problema pode preencher bolhas de baias de decodificação (em instruções como um operando mul ou pshufb , que produzem mais de 1 uop do decodificador e causar uma parada de 3 a 7 ciclos para o microcódigo). Ou no Silvermont, apenas uma instrução com mais de 3 prefixos (incluindo bytes de escape e prefixos obrigatórios), por exemplo, REX + qualquer instrução SSSE3 ou SSE4. Mas observe que há um buffer de loop de ~ 28 uop, portanto loops pequenos não sofrem com esses stalls de decodificação.

inc / dec não são as únicas instruções que decodificam como 1, mas emitem como 2: push / pop , call / ret , e lea com 3 componentes fazem isso também. Assim, o AVX512 do KNL reúne instruções. Fonte: Manual de otimização da Intel , 17.1.2 Motor fora de ordem (KNL). É apenas uma pequena penalidade de throughput (e às vezes nem mesmo isso se algo mais é um gargalo maior), então geralmente é bom usar inc para ajuste “genérico”.


O manual de otimização da Intel ainda recomenda add 1 mais de add 1 inc em geral, para evitar riscos de baias de bandeira parcial. Mas como o compilador da Intel não faz isso por padrão, não é muito provável que os futuros processadores fiquem lentos em todos os casos, como o P4 fez.

O Clang 5.0 e o Intel ICC 17 (no Godbolt) usam inc ao otimizar a velocidade ( -O3 ), não apenas pelo tamanho. -mtune=pentium4 faz com que evitem inc / dec , mas o padrão -mtune=generic não coloca muito peso em P4.

ICC17 -xMIC-AVX512 (equivalente a gcc’s -march=knl ) evita inc , o que provavelmente é uma boa aposta em geral para Silvermont / KNL. Mas normalmente não é um desastre de desempenho usar inc , então provavelmente ainda é apropriado para o ajuste “genérico” usar inc / dec na maioria dos códigos, especialmente quando o resultado do sinalizador não faz parte do caminho crítico.


Com exceção do Silvermont, este é um conselho de otimização mais antigo do Pentium4 . Em CPUs modernas, só há um problema se você realmente ler um flag que não foi escrito pelo último insn que escreveu algum sinalizador. Por exemplo, em BigInteger adc loops. (E nesse caso, você precisa preservar o CF, então usar o add quebraria o seu código.)

add escreve todos os bits de flag de condição no registro EFLAGS. A renomeação de registros torna a gravação fácil apenas para execução fora de ordem: veja riscos de gravação após gravação e gravação após leitura . add eax, 1 e add ecx, 1 pode executar em paralelo porque eles são totalmente independentes um do outro. (Até mesmo o Pentium4 renomeia os bits de sinalizador de condição separados do resto do EFLAGS, uma vez que mesmo add deixa os bits ativados por interrupções e muitos outros não modificados.)

No P4, inc e dec dependem do valor anterior de todos os sinalizadores , portanto eles não podem executar em paralelo uns com os outros ou com instruções de configuração de sinalizadores anteriores. (Por exemplo, add eax, [mem] / inc ecx faz o inc esperar até depois do add , mesmo que o load do add perca no cache.) Isso é chamado de falsa dependência . As gravações de sinalizador parcial funcionam lendo o valor antigo dos sinalizadores, atualizando os bits diferentes de CF e, em seguida, gravando os sinalizadores completos.

Todos os outros CPUs x86 fora de ordem (incluindo os da AMD) renomear partes diferentes de flags separadamente, então internamente eles fazem uma atualização somente de gravação para todos os flags, exceto CF. (fonte: guia de microarquitetura da Agner Fog ). Apenas algumas instruções, como adc ou cmc , leem e escrevem flags. Mas também shl r, cl (veja abaixo).


Casos em que add dest, 1 é preferível a inc dest , pelo menos para famílias Intel P6 / SnB uarch :

  • Memory-destination : add [rdi], 1 pode micro-fusionar o store e o load + add no Intel Core2 e SnB-family , então são dois uops de domínio fundido / 4 uops de domínio não fundido.
    inc [rdi] só pode micro-fundir a loja, então é 3F / 4U.
    De acordo com as tabelas de Agner Fog, a AMD e a Silvermont executam o memory-dest inc e add o mesmo, como um único macro-op / uop.

    Mas cuidado com os efeitos do cache uop com add [label], 1 que precisa de um endereço de 32 bits e um imediato de 8 bits para o mesmo uop.

  • Antes de um deslocamento / rotação de contagem variável para quebrar a dependência de sinalizadores e evitar a mesclagem de sinalização parcial: shl reg, cl tem uma dependência de input nos sinalizadores, devido ao infeliz histórico do CISC: ele deve deixá-los inalterados se a contagem de turnos for 0

    Na família Intel SnB, os turnos de contagem variável são 3 uops (acima de 1 no Core2 / Nehalem). AFAICT, dois dos sinalizadores de leitura / escrita do uops, e um uop independente lê reg e cl , e grava reg . É um caso estranho de ter melhor latência (1c + conflitos de resources inevitáveis) do que throughput (1.5c), e somente ser capaz de atingir o throughput máximo se misturado com instruções que quebram dependencies em flags. ( Eu postei mais sobre isso no fórum da Agner Fog). Use BMI2 shlx quando possível; é 1 uop e a contagem pode estar em qualquer registro.

    De qualquer forma, inc (escrita de flags mas deixar CF não modificada) antes de shl contagem de shl deixa com uma falsa dependência de qualquer CF gravado por último, e em SnB / IvB pode requerer um uop extra para mesclar flags.

    O Core2 / Nehalem consegue evitar até mesmo o falso dep em flags: Merom executa um loop de 6 shl reg,cl independentes shl reg,cl instruções em quase dois turnos por clock, mesmo desempenho com cl = 0 ou cl = 13. Qualquer coisa melhor que 1 por relógio prova que não há dependência de input nos sinalizadores.

    Eu tentei loops com shl edx, 2 e shl edx, 0 (mudanças de contagem imediata), mas não vi uma diferença de velocidade entre dec e sub no Core2, HSW ou SKL. Eu não sei sobre a AMD.

Atualização: O bom desempenho do turno na família Intel P6 tem o custo de um grande buraco de desempenho que você precisa evitar: quando uma instrução depende do resultado do sinalizador de uma instrução de mudança: O front end paralisa até a instrução ser retirada . (Fonte: Manual de otimização da Intel, (Seção 3.5.2.6: Stalls de registro parcial da bandeira) ). Então shr eax, 2 / jnz é bastante catastrófico para performance em pré-Sandybridge da Intel, eu acho! Use shr eax, 2 / test eax,eax / jnz se você se preocupa com Nehalem e anteriores. Os exemplos da Intel deixam claro que isso se aplica a mudanças de contagem imediata, não apenas count = cl .

Em processadores baseados na microarquitetura Intel Core [isso significa Core 2 e posterior], shift immediate por 1 é manipulado por um hardware especial, de modo que não ocorra sinalização parcial.

Intel realmente significa o opcode especial sem imediato, que muda por um 1 implícito. Eu acho que há uma diferença de desempenho entre as duas maneiras de codificar shr eax,1 , com a codificação curta (usando o código de operação 8086 original D1 /5 ) produzindo um resultado de sinalizador somente de gravação (parcial), mas a codificação mais longa C1 /5, imm8 com um 1 imediato) não tendo sua verificação imediata para 0 até o tempo de execução, mas sem rastrear a saída da bandeira no maquinário fora de ordem.

Como o loop de bits é comum, mas o loop de todo segundo (ou qualquer outro passo) é muito incomum, isso parece uma escolha razoável de design. Isso explica por que os compiladores gostam de test o resultado de um turno em vez de usar diretamente os resultados de flag do shr .

Atualização: para variações de contagem de variables ​​na família SnB, o manual de otimização da Intel diz:

3.5.1.6 Rotação de Contagem de Bit Variável e Shift

No nome de código da microarquitetura Intel Sandy Bridge, a instrução “ROL / ROR / SHL / SHR reg, cl” tem três microinstruções. Quando o resultado do sinalizador não é necessário, uma dessas micro-operações pode ser descartada, proporcionando melhor desempenho em muitos usos comuns . Quando essas instruções atualizam os resultados de sinalizadores parciais que são usados ​​subseqüentemente, o stream total de três microinstruções deve passar pelo pipeline de execução e descontinuidade, experimentando um desempenho mais lento. No nome de código da microarquitetura Intel Ivy Bridge, a execução do stream completo de três micro-operações para usar o resultado de sinalizador parcial atualizado tem um atraso adicional.

Considere a sequência em loop abaixo:

 loop: shl eax, cl add ebx, eax dec edx ; DEC does not update carry, causing SHL to execute slower three micro-ops flow jnz loop 

A instrução DEC não modifica o flag carry. Consequentemente, a instrução SHL EAX, CL precisa executar o stream de três micro-operações em iterações subseqüentes. A instrução SUB atualizará todos os sinalizadores. Assim, a substituição de DEC por SUB permitirá que SHL EAX, CL execute o stream de duas microinstruções.


Terminologia

As baias de bandeira parcial acontecem quando as bandeiras são lidas , se acontecerem. O P4 nunca tem baias de bandeira parcial, porque elas nunca precisam ser mescladas. Tem dependencies falsas em vez disso.

Várias respostas / comentários misturam a terminologia. Eles descrevem uma dependência falsa, mas depois chamam de tenda de bandeira parcial. É uma desaceleração que acontece por causa da escrita de apenas algumas das bandeiras, mas o termo ” stall de bandeira parcial” é o que acontece no hardware pré-SnB Intel quando as gravações de flag parcial precisam ser mescladas. As CPUs Intel SnB-family inserem um uop extra para mesclar flags sem parar. Nehalem e mais cedo param por ~ 7 ciclos. Não tenho certeza de quão grande é a penalidade nos processadores da AMD.

(Observe que as penalidades de registro parcial nem sempre são as mesmas que as sinalizações parciais, veja abaixo).

 ### Partial flag stall on Intel P6-family CPUs: bigint_loop: adc eax, [array_end + rcx*4] # partial-flag stall when adc reads CF inc rcx # rcx counts up from negative values towards zero # test rcx,rcx # eliminate partial-flag stalls by writing all flags, or better use add rcx,1 jnz # this loop doesn't do anything useful; it's not normally useful to loop the carry-out back to the carry-in for the same accumulator. # Note that `test` will change the input to the next adc, and so would replacing inc with add 1 

Em outros casos, por exemplo, uma gravação de sinalização parcial seguida por uma gravação de sinalização completa ou uma leitura apenas dos sinalizadores escritos por inc , é adequada. Nas CPUs da família SnB, o inc/dec pode até mesmo macro-fusível com um jcc , o mesmo que add/sub .

Depois do P4, a Intel desistiu de tentar fazer com que as pessoas -mtune=pentium4 novamente com -mtune=pentium4 ou modificassem o texto escrito à mão para evitar sérios gargalos. (O ajuste para uma microarquitetura específica sempre será uma coisa, mas o P4 era incomum em desaprovar tantas coisas que costumavam ser rápidas em CPUs anteriores e, portanto, eram comuns em binários existentes.) P4 queria que as pessoas usassem um subconjunto do tipo RISC. o x86, e também tinha dicas de previsão de ramificação como prefixos para instruções JCC. (Ele também tinha outros problemas sérios, como o cache de rastreio que não era bom o suficiente, e decodificadores fracos que significavam mau desempenho em falhas de cache de rastreio. Sem mencionar que toda a filosofia de clocking muito alto atingiu a parede de densidade de energia .)

Quando a Intel abandonou o P4, eles voltaram para os projetos da família P6 (Pentium-M / Core2 / Nehalem), que herdaram o controle partial-flag / partial-log das CPUs anteriores da família P6 (PPro para PIII) que datado o mis-step do netburst. (Nem tudo sobre o P4 era inerentemente ruim, e algumas das ideias reapareceram no Sandybridge, mas o NetBurst é considerado um erro.) Algumas instruções muito CISC ainda são mais lentas do que as alternativas de multi-instrução, por exemplo, enter , loop , ou bt [mem], reg (porque o valor de reg afeta qual endereço de memory é usado), mas estes eram todos lentos em CPUs mais antigas, então os compiladores já os evitavam.

O Pentium-M até melhorou o suporte de hardware para regs parciais (penalidades de fusão menores). Em Sandybridge, a Intel manteve a renomeação de partial-flag e partial-reg e tornou muito mais eficiente quando a mesclagem é necessária (mesclando uop inserido com no ou mínimo stall). SnB fez grandes mudanças internas e é considerado uma nova família uarch, apesar de herdar muito do Nehalem e algumas idéias do P4. (Mas note que o cache decod-uop do SnB não é um cache de rastreamento, por isso é uma solução muito diferente para o problema de taxa de transferência / energia do decodificador que o cache de rastreamento do netburst tentou resolver.)


Por exemplo, inc al e inc ah podem rodar em paralelo em CPUs da família P6 / SnB, mas a leitura eax depois requer mesclagem .

PPro / PIII stall por 5-6 ciclos ao ler o registro completo. Core2 / Nehalem procanvasm apenas 2 ou 3 ciclos ao inserir um uop de mesclagem para regs parciais, mas sinalizadores parciais ainda são mais longos.

SnB insere um uop de mesclagem sem parar, como para sinalizadores. O guia de otimização da Intel diz que para unir AH / BH / CH / DH no registro mais amplo, inserir o uping de mesclagem requer um ciclo inteiro de edição / renomeação durante o qual nenhum outro uop pode ser alocado. Mas para low8 / low16, o upl de mesclagem é “parte do stream”, portanto, aparentemente não causa penalidades de throughput de front-end adicionais além de ocupar um dos quatro slots em um ciclo de emissão / renomeação.

No IvyBridge (ou pelo menos em Haswell), a Intel abandonou a renomeação de registro parcial para registros low8 e low16, mantendo-a apenas para registros high8 (AH / BH / CH / DH). A leitura de registros high8 tem latência extra. Além disso, setcc al tem uma dependência falsa do valor antigo de rax, diferente de Nehalem e anterior (e provavelmente de Sandybridge). Veja este Q & A de desempenho de registro parcial HSW / SKL para os detalhes.

(Eu já afirmei que Haswell poderia unir AH sem uop, mas isso não é verdade e não é o que diz o guia de Agner Fog. Eu folheei muito rápido e infelizmente repeti meu entendimento errado em muitos comentários e outros posts.)

Os processadores da AMD, e a Intel Silvermont, não renomear regs parciais (além de flags), então mov al, [mem] tem uma dependência falsa do antigo valor de eax. (A vantagem não é a lentidão de mesclagem de registro parcial ao ler o registro completo mais tarde.)


Normalmente, o único add tempo em vez de inc fará com que seu código seja mais rápido na AMD ou mainstream da Intel é quando o seu código realmente depende do comportamento de inc não tocar no CF. isto é, geralmente add somente ajuda quando ele quebra seu código , mas observe o caso shl mencionado acima, em que a instrução lê sinalizadores, mas geralmente seu código não se importa com isso, então é uma falsa dependência.

Se você realmente quer deixar o CF não modificado, as CPUs pré-família SnB têm sérios problemas com baias de bandeira parcial, mas na família SnB a sobrecarga de ter a CPU fundindo os sinalizadores parciais é muito baixa, então pode ser melhor manter usando inc ou dec como parte de uma condição de loop ao direcionar essa CPU, com algum desenrolar. (Para mais detalhes, consulte o BigInteger adc Q & A vinculado anteriormente). Pode ser útil usar lea para fazer a aritmética sem afetar as flags, se você não precisar ramificar no resultado.

Dependendo da implementação da CPU das instruções, uma atualização de registro parcial pode causar uma parada. De acordo com o guia de otimização da Agner Fog, página 62 ,

Por razões históricas, as instruções INC e DEC deixam o flag carry inalterado, enquanto as outras flags aritméticas são gravadas. Isso causa uma dependência falsa do valor anterior dos sinalizadores e custa um μop extra. Para evitar esses problemas, é recomendável que você sempre use ADD e SUB vez de INC e DEC . Por exemplo, o INC EAX deve ser substituído por ADD EAX,1 .

Consulte também a página 83 em “Paras de sinalizadores parciais” e a página 100 em “Sinalizações de sinalizadores parciais”.