void reset_if_true(void*& ptr, bool cond)
{
if (cond)
ptr = nullptr;
}
Le naïf solution sera sans aucun doute la manière la plus rapide dans la majorité des cas. Bien qu'il dispose d'une succursale, qui peut être lent sur moderne pipeline processeurs, il n'est lente si la branche est mispredicted. Depuis la direction générale, les indicateurs sont très bon, de nos jours, à moins que la valeur de cond
est extrêmement imprévisible, il est probable qu'une simple branche conditionnelle est le moyen le plus rapide pour écrire le code.
Et si elle ne l'est pas, un bon compilateur doit savoir et être en mesure d'optimiser le code pour quelque chose de mieux, compte tenu de l'architecture cible. Qui va à gnasher729 point: il suffit d'écrire le code de la manière la plus simple et de laisser l'optimisation dans les mains de l'optimiseur.
Tout cela est de bon conseil en général, il est parfois poussée à l'extrême. Si vous soucier de la vitesse de ce code, vous devez vérifier et voir ce que le compilateur est en train de faire avec elle. Vérifiez le code de l'objet qu'il génère, et assurez-vous qu'il est raisonnable et que le code de fonction est prise en inline.
Un tel examen peut être assez révélateur. Par exemple, considérons x86-64, où les branches peuvent être très coûteux dans le cas où la direction de la prévision est déjouée (qui est vraiment le seul moment où c'est une question intéressante, supposons donc qu' cond
est totalement imprévisible). Presque tous les compilateurs vont générer à la suite de l'implémentation naïve:
reset_if_true(void*&, bool):
test sil, sil ; test 'cond'
je CondIsFalse
mov QWORD PTR [rdi], 0 ; set 'ptr' to nullptr, and fall through
CondIsFalse:
ret
C'est sur que serré de code que vous pouvez imaginer. Mais si vous mettez de la branche prédicteur dans un cas pathologique, il pourrait finir par être plus lent que d'utiliser un conditionnel déplacer:
reset_if_true(void*&, bool):
xor eax, eax ; pre-zero the register RAX
test sil, sil ; test 'cond'
cmove rax, QWORD PTR [rdi] ; if 'cond' is false, set the register RAX to 'ptr'
mov QWORD PTR [rdi], rax ; set 'ptr' to the value in the register RAX
ret ; (which is either 'ptr' or 0)
Conditionnel se déplace relativement élevé de temps de latence, de sorte qu'ils sont beaucoup plus lentes que bien prédit une branche, mais ils peuvent être plus rapides qu'un de totalement imprévisible de la branche. Vous attendez un compilateur de savoir ce lors du ciblage de l'architecture x86, mais il n'a pas (au moins dans ce simple exemple) ont aucune connaissance sur le cond
's de la prévisibilité. Il suppose le cas simple, que la direction de la prévision sera de votre côté, et génère Un code à la place de code B.
Si vous décidez que vous voulez encourager le compilateur de générer sans branches code à cause de l'imprévisible condition, vous pouvez essayer ce qui suit:
void reset_if_true_alt(void*& ptr, bool cond)
{
ptr = (cond) ? nullptr : ptr;
}
Il réussit à persuader les versions modernes de Clang pour générer sans branches code B, mais est un pessimization dans GCC et MSVC. Si vous n'avez pas coché l'assembly généré, vous n'auriez pas connu que. Si vous voulez forcer la GCC et MSVC pour générer sans branches code, vous aurez à travailler plus dur. Par exemple, vous pouvez utiliser la variante publié dans la question:
void reset_if_true(void*& ptr, bool cond)
{
void* p[] = { ptr, nullptr };
ptr = p[cond];
}
Lorsque le ciblage des x86, tous les compilateurs génèrent sans branches code, mais il n'est pas spécialement joli code. En fait, aucun d'entre eux génèrent conditionnelle se déplace. Au lieu de cela, vous obtenez plusieurs accès à la mémoire dans l'ordre pour construire le tableau:
reset_if_true_alt(void*&, bool):
mov rax, QWORD PTR [rdi]
movzx esi, sil
mov QWORD PTR [rsp-16], 0
mov QWORD PTR [rsp-24], rax
mov rax, QWORD PTR [rsp-24+rsi*8]
mov QWORD PTR [rdi], rax
ret
Laid et très probablement inefficace. J'avais prédire qu'il donne à l'un saut conditionnel version une course pour son argent, même dans le cas où la branche est mispredicted. Vous auriez à l'indice de référence pour être sûr, bien sûr, mais il n'est probablement pas un bon choix.
Si vous étiez encore désespérée pour éliminer la branche sur MSVC ou GCC, vous avez à faire quelque chose de plus laid impliquant réinterpréter le pointeur de bits et de se tourner. Quelque chose comme:
void reset_if_true_alt(void*& ptr, bool cond)
{
std::uintptr_t p = reinterpret_cast<std::uintptr_t&>(ptr);
p &= -(!cond);
ptr = reinterpret_cast<void*>(p);
}
Qui vous donnera les éléments suivants:
reset_if_true_alt(void*&, bool):
xor eax, eax
test sil, sil
sete al
neg eax
cdqe
and QWORD PTR [rdi], rax
ret
Encore une fois, ici, nous avons des instructions plus qu'une simple branche, mais au moins ils sont relativement faible temps de latence des instructions. Un test sur des données réalistes vous dira si le compromis est en vaut la peine. Et vous donner la justification, vous devez mettre un commentaire si vous allez en fait le check-in code comme celui-ci.
Une fois que je suis descendu de la bit-tourner trou de lapin, j'ai été en mesure de forcer MSVC et de la GCC à l'utilisation conditionnelle de déplacer les instructions. Apparemment ils ne le faisaient pas cette optimisation, parce que nous nous intéressions à un pointeur:
void reset_if_true_alt(void*& ptr, bool cond)
{
std::uintptr_t p = reinterpret_cast<std::uintptr_t&>(ptr);
ptr = reinterpret_cast<void*>(cond ? 0 : p);
}
reset_if_true_alt(void*&, bool):
mov rax, QWORD PTR [rdi]
xor edx, edx
test sil, sil
cmovne rax, rdx
mov QWORD PTR [rdi], rax
ret
Compte tenu de la latence de CMOVNE et le même nombre d'instructions, je ne sais pas si ce serait réellement plus rapide que la version précédente. L'indice de référence que vous avez couru vous dirais si elle l'était.
De même, si nous avons peu-l'ornement de l'état, de nous sauver nous-mêmes un accès à la mémoire:
void reset_if_true_alt(void*& ptr, bool cond)
{
std::uintptr_t c = (cond ? 0 : -1);
reinterpret_cast<std::uintptr_t&>(ptr) &= c;
}
reset_if_true_alt(void*&, bool):
xor esi, 1
movzx esi, sil
neg rsi
and QWORD PTR [rdi], rsi
ret
(C'est GCC. MSVC fait quelque chose de légèrement différent, préférant le son caractéristique de la séquence de neg
, sbb
, neg
, et dec
des instructions, mais les deux sont moralement équivalent. Clang transforme dans le même conditionnelle déplacement que nous avons vu générer ci-dessus). Cela peut être le meilleur code pourtant, si nous avons besoin pour éviter les branches, considérant qu'il génère sane de sortie sur tous testé les compilateurs tout en préservant (dans une certaine mesure) de la lisibilité dans le code source.