É if( a < 901 )
mais rápido que if( a <= 900 )
.
Não é exatamente como neste exemplo simples, mas há pequenas alterações de desempenho no código complexo de loop. Suponho que isso tenha algo a ver com o código de máquina gerado, caso isso seja verdade.
<
é duas vezes mais rápido que digitar<=
.Respostas:
Não, não será mais rápido na maioria das arquiteturas. Você não especificou, mas no x86, todas as comparações integrais serão normalmente implementadas em duas instruções da máquina:
test
oucmp
instrução, que defineEFLAGS
Jcc
instrução (jump) , dependendo do tipo de comparação (e layout do código):jne
- Salte se não for igual ->ZF = 0
jz
- Salte se zero (igual) ->ZF = 1
jg
- Salte se maior ->ZF = 0 and SF = OF
Exemplo (Editado por brevidade) Compilado com
$ gcc -m32 -S -masm=intel test.c
Compila para:
E
Compila para:
Portanto, a única diferença entre os dois é
jg
umajge
instrução versus uma instrução. Os dois levarão a mesma quantidade de tempo.Gostaria de abordar o comentário de que nada indica que as diferentes instruções de salto levam a mesma quantidade de tempo. Este é um pouco complicado de responder, mas eis o que posso dar: Na Referência do conjunto de instruções da Intel , todos estão agrupados sob uma instrução comum
Jcc
(Salte se a condição for atendida). O mesmo agrupamento é feito em conjunto no Manual de Referência da Otimização , no Apêndice C. Latência e Taxa de Transferência.Os valores para
Jcc
são:com a seguinte nota de rodapé
Jcc
:Portanto, nada nos documentos da Intel trata uma
Jcc
instrução de maneira diferente das outras.Se pensarmos nos circuitos reais usados para implementar as instruções, podemos assumir que haveria portas AND / OR simples nos diferentes bits
EFLAGS
, para determinar se as condições são atendidas. Portanto, não há razão para que uma instrução que teste dois bits deva levar mais ou menos tempo do que uma que teste apenas um (Ignorando o atraso de propagação da porta, que é muito menor que o período do relógio).Editar: Ponto flutuante
Isso também vale para o ponto flutuante x87: (Praticamente o mesmo código acima, mas com em
double
vez deint
.)fonte
jg
ejnle
são as mesmas instruções7F
:-)Historicamente (estamos falando dos anos 80 e início dos 90), havia algumas arquiteturas nas quais isso era verdade. O problema principal é que a comparação de números inteiros é inerentemente implementada por meio de subtrações de números inteiros. Isso dá origem aos seguintes casos.
Agora, quando
A < B
a subtração precisar emprestar um valor alto para que a subtração esteja correta, assim como você carrega e empresta ao adicionar e subtrair manualmente. Esse bit "emprestado" era geralmente chamado de bit de transporte e seria testável por uma instrução de ramificação. Um segundo bit chamado zero seria definido se a subtração fosse idêntica a zero, o que implicava igualdade.Geralmente, havia pelo menos duas instruções de ramificação condicional, uma para ramificar no bit de transporte e outra no bit zero.
Agora, para chegar ao cerne da questão, vamos expandir a tabela anterior para incluir os resultados carry e zero bit.
Portanto, implementar uma ramificação para
A < B
pode ser feito em uma instrução, porque o bit de transporte é claro apenas neste caso, ou seja,Mas, se quisermos fazer uma comparação menor ou igual, precisamos fazer uma verificação adicional do sinalizador zero para entender o caso da igualdade.
Portanto, em algumas máquinas, o uso de uma comparação "menor que" pode salvar uma instrução da máquina . Isso foi relevante na era da velocidade do processador sub-megahertz e na proporção de 1: 1 da CPU para a memória, mas hoje é quase totalmente irrelevante hoje.
fonte
jge
, que testam os sinalizadores zero e sinal / transporte.<=
teste pode ser implementada em uma instrução com trocando os operandos e testando paranot <
(a equivalente>=
) Esta é a desejada<=
com operandos trocados:cmp B,A; bcs addr
. Esse é o raciocínio este teste foi omitido pela Intel, que considerou redundante e você não podia pagar instruções redundantes nesses momentos :-)Supondo que estamos falando de tipos inteiros internos, não há como um ser mais rápido que o outro. Eles são obviamente semanticamente idênticos. Ambos pedem que o compilador faça exatamente a mesma coisa. Somente um compilador horrivelmente quebrado geraria código inferior para um deles.
Se houver alguma plataforma
<
mais rápida do que<=
para tipos inteiros simples, o compilador deve sempre converter<=
para<
para constantes. Qualquer compilador que não o fizesse seria apenas um compilador ruim (para essa plataforma).fonte
<
nem<=
tem velocidade até que o compilador decide qual a velocidade que eles têm. Essa é uma otimização muito simples para os compiladores quando você considera que eles geralmente já executam otimização de código morto, otimização de chamada de cauda, içamento de loop (e desenrolamento, às vezes), paralelização automática de vários loops, etc. Por que perder tempo pensando em otimizações prematuras ? Obter um protótipo em execução, perfil para determinar onde as otimizações mais significativos mentir, executar as otimizações em ordem de importância e perfil novamente ao longo do caminho para medir o progresso ...(a < C)
para(a <= C-1)
(para alguma constanteC
) fazC
com que seja mais difícil codificar no conjunto de instruções. Por exemplo, um conjunto de instruções pode ser capaz de representar constantes assinadas de -127 a 128 em uma forma compacta nas comparações, mas as constantes fora desse intervalo precisam ser carregadas usando uma codificação mais longa e mais lenta ou outra instrução inteiramente. Portanto, uma comparação como essa(a < -127)
pode não ter uma transformação direta.a > 127
aa > 128
porque você não tem escolha lá, você use o que você precisa. Estamos comparandoa > 127
aa >= 128
, que não pode exigir codificação ou instruções diferentes, porque elas têm a mesma tabela de verdade. Qualquer codificação de um é igualmente uma codificação do outro.<=
para<
para constantes". Até onde eu sei, essa transformação envolve mudar a constante. Por exemplo,a <= 42
é compilado comoa < 43
porque<
é mais rápido. Em alguns casos extremos, essa transformação não seria proveitosa porque a nova constante pode exigir instruções mais ou mais lentas. Claroa > 127
ea >= 128
são equivalentes e um compilador deve codificar as duas formas na (mesma) maneira mais rápida, mas isso não é inconsistente com o que eu disse.Vejo que nem é mais rápido. O compilador gera o mesmo código de máquina em cada condição com um valor diferente.
Meu exemplo
if
é do GCC na plataforma x86_64 no Linux.Os escritores de compiladores são pessoas bastante inteligentes, e pensam nessas coisas e em muitas outras que a maioria de nós considera um dado adquirido.
Percebi que, se não for uma constante, o mesmo código de máquina será gerado nos dois casos.
fonte
if(a <=900)
para demonstrar que ele gera exatamente a mesma asm :)Para código de ponto flutuante, a comparação <= pode realmente ser mais lenta (por uma instrução), mesmo em arquiteturas modernas. Aqui está a primeira função:
No PowerPC, primeiro isso faz uma comparação de ponto flutuante (que atualiza
cr
, o registro de condições), depois move o registro de condições para um GPR, muda o bit "comparado menos que" para o local e depois retorna. São necessárias quatro instruções.Agora considere esta função:
Isso requer o mesmo trabalho que o
compare_strict
descrito acima, mas agora há dois bits de interesse: "era menor que" e "era igual a". Isso requer uma instrução extra (cror
- registro de condição OR bit a bit) para combinar esses dois bits em um. Portanto,compare_loose
requer cinco instruções, enquantocompare_strict
requer quatro.Você pode pensar que o compilador pode otimizar a segunda função da seguinte maneira:
No entanto, isso irá lidar incorretamente com NaNs.
NaN1 <= NaN2
eNaN1 > NaN2
precisa avaliar como falso.fonte
fucomip
define ZF e CF.cr
é equivalente a sinalizadores comoZF
eCF
no x86. (Embora o CR seja mais flexível.) O que o pôster está falando é mover o resultado para um GPR: que requer duas instruções no PowerPC, mas o x86 possui uma instrução de movimentação condicional.Talvez o autor desse livro sem nome tenha lido que
a > 0
corre mais rápido do quea >= 1
e pensa que isso é verdade universalmente.Mas é porque a
0
está envolvido (porqueCMP
pode, dependendo da arquitetura, substituído, por exemplo, porOR
) e não por causa do<
.fonte
(a >= 1)
executar mais lento do que(a > 0)
, uma vez que o primeiro pode ser trivialmente transformou a este último pelo otimizador ..No mínimo, se isso fosse verdade, um compilador poderia otimizar trivialmente a <= b para! (A> b) e, mesmo que a comparação em si fosse realmente mais lenta, com todos, exceto o compilador mais ingênuo, você não notaria diferença .
fonte
NOT
é feito apenas por outra instrução (je
vs.jne
)Eles têm a mesma velocidade. Talvez em alguma arquitetura especial o que ele disse esteja certo, mas na família x86 pelo menos eu sei que eles são iguais. Porque, para fazer isso, a CPU fará uma subtração (a - b) e depois verificará os sinalizadores do registro do sinalizador. Dois bits desse registrador são chamados ZF (sinalizador zero) e SF (sinalizador), e são feitos em um ciclo, porque o fazem com uma operação de máscara.
fonte
Isso seria altamente dependente da arquitetura subjacente na qual o C é compilado. Alguns processadores e arquiteturas podem ter instruções explícitas para igual ou menor que e igual a, que são executadas em diferentes números de ciclos.
Isso seria bastante incomum, pois o compilador poderia contorná-lo, tornando-o irrelevante.
fonte
TL; DRResposta
Para a maioria das combinações de arquitetura, compilador e idioma, não será mais rápido.
Resposta completa
Outras respostas se concentraram em arquitetura x86 , e eu não conheço bem a arquitetura ARM (que seu assembler de exemplo parece) o suficiente para comentar especificamente sobre o código gerado, mas este é um exemplo de micro-otimização que é muito arquitetura específico e é tão provável que seja uma anti-otimização quanto uma otimização .
Como tal, eu sugeriria que esse tipo de micro-otimização é um exemplo de culto à carga programação de , e não das melhores práticas de engenharia de software.
Provavelmente existem algumas arquiteturas em que isso é uma otimização, mas eu sei de pelo menos uma arquitetura em que o oposto pode ser verdadeiro. A venerável arquitetura do Transputer tinha apenas instruções de código de máquina iguais ou maiores que ou iguais a , portanto todas as comparações tiveram que ser construídas a partir dessas primitivas.
Mesmo assim, em quase todos os casos, o compilador podia ordenar as instruções de avaliação de tal maneira que, na prática, nenhuma comparação tivesse vantagem sobre outras. No pior caso, pode ser necessário adicionar uma instrução reversa (REV) para trocar os dois itens principais na pilha de operandos . Essa era uma instrução de byte único que demorou um único ciclo para ser executada, assim teve a menor sobrecarga possível.
Se uma micro-otimização como essa é uma otimização ou uma anti-otimização depende da arquitetura específica que você está usando, por isso é geralmente uma má idéia adquirir o hábito de usar micro-otimizações específicas da arquitetura; caso contrário, você pode instintivamente use um quando for inapropriado e parece que é exatamente isso que o livro que você está lendo está defendendo.
fonte
Você não deve perceber a diferença, mesmo que exista. Além disso, na prática, você terá que fazer um adicional
a + 1
oua - 1
manter a condição, a menos que use algumas constantes mágicas, o que é uma prática muito ruim em todos os aspectos.fonte
Você poderia dizer que a linha está correta na maioria das linguagens de script, pois o caractere extra resulta em um processamento de código um pouco mais lento. No entanto, como a resposta principal apontou, ela não deve ter efeito em C ++, e qualquer coisa que esteja sendo feita com uma linguagem de script provavelmente não está preocupada com a otimização.
fonte
Quando eu escrevi essa resposta, eu só estava olhando para a pergunta do título sobre <vs. <= em geral, não é o exemplo específico de uma constante
a < 901
vs.a <= 900
. Muitos compiladores sempre diminuem a magnitude das constantes convertendo entre<
e<=
, por exemplo, porque o operando imediato x86 possui uma codificação de 1 byte mais curta para -128..127.Para o ARM e especialmente o AArch64, a capacidade de codificação imediata depende da capacidade de rotacionar um campo estreito para qualquer posição em uma palavra. Então,
cmp w0, #0x00f000
seria codificável, enquantocmp w0, #0x00effff
pode não ser. Portanto, a regra de redução do tamanho para comparação versus uma constante em tempo de compilação nem sempre se aplica ao AArch64.<vs. <= em geral, inclusive para condições variáveis de tempo de execução
Na linguagem assembly na maioria das máquinas, uma comparação para
<=
tem o mesmo custo que uma comparação para<
. Isso se aplica se você estiver ramificando-o, fazendo booleano para criar um número inteiro 0/1 ou usando-o como predicado para uma operação de seleção sem ramificação (como x86 CMOV). As outras respostas abordaram apenas essa parte da pergunta.Mas esta pergunta é sobre os operadores C ++, a entrada para o otimizador. Normalmente, ambos são igualmente eficientes; o conselho do livro parece totalmente falso porque os compiladores sempre podem transformar a comparação que eles implementam em asm. Mas há pelo menos uma exceção em que o uso
<=
pode criar acidentalmente algo que o compilador não pode otimizar.Como condição de loop, há casos em que
<=
é qualitativamente diferente de<
, quando impede o compilador de provar que um loop não é infinito. Isso pode fazer uma grande diferença, desativando a vetorização automática.O estouro não assinado é bem definido como base 2, ao contrário do UB (estouro assinado). Os contadores de loop assinados geralmente estão seguros disso com os compiladores que otimizam com base no UB de overflow com sinal não acontecendo:
++i <= size
sempre acabará sempre se tornando falso. ( O que todo programador C deve saber sobre comportamento indefinido )Os compiladores só podem otimizar de maneira a preservar o comportamento (definido e legalmente observável) da fonte C ++ para todos os possíveis valores de entrada , exceto aqueles que levam a um comportamento indefinido.
(Um simples
i <= size
também criaria o problema, mas pensei que calcular um limite superior era um exemplo mais realista de introduzir acidentalmente a possibilidade de um loop infinito para uma entrada que você não se importa, mas que o compilador deve considerar.)Nesse caso,
size=0
leva aupper_bound=UINT_MAX
ei <= UINT_MAX
é sempre verdade. Portanto, esse loop é infinitosize=0
, e o compilador deve respeitar isso, mesmo que você como programador provavelmente nunca pretenda passar size = 0. Se o compilador puder incorporar essa função em um chamador, onde poderá provar que size = 0 é impossível, então ótimo, ele poderá otimizar como poderiai < size
.Asm like
if(!size) skip the loop;
do{...}while(--size);
é uma maneira normalmente eficiente de otimizar umfor( i<size )
loop, se o valor real dei
não for necessário dentro do loop ( por que os loops são sempre compilados no estilo "do ... while" (salto de cauda)? ).Mas isso não pode ser infinito: se inserido com
size==0
, obtemos 2 ^ n iterações. (A iteração sobre todos os números inteiros não assinados em um loop C torna possível expressar um loop sobre todos os números inteiros não assinados, incluindo zero, mas não é fácil sem um sinalizador carry do jeito que está no asm.)Com a possibilidade de envolver o contador de loop, os compiladores modernos simplesmente "desistem" e não otimizam de maneira tão agressiva.
Exemplo: soma dos números inteiros de 1 a n
O uso de
i <= n
derrotas não assinadas derrota o reconhecimento de expressões idiomáticas do clang que otimizasum(1 .. n)
loops com um formulário fechado com base nan * (n+1) / 2
fórmula de Gauss .x86-64 asm de clang7.0 e gcc8.2 no Godbolt compiler explorer
Mas para a versão ingênua, obtemos apenas um loop estúpido do clang.
O GCC não usa um formulário fechado de qualquer maneira, portanto a escolha da condição do loop não o prejudica ; vetoriza automaticamente com adição de número inteiro SIMD, executando 4
i
valores em paralelo nos elementos de um registro XMM.Ele também possui um loop escalar simples que eu acho que ele usa para muito pequeno
n
e / ou para o caso de loop infinito.BTW, esses dois loops desperdiçam uma instrução (e um uop nas CPUs da família Sandybridge) na sobrecarga do loop.
sub eax,1
/ emjnz
vez deadd eax,1
/ cmp / jcc seria mais eficiente. 1 uop em vez de 2 (após a fusão macro de sub / jcc ou cmp / jcc). O código após os dois loops grava EAX incondicionalmente, portanto não está usando o valor final do contador de loops.fonte
<
ou<=
. Mas com certeza,test ecx,ecx
/bt eax, 3
/jbe
irá pular se ZF estiver definido (ecx == 0) ou se CF estiver definido (bit 3 de EAX == 1), causando uma parada parcial de sinalizador na maioria das CPUs, porque os sinalizadores que lê não são todos vem da última instrução para escrever qualquer sinalizador. Na família Sandybridge, na verdade, ele não pára, só precisa inserir um uop em fusão.cmp
/test
escrevo todos os sinalizadores, masbt
deixa o ZF inalterado. felixcloutier.com/x86/btSomente se as pessoas que criaram os computadores forem ruins com lógica booleana. O que eles não deveriam ser.
Toda comparação (
>=
<=
>
<
) pode ser feita na mesma velocidade.O que toda comparação é, é apenas uma subtração (a diferença) e ver se é positivo / negativo.
(Se
msb
estiver definido, o número é negativo)Como verificar
a >= b
? Suba-b >= 0
Verifique sea-b
é positivo.Como verificar
a <= b
? Sub0 <= b-a
Verifique seb-a
é positivo.Como verificar
a < b
? Suba-b < 0
Verifique sea-b
é negativo.Como verificar
a > b
? Sub0 > b-a
Verifique seb-a
é negativo.Simplificando, o computador pode fazer isso por baixo do capô para a operação fornecida:
a >= b
==msb(a-b)==0
a <= b
==msb(b-a)==0
a > b
==msb(b-a)==1
a < b
==msb(a-b)==1
e, claro, o computador não seria realmente precisa fazer o
==0
ou==1
qualquer um.para o
==0
que poderia apenas inverter omsb
do circuito.Enfim, eles certamente não teriam feito
a >= b
ser calculado comoa>b || a==b
lolfonte