Quand j'ai écrit cette réponse, je ne regardais que la question du titre sur <vs <= en général, pas l'exemple spécifique d'une constante a < 901
par rapport a <= 900
. De nombreux compilateurs réduisent toujours l'amplitude des constantes en convertissant entre <
et <=
, par exemple parce que l'opérande immédiat x86 a un codage à 1 octet plus court pour -128..127.
Pour ARM et en particulier AArch64, le fait de pouvoir encoder en tant qu'immédiat dépend de pouvoir faire pivoter un champ étroit dans n'importe quelle position dans un mot. Donccmp w0, #0x00f000
serait donc encodable, alors que cmp w0, #0x00effff
ce ne l'est peut-être pas. Ainsi, la règle de comparaison plus petite par rapport à une constante de temps de compilation ne s'applique pas toujours à AArch64.
<vs. <= en général, y compris pour les conditions à variable d'exécution
En langage d'assemblage sur la plupart des machines, une comparaison pour <=
a le même coût qu'une comparaison pour <
. Cela s'applique que vous le branchiez, le booléeniez pour créer un entier 0/1 ou l'utilisiez comme prédicat pour une opération de sélection sans branche (comme x86 CMOV). Les autres réponses n'ont abordé que cette partie de la question.
Mais cette question concerne les opérateurs C ++, l' entrée de l'optimiseur. Normalement, ils sont tous deux aussi efficaces; les conseils du livre semblent totalement faux car les compilateurs peuvent toujours transformer la comparaison qu'ils implémentent en asm. Mais il y a au moins une exception où l'utilisation <=
peut créer accidentellement quelque chose que le compilateur ne peut pas optimiser.
En tant que condition de boucle, il existe des cas où <=
est qualitativement différent de <
, lorsqu'il empêche le compilateur de prouver qu'une boucle n'est pas infinie. Cela peut faire une grande différence en désactivant la vectorisation automatique.
Le débordement non signé est bien défini comme un bouclage en base 2, contrairement au débordement signé (UB). Les compteurs de boucles signées sont généralement à l'abri de cela avec des compilateurs qui optimisent en fonction de l'UB de débordement signé qui ne se ++i <= size
produira pas toujours. ( Ce que tout programmeur C devrait savoir sur le comportement indéfini )
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++)
...
Les compilateurs ne peuvent optimiser que de manière à préserver le comportement (défini et légalement observable) de la source C ++ pour toutes les valeurs d'entrée possibles , à l'exception de celles qui conduisent à un comportement non défini.
(Un simple i <= size
créerait également le problème, mais je pensais que calculer une limite supérieure était un exemple plus réaliste d'introduire accidentellement la possibilité d'une boucle infinie pour une entrée qui ne vous intéressait pas mais que le compilateur devait considérer.)
Dans ce cas, size=0
conduit à upper_bound=UINT_MAX
, et i <= UINT_MAX
est toujours vrai. Cette boucle est donc infinie pour size=0
, et le compilateur doit respecter cela même si vous, en tant que programmeur, n'avez probablement jamais l'intention de passer size = 0. Si le compilateur peut incorporer cette fonction dans un appelant où il peut prouver que size = 0 est impossible, alors grand, il peut optimiser comme il le pourrait pour i < size
.
Asm like if(!size) skip the loop;
do{...}while(--size);
est un moyen normalement efficace d'optimiser une for( i<size )
boucle, si la valeur réelle de i
n'est pas nécessaire à l'intérieur de la boucle ( pourquoi les boucles sont-elles toujours compilées dans le style "do ... while" (tail jump)? ).
Mais cela ne {} tout en ne peut pas être infini: si entré avec size==0
, nous obtenons 2 ^ n itérations. (L' itération sur tous les entiers non signés dans une boucle for C permet d'exprimer une boucle sur tous les entiers non signés, y compris zéro, mais ce n'est pas facile sans un indicateur de report tel qu'il est dans asm.)
Le bouclage du compteur de boucles étant une possibilité, les compilateurs modernes «abandonnent» souvent et n'optimisent pas de manière aussi agressive.
Exemple: somme d'entiers de 1 à n
L' i <= n
utilisation de la reconnaissance des idiomes de clang défait non signé qui optimise les sum(1 .. n)
boucles avec une forme fermée basée sur la n * (n+1) / 2
formule 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 et gcc8.2 sur l'explorateur du compilateur Godbolt
# 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
Mais pour la version naïve, nous obtenons juste une boucle stupide de 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
GCC n'utilise pas de forme fermée dans les deux cas, donc le choix de la condition de boucle ne lui fait pas vraiment de mal ; il vectorise automatiquement avec l'addition d'entiers SIMD, exécutant 4 i
valeurs en parallèle dans les éléments d'un registre 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.
Il a également une boucle scalaire simple que je pense qu'il utilise pour les très petites n
et / ou pour le cas de la boucle infinie.
BTW, ces deux boucles gaspillent une instruction (et un uop sur les processeurs de la famille Sandybridge) sur la surcharge de boucle. sub eax,1
/ jnz
au lieu de add eax,1
/ cmp / jcc serait plus efficace. 1 uop au lieu de 2 (après macro-fusion de sub / jcc ou cmp / jcc). Le code après les deux boucles écrit EAX sans condition, il n'utilise donc pas la valeur finale du compteur de boucles.