web-dev-qa-db-fra.com

Est-ce que <plus vite que <=?

Je lis un livre où l'auteur dit que if( a < 901 ) est plus rapide que if( a <= 900 ).

Pas exactement comme dans cet exemple simple, mais il y a de légers changements de performances sur le code complexe en boucle. Je suppose que cela doit faire quelque chose avec le code machine généré au cas où ce serait même vrai.

1477

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:

  • Une instruction test ou cmp, qui définit EFLAGS
  • Et une instruction Jcc (saut) , selon le type de comparaison (et la disposition du code):
    • jne - Saut si non égal -> ZF = 0
    • jz - Saute si zéro (égal) -> ZF = 1
    • jg - Saut si supérieur -> ZF = 0 and SF = OF
    • (etc...)

Exemple (Édité par souci de brièveté) Compilé avec $ gcc -m32 -S -masm=intel test.c

    if (a < b) {
        // Do something 1
    }

Compile à:

    mov     eax, DWORD PTR [esp+24]      ; a
    cmp     eax, DWORD PTR [esp+28]      ; b
    jge     .L2                          ; jump if a is >= b
    ; Do something 1
.L2:

Et

    if (a <= b) {
        // Do something 2
    }

Compile à:

    mov     eax, DWORD PTR [esp+24]      ; a
    cmp     eax, DWORD PTR [esp+28]      ; b
    jg      .L5                          ; jump if a is > b
    ; Do something 2
.L5:

Donc, la seule différence entre les deux est une instruction jg par rapport à une instruction jge. Les deux prendront le même temps.


J'aimerais aborder le commentaire selon lequel rien n'indique que les différentes instructions de saut prennent le même temps. Celui-ci est un peu délicat à répondre, mais voici ce que je peux vous donner: Dans le référence du jeu d'instructions Intel , ils sont tous regroupés sous une instruction commune, Jcc (saut si la condition est rencontré). Le même groupe est constitué sous le Guide de référence d'optimisation , dans l'Annexe C. Latence et débit.

Latence - Nombre de cycles d'horloge nécessaires au noyau d'exécution pour terminer l'exécution de tous les µops constituant une instruction.

Débit - Nombre de cycles d’horloge à attendre pour que les ports d’émission soient libres d’accepter à nouveau la même instruction. Pour de nombreuses instructions, le débit d’une instruction peut être considérablement inférieur à sa latence.

Les valeurs pour Jcc sont:

      Latency   Throughput
Jcc     N/A        0.5

avec la note de bas de page suivante sur Jcc:

7) La sélection des instructions de saut conditionnelles doit être basée sur la recommandation de la section 3.4.1, "Optimisation des prévisions de branche", afin d'améliorer la prévisibilité des branches. Lorsque les branches sont prédites avec succès, la latence de jcc est effectivement égale à zéro.

Ainsi, rien dans les documents Intel ne traite jamais une instruction Jcc différemment des autres.

Si l’on pense au circuit utilisé pour mettre en oeuvre les instructions, on peut supposer qu’il y aurait des portes simples ET/OU sur les différents bits de EFLAGS pour déterminer si les conditions sont remplies. Il n'y a donc aucune raison qu'une instruction testant deux bits ne prenne plus ou moins de temps qu'une seule testant un seul (Ignorer le délai de propagation de la porte, qui est bien inférieur à la période d'horloge).


Modifier: virgule flottante

Ceci est également vrai pour le nombre à virgule flottante x87: (à peu près le même code que ci-dessus, mais avec double au lieu de int.)

        fld     QWORD PTR [esp+32]
        fld     QWORD PTR [esp+40]
        fucomip st, st(1)              ; Compare ST(0) and ST(1), and set CF, PF, ZF in EFLAGS
        fstp    st(0)
        seta    al                     ; Set al if above (CF=0 and ZF=0).
        test    al, al
        je      .L2
        ; Do something 1
.L2:

        fld     QWORD PTR [esp+32]
        fld     QWORD PTR [esp+40]
        fucomip st, st(1)              ; (same thing as above)
        fstp    st(0)
        setae   al                     ; Set al if above or equal (CF=0).
        test    al, al
        je      .L5
        ; Do something 2
.L5:
        leave
        ret
1645
Jonathon Reinhart

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 fondamental 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.

Comparison     Subtraction
----------     -----------
A < B      --> A - B < 0
A = B      --> A - B = 0
A > B      --> A - B > 0

Maintenant, quand A < B la soustraction doit emprunter un bit fort pour que la soustraction soit correcte, tout comme vous le faites lorsque vous effectuez un emprunt et que vous empruntez lorsque vous ajoutez et soustrayez à la main. Ce bit "emprunté" était généralement appelé report et pouvait être testé par une instruction de branchement. Un second bit appelé bit zéro serait activé si la soustraction était identique à zéro, ce qui impliquait l'égalité.

Il y avait généralement au moins deux instructions de branchement conditionnelles, une pour dériver sur le bit de retenue et une sur le bit zéro.

Maintenant, pour aller au fond des choses, développons le tableau précédent pour inclure les résultats de retenue et de zéro bit.

Comparison     Subtraction  Carry Bit  Zero Bit
----------     -----------  ---------  --------
A < B      --> A - B < 0    0          0
A = B      --> A - B = 0    1          1
A > B      --> A - B > 0    1          0

Ainsi, l'implémentation d'une branche pour A < B peut être effectuée en une seule instruction, car le bit de report est clair niquement dans ce cas, c'est-à-dire

;; Implementation of "if (A < B) goto address;"
cmp  A, B          ;; compare A to B
bcz  address       ;; Branch if Carry is Zero to the new address

Toutefois, si nous voulons effectuer une comparaison inférieure ou égale, nous devons effectuer une vérification supplémentaire de l'indicateur zéro pour détecter le cas d'égalité.

;; Implementation of "if (A <= B) goto address;"
cmp A, B           ;; compare A to B
bcz address        ;; branch if A < B
bzs address        ;; also, Branch if the Zero bit is Set

Ainsi, sur certaines machines, en utilisant une comparaison "inférieure à" pourrait enregistrer ne instruction machine. Cela était pertinent à l'ère de la vitesse du processeur sous-mégahertz et des rapports de vitesse processeur-mémoire de 1: 1, mais cela n'a presque plus lieu d'être aujourd'hui.

584
Lucas

En supposant qu'il s'agisse de types entiers internes, il est impossible que l'un soit plus rapide que l'autre. Ils sont évidemment sémantiquement identiques. Ils demandent tous les 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 existait une plate-forme où < était plus rapide que <= pour les types entiers simples, le compilateur devrait toujours convertir <= en < pour les constantes. Tout compilateur qui ne le serait pas serait simplement un mauvais compilateur (pour cette plate-forme).

88
David Schwartz

Je vois que ni est plus rapide. Le compilateur génère le même code machine dans chaque condition avec une valeur différente.

if(a < 901)
cmpl  $900, -4(%rbp)
jg .L2

if(a <=901)
cmpl  $901, -4(%rbp)
jg .L3

Mon exemple if provient de GCC sur une plate-forme x86_64 sous Linux.

Les rédacteurs de compilateurs sont des personnes très intelligentes, et ils pensent à ces choses et à beaucoup d'autres que la plupart d'entre nous prenons pour acquis.

J'ai remarqué que si ce n'est pas une constante, le même code machine est généré dans les deux cas.

int b;
if(a < b)
cmpl  -4(%rbp), %eax
jge   .L2

if(a <=b)
cmpl  -4(%rbp), %eax
jg .L3
66
Adrian Cornish

Pour le code en virgule flottante, la comparaison <= peut en effet être plus lente (d’une instruction à l’autre), même sur les architectures modernes. Voici la première fonction:

int compare_strict(double a, double b) { return a < b; }

Sur PowerPC, ceci effectue d’abord une comparaison en virgule flottante (qui met à jour cr, le registre de conditions), puis déplace le registre de conditions vers un GPR, décale le bit "comparé à" à, puis renvoie. Il faut quatre instructions.

Maintenant, considérons cette fonction à la place:

int compare_loose(double a, double b) { return a <= b; }

Cela nécessite le même travail que compare_strict ci-dessus, mais il y a maintenant deux éléments d'intérêt: "était inférieur à" et "était égal à". Cela nécessite une instruction supplémentaire (cror - registre de condition bit à bit OU) pour combiner ces deux bits en un seul. Donc, compare_loose nécessite cinq instructions, alors que compare_strict en nécessite quatre.

Vous pourriez penser que le compilateur pourrait optimiser la seconde fonction de la manière suivante:

int compare_loose(double a, double b) { return ! (a > b); }

Cependant, cela ne gérera pas correctement les NaN. NaN1 <= NaN2 et NaN1 > NaN2 doivent être évalués comme faux.

50
ridiculous_fish

L'auteur de ce livre sans nom a peut-être lu que a > 0 est plus rapide que a >= 1 et pense que cela est vrai universellement.

Mais c’est parce qu’un 0 est impliqué (parce que CMP peut, en fonction de l’architecture, être remplacé, par exemple, par OR) et non à cause du <.

34
glglgl

À tout le moins, si cela était vrai, un compilateur pourrait optimiser trivialement un <= b à! (A> b), et même si la comparaison elle-même était plus lente, vous ne remarqueriez aucune différence avec le compilateur le plus naïf. .

32
Eliot Ball

Ils ont la même vitesse. Peut-être que dans une architecture particulière ce qu'il/elle a dit est correct, mais dans la famille x86 au moins, je sais que ce sont les mêmes. Parce que pour cela, la CPU effectue une soustraction (a - b) puis vérifie les indicateurs du registre des indicateurs. 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 cela se fera avec une opération de masque.

15
Masoud

Cela dépendrait beaucoup de l'architecture sous-jacente à laquelle le C est compilé. Certains processeurs et architectures peuvent avoir des instructions explicites égales, inférieures ou égales, qui s'exécutent selon un nombre de cycles différent.

Ce serait plutôt inhabituel, car le compilateur pourrait contourner le problème, le rendant ainsi inutile.

14
Telgin

TL; DR réponse

Pour la plupart des combinaisons d'architecture, de compilateur et de langage, cela 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 suffisamment l'architecture ARM (que votre exemple d'assembleur semble être) pour commenter spécifiquement le code généré, mais ceci est un exemple de micro-optimisation qui est très spécifique à l'architecture, et qui est aussi probable être une anti-optimisation comme c'est une optimisation .

En tant que tel, je suggérerais que cette sorte de micro-optimisation est un exemple de culte du fret ​​plutôt que de la meilleure pratique d'ingénierie logicielle.

Il y a probablement des architectures dans lesquelles il s'agit d'une optimisation, mais je connais au moins une architecture dans laquelle le contraire peut être vrai. La vénérable architecture Transputer n'avait que des instructions de code machine pour égales à et supérieures ou égales à , toutes les comparaisons ont donc été construites à partir de ces primitives.

Même alors, dans presque tous les cas, le compilateur pouvait ordonner les instructions d'évaluation de telle sorte qu'en pratique, aucune comparaison ne présente d'avantage sur aucune autre. Dans le pire des cas, il faudra peut-être 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 octet qui ne prenait qu’un cycle, de sorte que les frais généraux étaient les plus faibles possibles.

Qu'une telle micro-optimisation soit ou non une optimisation ou une anti-optimisation dépend de l'architecture spécifique que vous utilisez, il est donc généralement déconseillé de prendre l’habitude d’utiliser des micro-optimisations spécifiques à l’architecture, sinon vous pourriez instinctivement en utiliser une lorsque cela ne convient pas, et il semble que c’est exactement ce que préconise le livre que vous lisez.

11
Mark Booth

Vous ne devriez pas pouvoir remarquer la différence, même s’il en existe. De plus, en pratique, vous devrez faire un a + 1 ou a - 1 supplémentaire pour que la condition reste valide à moins que vous n'utilisiez des constantes magiques, ce qui est une très mauvaise pratique par tous les moyens.

6
shinkou

Vous pouvez dire que cette ligne est correcte dans la plupart des langages de script, car le caractère supplémentaire entraîne un traitement du code légèrement plus lent. Cependant, comme l'a souligné la réponse principale, cela ne devrait avoir aucun effet en C++, et tout ce qui est fait avec un langage de script n'est probablement pas préoccupé par l'optimisation.

4
Ecksters

Lorsque j’ai répondu à cette question, j’étais en train de regarder la question du titre sur <vs. <= en général, et non l’exemple spécifique d’une constante a < 901 vs. a <= 900. De nombreux compilateurs réduisent toujours la magnitude des constantes en effectuant une conversion entre < et <=, par ex. parce que l'opérande immédiat x86 a un codage plus court de 1 octet pour -128..127.

Pour ARM et en particulier AArch64, la possibilité de coder immédiatement dépend de la possibilité de faire pivoter un champ étroit dans n’importe quelle position du mot. Ainsi, cmp w0, #0x00f000 serait codable, alors que cmp w0, #0x00effff pourrait ne pas l'être. Ainsi, la règle de réduction de la comparaison par rapport à une constante de compilation ne s'applique pas toujours à AArch64.


<vs. <en général, y compris pour les conditions de variable d'exécution

En langage assembleur sur la plupart des machines, une comparaison pour <= a le même coût qu'une comparaison pour <. Ceci s’applique que vous changiez de branche, que vous le booléonniez pour créer un entier 0/1, ou que vous l’utilisiez comme prédicat pour une opération de sélection sans branche (comme x86 CMOV). Les autres réponses ont uniquement abordé cette partie de la question.

Mais cette question concerne les opérateurs C++, les entrée de l'optimiseur. Normalement, ils sont tout aussi efficaces; les conseils du livre semblent totalement fausses, car les compilateurs peuvent toujours transformer la comparaison qu'ils implémentent dans asm. Mais il existe au moins une exception où l'utilisation de <= 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 infini. Cela peut faire toute la différence en désactivant l’auto-vectorisation.

Le débordement non signé est bien défini comme un bouclage de base 2, contrairement au débordement signé (UB). Les compteurs de boucles signées sont généralement sécurisés grâce aux compilateurs qui optimisent en fonction du dépassement de mémoire signée UB ne se produisant pas: ++i <= size finira toujours par devenir faux. ( 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 tout ​​valeurs d'entrée possibles , sauf ceux qui conduisent à un comportement indéfini.

(Un simple i <= size créerait aussi le problème, mais je pensais que calculer une limite supérieure était un exemple plus réaliste d'introduction accidentelle de la possibilité d'une boucle infinie pour une entrée qui ne vous intéressait pas mais que le compilateur doit prendre en compte. .)

Dans ce cas, size=0 conduit à upper_bound=UINT_MAX, et i <= UINT_MAX est toujours vrai. Donc, cette boucle est infinie pour size=0, et le compilateur doit respecter cela même si, en tant que programmeur, vous n’avez probablement jamais l’intention de passer size = 0. Si le compilateur peut intégrer cette fonction à un appelant où il peut prouver que size = 0 est impossible, alors, il peut optimiser comme il le pouvait pour i < size.

Asm like if(!size) skip the loop;do{...}while(--size); est un moyen efficace d'optimiser une boucle for( i<size ) 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" (saut de queue)? ).

Mais cela {} tant que ne peut pas être infini: si entré avec size==0, nous obtenons 2 ^ n itérations. ( Itérer 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 indicateur de retenue comme dans asm.)

Lorsque le compteur de boucles est enveloppé, les compilateurs modernes "abandonnent" souvent et n'optimisent pas de manière aussi agressive.

Exemple: somme d'entiers de 1 à n

L'utilisation de i <= n non signé annule la reconnaissance idiomatique de Clang qui optimise les boucles sum(1 .. n) avec une forme fermée basée sur la n * (n+1) / 2 de Gauss formule.

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 simplement une boucle stupide de la part 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 de toute façon, donc le choix de la condition de boucle ne lui fait pas vraiment mal ; il se vectorise automatiquement avec l'addition d'entiers SIMD, en exécutant 4 valeurs i 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 aussi une boucle scalaire simple que je pense utiliser pour très petit n, et/ou pour le cas de la boucle infinie.

En passant, ces deux boucles gaspillent une instruction (et un uop sur les CPU de la famille Sandybridge) sur le temps système de la 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 de manière inconditionnelle, il n’utilise donc pas la valeur finale du compteur de boucles.

3
Peter Cordes

Seulement si les personnes qui ont créé les ordinateurs sont mauvaises 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, est juste une soustraction (la différence) et voir si c'est positif/négatif.
(Si la msb est définie, le nombre est négatif)

Comment vérifier a >= b? Sub a-b >= 0 Vérifiez si a-b est positif.
Comment vérifier a <= b? Sub 0 <= b-a Vérifiez si b-a est positif.
Comment vérifier a < b? Sub a-b < 0 Vérifiez si a-b est négatif.
Comment vérifier a > b? Sub 0 > b-a Vérifiez si b-a est négatif.

En termes simples, l’ordinateur peut simplement le faire 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 besoin de faire le ==0 ou ==1 non plus.
Pour le ==0, il pourrait simplement inverser la msb du circuit.

De toute façon, ils n'auraient certainement pas fait que a >= b soit calculé comme a>b || a==b lol

0
Puddle