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, enquanto cmp 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 )
void foo(unsigned size) {
unsigned upper_bound = size - 1; // or any calculation that could produce UINT_MAX
for(unsigned i=0 ; i <= upper_bound ; i++)
...
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 a upper_bound=UINT_MAX
e i <= UINT_MAX
é sempre verdade. Portanto, esse loop é infinito size=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 poderia i < size
.
Asm like if(!size) skip the loop;
do{...}while(--size);
é uma maneira normalmente eficiente de otimizar um for( i<size )
loop, se o valor real de i
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 otimiza sum(1 .. n)
loops com um formulário fechado com base na n * (n+1) / 2
fórmula de Gauss .
unsigned sum_1_to_n_finite(unsigned n) {
unsigned total = 0;
for (unsigned i = 0 ; i < n+1 ; ++i)
total += i;
return total;
}
x86-64 asm de clang7.0 e gcc8.2 no Godbolt compiler explorer
# clang7.0 -O3 closed-form
cmp edi, -1 # n passed in EDI: x86-64 System V calling convention
je .LBB1_1 # if (n == UINT_MAX) return 0; // C++ loop runs 0 times
# else fall through into the closed-form calc
mov ecx, edi # zero-extend n into RCX
lea eax, [rdi - 1] # n-1
imul rax, rcx # n * (n-1) # 64-bit
shr rax # n * (n-1) / 2
add eax, edi # n + (stuff / 2) = n * (n+1) / 2 # truncated to 32-bit
ret # computed without possible overflow of the product before right shifting
.LBB1_1:
xor eax, eax
ret
Mas para a versão ingênua, obtemos apenas um loop estúpido do clang.
unsigned sum_1_to_n_naive(unsigned n) {
unsigned total = 0;
for (unsigned i = 0 ; i<=n ; ++i)
total += i;
return total;
}
# clang7.0 -O3
sum_1_to_n(unsigned int):
xor ecx, ecx # i = 0
xor eax, eax # retval = 0
.LBB0_1: # do {
add eax, ecx # retval += i
add ecx, 1 # ++1
cmp ecx, edi
jbe .LBB0_1 # } while( i<n );
ret
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.
# "naive" inner loop
.L3:
add eax, 1 # do {
paddd xmm0, xmm1 # vect_total_4.6, vect_vec_iv_.5
paddd xmm1, xmm2 # vect_vec_iv_.5, tmp114
cmp edx, eax # bnd.1, ivtmp.14 # bound and induction-variable tmp, I think.
ja .L3 #, # }while( n > i )
"finite" inner loop
# before the loop:
# xmm0 = 0 = totals
# xmm1 = {0,1,2,3} = i
# xmm2 = set1_epi32(4)
.L13: # do {
add eax, 1 # i++
paddd xmm0, xmm1 # total[0..3] += i[0..3]
paddd xmm1, xmm2 # i[0..3] += 4
cmp eax, edx
jne .L13 # }while( i != upper_limit );
then horizontal sum xmm0
and peeled cleanup for the last n%3 iterations, or something.
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
/ em jnz
vez de add 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.