12
votes

Micro-optimisation d'une fonction de comparaison C ++

J'ai un comparer () code> fonction qui ressemble à ceci:

_Z7Comparebii:
.LFB4:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    pushq   %rbp
    .cfi_def_cfa_offset 16
    movq    %rsp, %rbp
    .cfi_offset 6, -16
    .cfi_def_cfa_register 6
    movl    %edi, %eax
    movl    %esi, -8(%rbp)
    movl    %edx, -12(%rbp)
    movb    %al, -4(%rbp)
    cmpb    $0, -4(%rbp)
    je      .L2
    movl    -8(%rbp), %eax
    cmpl    -12(%rbp), %eax
    setge   %al
    jmp     .L3
.L2:
    movl    -8(%rbp), %eax
    cmpl    -12(%rbp), %eax
    setle   %al
.L3:
    leave
    ret
    .cfi_endproc
.LFE4:
    .size   _Z7Comparebii, .-_Z7Comparebii
    .section        .text._Z8Compare2bii,"axG",@progbits,_Z8Compare2bii,comdat
    .weak   _Z8Compare2bii
    .type   _Z8Compare2bii, @function
_Z8Compare2bii:
.LFB5:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    pushq   %rbp
    .cfi_def_cfa_offset 16
    movq    %rsp, %rbp
    .cfi_offset 6, -16
    .cfi_def_cfa_register 6
    movl    %edi, %eax
    movl    %esi, -24(%rbp)
    movl    %edx, -28(%rbp)
    movb    %al, -20(%rbp)
    movw    $0, -16(%rbp)
    movl    -24(%rbp), %eax
    cmpl    -28(%rbp), %eax
    setle   %al
    movb    %al, -16(%rbp)
    movl    -24(%rbp), %eax
    cmpl    -28(%rbp), %eax
    setge   %al
    movb    %al, -15(%rbp)
    movzbl  -20(%rbp), %eax
    cltq
    movzbl  -16(%rbp,%rax), %eax
    leave
    ret
    .cfi_endproc
.LFE5:
    .size   _Z8Compare2bii, .-_Z8Compare2bii
    .text


28 commentaires

Optimiser pour éviter la prédiction des succursales N'est-ce pas un oxymoron?


Vous devrez partager le code de montage depuis ce qui se passe dépend beaucoup de quel compilateur que vous utilisez et à quel niveau d'optimisation.


@ Last Line: Alors pourquoi ne postez-vous pas l'assemblage?


Vous n'avez pas mis la graine. Peut-être que le compilateur est assez intelligent de savoir ce que rand () revient dans ce cas? Juste une pensée rapide. Aussi, vous devriez vraiment comparer l'Assemblée. Même si vous êtes de l'Assemblée-Code-analphabète, vous pouvez toujours montrer la différence.


Aurait pu être un mouvement conditionnel .. montrer l'assemblage.


Il est difficile de dire sans voir ce que fait le compilateur.


Quelle est votre arc et votre compilateur?


Quel est le timing pour un [i] = faux? Plus rapide que le 1.61NS?


@Lukarahne: J'utilise g ++ 4.4.3-4ubuntu5 sur un Intel Xeon X5570. @ Autres: supporte avec moi pendant que je découvre comment poster l'assemblée.


@Antonroth Comparer () et Comparez2 () Horcier à ~ 3.14ns et ~ 1.61NS, respectivement, dans les 3 cas [I]: rand (), vrai et faux.


Attends, ce qui est plus rapide? Votre dernier commentaire vient de dire que comparer () est plus rapide, mais vous en avez l'inverse dans votre question.


Ne vous inquiétez pas de répondre à mon commentaire.


Juste pour confirmer, est-ce compilé avec -O3 ?


BTW Ce que vous voulez éviter est les branches, pas la prédiction de la branche - la prédiction des succursales est un bonne chose.


@Mysticia compare2 () est plus rapide, j'ai tapé mon commentaire à Antonroth à la hâte et ensuite la éditer rapidement par la suite.


@Olicharlesworth oui, avec -O3


@MarkRansom: Sauf si cela ne prédit pas de manière incorrecte un pourcentage important du temps ...


Auquel cas, le terme serait "éviter les délais de branche", qui peut également être fait en évitant complètement les branches.


Oui. Dans ce cas, la prédiction des succursales sera fausse un pourcentage important du temps.


@Olicharlesworth, même s'il s'agissait de 100% du temps, ce ne serait pas pire que de caler le pipeline le ferait-il? Et de manière réaliste, cela ne devrait pas être faux de plus de 50%.


La question posée dans le premier commentaire sur cette question, heureusement ignorée par tout le monde, est «est une mauvaise prédiction que pas de prédiction»?


@MarkRansom Eh bien, si vous comparez deux processeurs à proximité identiques: une avec et une avec une prédiction sans succursale, il est probable que celui-ci n'est probablement pas plus rapide que celui avec celui-ci et mal prédit 100% du temps. C'est parce que le nettoyage de mauvaise réputation a des frais généraux importants.


@David Notez le SEEGE et SETLE INSTRUCTIONS? Ils fournissent un résultat booléen d'une comparaison sans faire de branches. Les deux versions utilisent ces instructions. Ne répond pas à votre question mais je pensais que vous le trouveriez intéressant.


Pourriez-vous essayer (rand () / 256)% 2 au lieu de rand ()% 2 ? Le moins important de rand () peut être bien prévisible et tout aussi bon que true ou false pour le prédicteur de la branche.


@MarkRansom: C'est vrai, pour des données aléatoires, on pourrait s'attendre à ce que 50% de taux de répression (je pense). Il faudrait qu'une contribution pathologique spécifique pour provoquer une hypothèse de 100%.


J'ai essayé (rand ()% 256) <128 au lieu de rand ()% 2 même résultat.


Il est facile de dire pourquoi les deux fonctions fonctionnent également pour un premier paramètre aléatoire / constant: aucun d'entre eux n'utilise des instructions de branche (lorsqu'elles sont inlinées / optimisées). Mais je ne sais pas pourquoi compare2 est plus rapide. En fait, la seule différence est que comparer utilise cmov instruction tandis que compare2 utilise deux fois plus d'accès à la mémoire. Je m'attendrais à ce que compare2 soit deux fois plus lent que comparer ...


@EvGeny Il y a clairement une succursale dans Comparer () - au moins lorsqu'il n'est pas inliné (et je ne vois pas pourquoi l'allusion aiderait si le compilateur ne peut pas déduire la valeur de plus en plus). G ++ 4.5.3 Sous Cygwin crée le code attendu pour moi sans surprises (trop de coups de mouvements dans l'assemblée donnée pour moi)


3 Réponses :


1
votes

Que diriez-vous ...

inline bool Compare4(bool greater, int p1, int p2) 
{
  return (greater ^ (p1 <= p2)) | (p1 == p2);
}


8 commentaires

Il me semble que compare3 (true, 1,1)! = Compare3 (false, 1,1) , qui rendrait la fonction incorrecte. Idem pour compare4 () .


Ajouter | (P1 == p2) et être heureux.


Hmm, je n'ai pas testé le code. Pas de compilateur dans ma machine à domicile. Va vérifier maintenant.


Bon sang, j'ai raté cette condition. Corrigé-le maintenant. Merci.


On dirait que Compare4 est le plus rapide.


En fait, par mes tests, je vois que la comparaison2 est la plus rapide. Je reçois la comparaison2 à environ 1,65 n et comparez4 à environ 2,55nes.


Cela ne traite pas vraiment de la question (c'est-à-dire "pourquoi la différence entre comparer () et comparer2 ()?")


@Olicharlesworth c'est un euphémisme;)



4
votes

Je pense que je pensais la majeure partie de celle-ci.

Lorsque j'ai posté l'assemblage pour les fonctions de mon op edit, j'ai noté que la version inlinette pourrait être différente. Je n'avais pas examiné ni posté le code temporel car c'était poilu, et parce que je pensais que le processus d'inlinage ne changerait pas si la ramification a lieu ou non dans comparer () code>. P> Lorsque je n'ai pas induit la fonction et répété mes mesures, j'ai eu les résultats suivants: p> xxx pré>

puis, lorsque j'ai remplacé a [i] = rand ( )% 2 code> avec A [i] = false code>, j'ai eu les suivants: p>

Compare(): 2.59ns avg
Compare2(): 3.16ns avg


0 commentaires

3
votes

J'ai écrit une bibliothèque C ++ appelée Celerero conçu pour tester de telles optimisations et alternatives. (Auto sans vergogne Promotion: https://github.com/digitalinblue/celero )

J'ai couru votre Coques utilisant le code suivant: p> xxx pré>

Les résultats sont présentés ci-dessous: p> xxx pré>

étant donné ce test, il ressemble à Compare2 Strort> est la meilleure option de cette micro-optimisation. p>

EDIT: P>

assemblage2 (le meilleur cas): p> xxx

ASSEMBLAGE3 ASSEMBLAGE (le meilleur des boîtiers suivants): P>

xor r11d, r11d
cmp r8d, r9d
mov r10d, r11d
setg    r10b
test    dl, dl
mov ecx, r11d
sete    cl
mov eax, r11d
cmp ecx, r10d
setne   al
cmp r8d, r9d
sete    r11b
or  eax, r11d


6 commentaires

Intéressant, mais ici, nous voulons savoir pourquoi c'est.


J'ai ajouté l'assemblée à ma réponse.


Je ne suis pas un fan de la façon dont vous avez fait benchmarking. Les temps mesurés sont dominés par le coût de rand () , masquant la véritable différence de performances entre les variantes.


C'est vrai que Rand () est coûteux, mais le coût est identique pour chaque test, il peut donc être pris en compte. Ce qui devrait être comparé est une heure de base (relative). Cela montre ce qui est vraiment plus rapide et de combien. La mesure de l'heure d'exécution moyenne est en fait incorrecte. Référence: codeproject.com/articles/525576/... < / a>


Compte tenu de la base de référence, la comparaison2 est de 1,014870 fois plus lente que la mesure de base et la comparaison3 est de 1,027476 fois plus lente.


Oh je vois. Je suppose que j'aurais juste vu la déclaration "Comparer2 est de 1,85x plus rapide que le comparateur3" (depuis (1.027476-1) / (1.014870-1) = 1,85). La façon dont vos chiffres sont rapportées rend l'ampleur de l'amélioration non évidente, à mon avis.