Est if( a < 901 )
plus rapide que if( a <= 900 )
.
Pas exactement comme dans cet exemple simple, mais il y a de légères modifications des performances sur le code complexe de boucle. Je suppose que cela doit faire quelque chose avec le code machine généré au cas où c'est même vrai.
<
saisie est deux fois plus rapide que la saisie<=
.Réponses:
Non, ce ne sera pas plus rapide sur la plupart des architectures. Vous n'avez pas spécifié, mais sur x86, toutes les comparaisons intégrales seront généralement implémentées dans deux instructions machine:
test
oucmp
, qui définitEFLAGS
Jcc
instruction (jump) , selon le type de comparaison (et la disposition du code):jne
- Saut si différent ->ZF = 0
jz
- Saut si zéro (égal) ->ZF = 1
jg
- Saut si plus grand ->ZF = 0 and SF = OF
Exemple (édité par souci de concision) Compilé avec
$ gcc -m32 -S -masm=intel test.c
Compile vers:
Et
Compile vers:
La seule différence entre les deux est donc
jg
unejge
instruction par rapport à une instruction. Les deux prendront le même temps.Je voudrais répondre au commentaire que rien n'indique que les différentes instructions de saut prennent le même temps. Celui-ci est un peu difficile à répondre, mais voici ce que je peux donner: dans la référence du jeu d'instructions Intel , ils sont tous regroupés sous une instruction commune,
Jcc
(sauter si la condition est remplie). Le même regroupement est effectué dans le Manuel de référence de l' optimisation , à l'annexe C. Latence et débit.Les valeurs de
Jcc
sont:avec la note de bas de page suivante
Jcc
:Ainsi, rien dans les documents Intel ne traite jamais une
Jcc
instruction différemment des autres.Si l'on pense aux circuits réels utilisés pour mettre en œuvre les instructions, on peut supposer qu'il y aurait de simples portes ET / OU sur les différents bits
EFLAGS
pour déterminer si les conditions sont remplies. Il n'y a donc aucune raison pour qu'une instruction testant deux bits prenne plus ou moins de temps qu'une seule testant un (Ignorer le délai de propagation de la porte, qui est bien inférieur à la période d'horloge).Modifier: virgule flottante
Cela vaut également pour la virgule flottante x87: (à peu près le même code que ci-dessus, mais avec
double
au lieu deint
.)la source
jg
etjnle
sont la même instruction,7F
:-)Historiquement (nous parlons des années 1980 et du début des années 1990), il y avait certaines architectures dans lesquelles cela était vrai. Le problème racine est que la comparaison d'entiers est implémentée de manière inhérente via des soustractions d'entiers. Cela donne lieu aux cas suivants.
Maintenant, lorsque
A < B
la soustraction doit emprunter un bit élevé pour que la soustraction soit correcte, tout comme vous effectuez et empruntez en ajoutant et en soustrayant à la main. Ce bit "emprunté" était généralement appelé bit de retenue et pouvait être testé par une instruction de branchement. Un deuxième bit appelé bit zéro serait défini si la soustraction était identique à zéro, ce qui impliquait l'égalité.Il y avait généralement au moins deux instructions de branchement conditionnel, une à branchement sur le bit de retenue et une sur le bit zéro.
Maintenant, pour aller au cœur du problème, développons le tableau précédent pour inclure les résultats de report et de bit zéro.
Ainsi, l'implémentation d'une branche pour
A < B
peut être effectuée dans une instruction, car le bit de report est clair uniquement dans ce cas, c'est-à-dire,Mais, si nous voulons faire une comparaison inférieure ou égale, nous devons effectuer une vérification supplémentaire de l'indicateur zéro pour attraper le cas de l'égalité.
Ainsi, sur certaines machines, l'utilisation d'une comparaison "inférieure à" peut enregistrer une instruction machine . C'était pertinent à l'ère de la vitesse du processeur sous-mégahertz et des rapports de vitesse 1: 1 CPU-mémoire, mais c'est presque totalement hors de propos aujourd'hui.
la source
jge
, qui testent à la fois les drapeaux zéro et signe / report.<=
test peut être mis en œuvre dans une instruction avec permutation des opérandes et de test pournot <
(équivalent à>=
) Ceci est le désiré<=
avec opérandes troqué:cmp B,A; bcs addr
. C'est la raison pour laquelle ce test a été omis par Intel, ils l'ont considéré comme redondant et vous ne pouviez pas vous permettre des instructions redondantes à ces moments :-)En supposant que nous parlons de types d'entiers internes, il n'y a aucun moyen possible que l'un soit plus rapide que l'autre. Ils sont évidemment sémantiquement identiques. Ils demandent tous deux au compilateur de faire exactement la même chose. Seul un compilateur horriblement cassé générerait un code inférieur pour l'un d'entre eux.
S'il y avait une plate-forme où
<
était plus rapide que<=
pour les types entiers simples, le compilateur devrait toujours se convertir<=
en<
constantes. Tout compilateur qui ne le serait pas serait juste un mauvais compilateur (pour cette plate-forme).la source
<
ni<=
avoir de vitesse jusqu'à ce que le compilateur décide quelle vitesse ils auront. Ceci est une optimisation très simple pour les compilateurs quand on considère qu'ils effectuent généralement déjà l' optimisation de code mort, l' optimisation d'appel queue, levage en boucle (et le déroulement, à l' occasion), parallélisation automatique des différentes boucles, etc ... Pourquoi perdre du temps méditée Optimisations prématurés ? Lancez un prototype, profilez-le pour déterminer où se trouvent les optimisations les plus significatives, effectuez ces optimisations par ordre d'importance et profilez à nouveau le long du chemin pour mesurer les progrès ...(a < C)
à(a <= C-1)
(pour certaines constantesC
)C
est plus difficile à coder dans le jeu d'instructions. Par exemple, un jeu d'instructions peut représenter des constantes signées de -127 à 128 sous une forme compacte dans les comparaisons, mais les constantes en dehors de cette plage doivent être chargées en utilisant soit un encodage plus long, plus lent, soit une autre instruction entièrement. Ainsi, une comparaison comme celle-ci(a < -127)
peut ne pas avoir une transformation simple.a > 127
pasa > 128
parce que vous n'avez pas le choix, vous utilisez celui dont vous avez besoin. Nous comparonsa > 127
àa >= 128
, qui ne peuvent pas nécessiter un encodage différent ou des instructions différentes car ils ont la même table de vérité. Tout encodage de l'un est également un encodage de l'autre.<=
en<
for constants". Pour autant que je sache, cette transformation implique de changer la constante. Par exemple,a <= 42
est compilé cara < 43
parce qu'il<
est plus rapide. Dans certains cas extrêmes, une telle transformation ne serait pas fructueuse car la nouvelle constante peut nécessiter des instructions plus ou plus lentes. Bien sûra > 127
, ilsa >= 128
sont équivalents et un compilateur devrait coder les deux formes de la (même) manière la plus rapide, mais ce n'est pas incompatible avec ce que j'ai dit.Je vois que ni l'un ni l'autre n'est plus rapide. Le compilateur génère le même code machine dans chaque condition avec une valeur différente.
Mon exemple
if
vient de GCC sur la plate-forme x86_64 sous Linux.Les auteurs de compilateurs sont des gens assez intelligents, et ils pensent à ces choses et à beaucoup d'autres que la plupart d'entre nous tiennent pour acquises.
J'ai remarqué que si ce n'est pas une constante, le même code machine est généré dans les deux cas.
la source
if(a <=900)
pour démontrer qu'il génère exactement le même asm :)Pour le code à virgule flottante, la comparaison <= peut en effet être plus lente (d'une instruction) même sur les architectures modernes. Voici la première fonction:
Sur PowerPC, cela effectue d'abord une comparaison à virgule flottante (qui met à jour
cr
le registre de conditions), puis déplace le registre de conditions vers un GPR, déplace le bit «comparé moins de» en place, puis revient. Il faut quatre instructions.Considérez maintenant cette fonction à la place:
Cela nécessite le même travail que
compare_strict
ci-dessus, mais maintenant il y a deux bits d'intérêt: "était inférieur à" et "était égal à". Cela nécessite une instruction supplémentaire (cror
- registre de condition OR au niveau du bit) pour combiner ces deux bits en un seul. Nécessite donccompare_loose
cinq instructions, tandis quecompare_strict
que quatre.Vous pourriez penser que le compilateur pourrait optimiser la deuxième fonction comme ceci:
Cependant, cela ne traitera pas correctement les NaN.
NaN1 <= NaN2
etNaN1 > NaN2
doivent à la fois évaluer faux.la source
fucomip
définit ZF et CF.cr
est l'équivalent de drapeaux commeZF
etCF
sur le x86. (Bien que le CR soit plus flexible.) Ce dont l'affiche parle, c'est de déplacer le résultat vers un GPR: qui prend deux instructions sur PowerPC, mais x86 a une instruction de déplacement conditionnel.Peut-être que l'auteur de ce livre sans nom a lu
a > 0
plus rapidementa >= 1
et pense que cela est vrai universellement.Mais c'est parce qu'un
0
est impliqué (carCMP
peut, selon l'architecture, remplacé par exemple parOR
) et non à cause du<
.la source
(a >= 1)
fonctionner plus lentement que(a > 0)
, car le premier peut être trivialement transformé en ce dernier par l'optimiseur ..À tout le moins, si cela était vrai, un compilateur pourrait optimiser trivialement un <= b à! (A> b), et donc même si la comparaison elle-même était en fait plus lente, avec tout sauf le compilateur le plus naïf, vous ne remarqueriez pas de différence .
la source
NOT
vient d'être créé par une autre instruction (je
vs.jne
)Ils ont la même vitesse. Peut-être que dans une architecture spéciale, ce qu'il a dit est juste, mais dans la famille x86 au moins, je sais que ce sont les mêmes. Parce que pour ce faire, le CPU fera une soustraction (a - b) puis vérifiera les drapeaux du registre des drapeaux. Deux bits de ce registre sont appelés ZF (drapeau zéro) et SF (drapeau de signe), et cela se fait en un cycle, car il le fera avec une opération de masque.
la source
Cela dépendrait fortement de l'architecture sous-jacente à laquelle le C est compilé. Certains processeurs et architectures peuvent avoir des instructions explicites égales ou inférieures et égales à, qui s'exécutent en différents nombres de cycles.
Ce serait assez inhabituel cependant, car le compilateur pourrait contourner cela, le rendant non pertinent.
la source
Réponse TL; DR
Pour la plupart des combinaisons d'architecture, de compilateur et de langage, ce ne sera pas plus rapide.
Réponse complète
D'autres réponses se sont concentrées sur l' architecture x86 , et je ne connais pas l' architecture ARM (que votre exemple d'assembleur semble être) assez bien pour commenter spécifiquement le code généré, mais ceci est un exemple de micro-optimisation qui est très architecture spécifique, et est aussi susceptible d’être une anti-optimisation qu’une optimisation .
En tant que tel, je dirais que ce type de micro-optimisation est un exemple de programmation culte du fret plutôt que de meilleures pratiques d'ingénierie logicielle.
Il y a probablement des architectures où c'est une optimisation, mais je connais au moins une architecture où le contraire peut être vrai. L' architecture vénérable de Transputer ne comportait que des instructions de code machine égales et supérieures ou égales à , donc toutes les comparaisons devaient être construites à partir de ces primitives.
Même alors, dans presque tous les cas, le compilateur pouvait ordonner les instructions d'évaluation de telle manière qu'en pratique, aucune comparaison n'avait aucun avantage sur aucune autre. Dans le pire des cas cependant, il peut être nécessaire d'ajouter une instruction inverse (REV) pour échanger les deux premiers éléments de la pile d'opérandes . Il s'agissait d'une instruction à un seul octet qui prenait un seul cycle à exécuter, donc avait le plus petit temps système possible.
Qu'une micro-optimisation comme celle-ci soit une optimisation ou une anti-optimisation dépend de l'architecture spécifique que vous utilisez, il est donc généralement une mauvaise idée de prendre l'habitude d'utiliser des micro-optimisations spécifiques à l'architecture, sinon vous pourriez instinctivement utilisez-en un quand il est inapproprié de le faire, et il semble que c'est exactement ce que préconise le livre que vous lisez.
la source
Vous ne devriez pas être en mesure de remarquer la différence même s'il y en a. De plus, dans la pratique, vous devrez faire une condition supplémentaire
a + 1
oua - 1
faire tenir la condition à moins que vous n'utilisiez des constantes magiques, ce qui est une très mauvaise pratique par tous les moyens.la source
Vous pourriez dire que la ligne est correcte dans la plupart des langages de script, car le caractère supplémentaire entraîne un traitement de code légèrement plus lent. Cependant, comme la première réponse l'a souligné, cela ne devrait pas avoir d'effet en C ++, et tout ce qui est fait avec un langage de script n'est probablement pas préoccupé par l'optimisation.
la source
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 rapporta <= 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. Donc
cmp w0, #0x00f000
serait donc encodable, alors quecmp 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 )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
, eti <= UINT_MAX
est toujours vrai. Cette boucle est donc infinie poursize=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 pouri < size
.Asm like
if(!size) skip the loop;
do{...}while(--size);
est un moyen normalement efficace d'optimiser unefor( i<size )
boucle, si la valeur réelle dei
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 lessum(1 .. n)
boucles avec une forme fermée basée sur lan * (n+1) / 2
formule de Gauss .x86-64 asm de clang7.0 et gcc8.2 sur l'explorateur du compilateur Godbolt
Mais pour la version naïve, nous obtenons juste une boucle stupide de clang.
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.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 deadd 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.la source
<
ou<=
. Mais bien sûr,test ecx,ecx
/bt eax, 3
/jbe
sautera si ZF est défini (ecx == 0), ou si CF est défini (bit 3 de EAX == 1), provoquant un blocage partiel des drapeaux sur la plupart des CPU parce que les drapeaux qu'il lit ne sont pas tous proviennent de la dernière instruction pour écrire des drapeaux. Sur la famille Sandybridge, il ne bloque pas, il suffit d'insérer un uop fusionnant.cmp
/test
écrire tous les drapeaux, maisbt
laisse ZF non modifié. felixcloutier.com/x86/btSeulement si les gens qui ont créé les ordinateurs sont mauvais avec la logique booléenne. Ce qu'ils ne devraient pas être.
Chaque comparaison (
>=
<=
>
<
) peut être effectuée à la même vitesse.Ce que chaque comparaison est, c'est juste une soustraction (la différence) et voir si c'est positif / négatif.
(Si
msb
est défini, le nombre est négatif)Comment vérifier
a >= b
? Sousa-b >= 0
Vérifier sia-b
est positif.Comment vérifier
a <= b
? Sous0 <= b-a
Vérifier sib-a
est positif.Comment vérifier
a < b
? Sousa-b < 0
Vérifier sia-b
est négatif.Comment vérifier
a > b
? Sous0 > b-a
Vérifier sib-a
est négatif.Autrement dit, l'ordinateur peut simplement faire cela sous le capot pour l'opération donnée:
a >= b
==msb(a-b)==0
a <= b
==msb(b-a)==0
a > b
==msb(b-a)==1
a < b
==msb(a-b)==1
et bien sûr l'ordinateur n'aurait pas vraiment besoin de faire le
==0
ou==1
autre.car
==0
il pourrait simplement inversermsb
le circuit.Quoi qu'il en soit, ils n'auraient certainement pas pu
a >= b
être calculés commea>b || a==b
lolla source