Donc, nous connaissons tous les règles de comparaison signées/non signées C/C++ dans lesquelles -1 > 2u == true
et je me trouve dans une situation où je souhaite implémenter efficacement les comparaisons "correctes".
Ma question est, qui est plus efficace avec des considérations à autant d'architectures que les gens sont familiers. De toute évidence, Intel et ARM ont un poids plus élevé.
Donné:
int x;
unsigned int y;
if (x < y) {}
Est-il préférable de promouvoir:
x < y => (int64)x < (int64)y
ou est-il préférable d'effectuer 2 comparaisons, à savoir:
x < y => (x < 0 || x < y)
La première implique une extension zéro, une extension de signe et une comparaison + branche, et la dernière ne nécessite aucune opération d'extension de signe, mais deux branches consécutives cmp +.
La sagesse traditionnelle suggère que les branches sont plus chères que les panneaux étendus, ce qui sera à la fois pipeline, mais il y a un blocage entre les extensions et la comparaison unique dans le premier cas, alors que dans le second cas, je peux imaginer que certaines architectures pourraient pipeline les 2 comparaisons, mais suivies de 2 branches conditionnelles?
Il existe un autre cas, où la valeur non signée est un type plus petit que le type signé, ce qui signifie que cela peut être fait avec une seule extension de zéro à la longueur du type signé, puis une seule comparaison ... dans ce cas, est-il préférable utiliser la version extend + cmp, ou la méthode de la comparaison double est-elle toujours préférée?
Intel? BRAS? D'autres? Je ne sais pas s'il y a une bonne réponse ici, mais j'aimerais entendre les gens prendre. Les performances à bas niveau sont difficiles à prévoir de nos jours, en particulier sur Intel et de plus en plus sur ARM.
Modifier:
J'ajouterais qu'il existe une résolution évidente, où les types ont une taille égale à celle de l'architecture int width; dans ce cas, il est évident que la solution à deux comparaisons est préférable, car la promotion en elle-même ne peut être effectuée efficacement. Il est clair que mon exemple int
répond à cette condition pour les architectures 32 bits et que vous pouvez transposer l'expérience de pensée à short
pour l'exercice appliqué aux plates-formes 32 bits.
Edit 2:
Désolé, j'ai oublié la u
dans -1 > 2u
! > _ <
Edit 3:
Je souhaite modifier la situation pour supposer que le résultat de la comparaison est une branche réelle et que le résultat n'est PAS renvoyé sous forme de valeur booléenne. Voici comment je préférerais le look de la structure; Bien que cela soulève un point intéressant, il existe un autre ensemble de permutations lorsque le résultat est un bool vs une branche.
int g;
void fun(int x, unsigned in y) { if((long long)x < (long long)y) g = 10; }
void gun(int x, unsigned in y) { if(x < 0 || x < y) g = 10; }
Cela produit généralement la branche prévue implicite lorsque vous rencontrez un if
;)
Eh bien, vous avez correctement décrit la situation: C/C++ n’a aucun moyen de faire une comparaison complète signée int/unsigned int avec une seule comparaison.
Je serais surpris que la promotion sur int64 soit plus rapide que deux comparaisons. D'après mon expérience, les compilateurs savent très bien qu'une sous-expression de ce type est pure (sans effets secondaires) et évite ainsi le recours à une deuxième branche. (Vous pouvez également explicitement désactiver le court-circuit en utilisant bitwise-or: (x < 0) | (x < y)
.) Par contre, mon expérience est que les compilateurs n'ont PAS tendance à faire beaucoup d'optimisation de cas spéciaux sur des entiers supérieurs à la taille native de Word, donc (int64)x < (int64)y
est plutôt probable. faire réellement une comparaison int complète.
En bout de ligne, il n’existe aucune incantation qui garantisse le meilleur code machine possible sur n’importe quel processeur, mais pour les compilateurs les plus courants sur les processeurs les plus courants, je suppose que le formulaire à deux comparaisons ne serait pas plus lent que la promotion. -int64 forme.
EDIT: Certains manigances sur Godbolt confirment que sur ARM32, GCC met beaucoup trop de machines dans l’approche int64. VC fait la même chose sur x86. Avec x64, cependant, l’approche int64 est en réalité une instruction plus courte (puisque la promotion et la comparaison 64 bits sont triviales). Le traitement en pipeline peut toutefois faire varier la performance réelle dans les deux sens. https://godbolt.org/g/wyG4yC
Vous devez juger cela au cas par cas. Il existe plusieurs raisons pour lesquelles les types signés seraient utilisés dans un programme:
int
dans tout son programme sans trop y penser.0
, de type int
.Dans le cas de 1), l’arithmétique doit être effectuée avec une arithmétique signée. Vous devez ensuite convertir le type le plus petit possible pour contenir les valeurs maximales attendues.
Supposons par exemple qu'une valeur puisse aller de -10000
à 10000
. Vous devrez ensuite utiliser un type signé 16 bits pour le représenter. Le type correct à utiliser ensuite, indépendamment de la plate-forme, est int_fast16_t
.
Les types int_fastn_t
et uint_fastn_t
exigent que le type soit au moins égal à n mais le compilateur est autorisé à choisir un type plus grand s'il donne un code plus rapide/un meilleur alignement.
2) est guéri en étudiant stdint.h
et en cessant d’être paresseux. En tant que programmeur, il faut toujours prendre en compte la taille et la signature de chaque variable déclarée dans le programme . Cela doit être fait au moment de la déclaration. Ou si vous obtenez une révélation plus tard, revenez en arrière et changez le type.
Si vous ne considérez pas les types avec soin, vous finirez certainement par écrire de nombreux bogues, souvent subtils. Ceci est particulièrement important en C++, qui est plus délicat à propos de la correction du type que C.
Lorsque "sloppy typing" est utilisé, le type souhaité est le plus souvent non signé plutôt que signé. Considérez cet exemple de frappe bâclée:
for(int i=0; i<n; i++)
Il n’a aucun sens d’utiliser l’entier signé ici, alors pourquoi l’utiliser? Très probablement, vous parcourez un tableau ou un conteneur, puis le type correct à utiliser est size_t
.
Ou bien, si vous connaissez la taille maximale que n
peut contenir, par exemple 100, vous pouvez utiliser le type le plus approprié pour cela:
for(uint_fast8_t i=0; i<100; i++)
3) est également guéri en étudiant. Notamment les différentes règles pour les promotions implicites qui existent dans ces langues, telles que les conversions arithmétiques habituelles et la promotion entière .
Compte tenu de la configuration spécifique que vous avez présentée:
int x;
unsigned int y;
et votre intention apparente d'évaluer si la valeur de x
est numériquement inférieure à celle de y
, en respectant le signe de x
, je serais enclin à l'écrire
if ((x < 0) || (x < y)) {}
c'est votre deuxième alternative. Il exprime clairement l'intention et peut être étendu à des types plus larges, à condition que la valeur représentable maximale du type y
soit au moins égale à la valeur représentable maximale du type x
. Ainsi, si vous êtes prêt à stipuler que les arguments auront cette forme, vous pourrez même l'écrire sous la forme d'une macro - évitez les yeux, adhérents du C++.
Convertir les deux arguments en un type entier signé, 64 bits, n’est pas une solution portable, car rien ne garantit qu’il s’agirait bien de promotion de int
ou unsigned int
. Il n'est pas non plus extensible à des types plus larges.
En ce qui concerne la performance relative de vos deux alternatives, je doute que la différence soit grande, mais si cela vous importe, vous voudrez alors rédiger un indice de référence prudent. Je pouvais imaginer la variante portable nécessitant une instruction machine de plus que l’autre, mais aussi une instruction de moins. Ce n'est que si ces comparaisons dominent les performances de votre application qu'une seule instruction produira une différence notable dans un sens ou dans l'autre.
Bien sûr, cela est spécifique à la situation que vous avez présentée. Si vous souhaitez gérer des comparaisons mixtes signées/non signées dans l'un ou l'autre ordre, pour de nombreux types différents, telles qu'elles sont triées au moment de la compilation, un wrapper basé sur des modèles peut vous aider à cela (et cela poserait la question de l'utilisation d'une macro), mais je suppose que vous vous interrogez sur les détails de la comparaison elle-même.
La version à deux branches serait certainement plus lente, mais en réalité, rien de tout cela n’est à deux branches… ni à une seule branche… sur x86.
Par exemple x86 gcc 7.1 sera pour la source C++:
bool compare(int x, unsigned int y) {
return (x < y); // "wrong" (will emit warning)
}
bool compare2(int x, unsigned int y) {
return (x < 0 || static_cast<unsigned int>(x) < y);
}
bool compare3(int x, unsigned int y) {
return static_cast<long long>(x) < static_cast<long long>(y);
}
Produire cette Assemblée ( Démo live de godbolt ):
compare(int, unsigned int):
cmp edi, esi
setb al
ret
compare2(int, unsigned int):
mov edx, edi
shr edx, 31
cmp edi, esi
setb al
or eax, edx
ret
compare3(int, unsigned int):
movsx rdi, edi
mov esi, esi
cmp rdi, rsi
setl al
ret
Et si vous essayez d'utiliser ces codes dans un code plus complexe, ils seront en ligne dans 99% des cas. Sans profiler, c’est juste deviner, mais "par instinct", je dirais que compare3
est "plus rapide", surtout quand il est exécuté dans un code erroné (un peu drôle, il fait la promotion appropriée 32-> 64 même pour les arguments intimes, il faudrait beaucoup d'efforts pour produire des appels de code comparables à certains désordres dans les 32b supérieurs de esi
... ... mais il s'en débarrasserait probablement quand il serait intégré à un calcul plus complexe, où il serait noté que l'argument est aussi déjà étendu, donc le compare3
est alors encore plus simple + plus court).
... comme je l'ai dit dans un commentaire, je n'effectue pas les tâches pour lesquelles j'en aurais besoin, par exemple, je ne peux pas imaginer travailler sur un domaine pour lequel la plage de données valide est inconnue. C++ est un ajustement parfait et j’apprécie exactement son fonctionnement (<
pour les types signés par rapport aux types non signés est bien défini et donne le code le plus court/le plus rapide, plus un avertissement est émis pour que je sois le programmeur responsable de le valider. besoin de changer la source de manière appropriée).
Une astuce portable que vous pouvez faire est de vérifier si vous pouvez élargir les deux arguments à intmax_t
à partir de <stdint.h>
, qui est le type intégral le plus large pris en charge par une implémentation. Vous pouvez vérifier (sizeof(intmax_t) > sizeof(x) && sizeof(intmax_t) >= sizeof(y))
et, le cas échéant, procéder à une conversion élargie. Cela fonctionne dans le cas très courant où int
a une largeur de 32 bits et long long int
une largeur de 64 bits.
En C++, vous pouvez faire des choses intelligentes avec un modèle de comparaison sécurisé qui vérifie std::numeric_limits<T>
sur ses arguments. Voici une version. (Compilez avec -Wno-sign-compare
sur gcc ou clang!)
#include <cassert>
#include <cstdint>
#include <limits>
using std::intmax_t;
using std::uintmax_t;
template<typename T, typename U>
inline bool safe_gt( T x, U y ) {
constexpr auto tinfo = std::numeric_limits<T>();
constexpr auto uinfo = std::numeric_limits<U>();
constexpr auto maxinfo = std::numeric_limits<intmax_t>();
static_assert(tinfo.is_integer, "");
static_assert(uinfo.is_integer, "");
if ( tinfo.is_signed == uinfo.is_signed )
return x > y;
else if ( maxinfo.max() >= tinfo.max() &&
maxinfo.max() >= uinfo.max() )
return static_cast<intmax_t>(x) > static_cast<intmax_t>(y);
else if (tinfo.is_signed) // x is signed, y unsigned.
return x > 0 && x > y;
else // y is signed, x unsigned.
return y < 0 || x > y;
}
int main()
{
assert(-2 > 1U);
assert(!safe_gt(-2, 1U));
assert(safe_gt(1U, -2));
assert(safe_gt(1UL, -2L));
assert(safe_gt(1ULL, -2LL));
assert(safe_gt(1ULL, -2));
}
Il pourrait être mis au courant de la virgule flottante en changeant deux lignes.
Avec un petit modèle jiggery-pokery, je pense que nous pouvons obtenir automatiquement le résultat optimal dans tous les scénarios:
#include<iostream>
#include<cassert>
template<class T> auto make_unsigned(T i) -> T { return i; }
auto make_unsigned(int i) -> unsigned int {
assert(i >= 0);
return static_cast<unsigned int>(i);
}
auto make_unsigned(short i) -> unsigned short {
assert(i >= 0);
return static_cast<unsigned short>(i);
}
auto make_unsigned(long long i) -> unsigned long long {
assert(i >= 0);
return static_cast<unsigned long long>(i);
}
template<
class I1,
class I2,
std::enable_if_t<(std::is_signed<I1>::value and std::is_signed<I2>::value)
or (not std::is_signed<I1>::value and not std::is_signed<I2>::value)>* = nullptr
>
bool unsigned_less(I1 i1, I2 i2) {
return i1 < i2;
};
template<
class I1,
class I2,
std::enable_if_t<std::is_signed<I1>::value and not std::is_signed<I2>::value>* = nullptr
>
bool unsigned_less(I1 i1, I2 i2) {
return (i1 < 0) or make_unsigned(i1) < i2;
};
template<
class I1,
class I2,
std::enable_if_t<not std::is_signed<I1>::value and std::is_signed<I2>::value>* = nullptr
>
bool unsigned_less(I1 i1, I2 i2) {
return not (i2 < 0) and i1 < make_unsigned(i2);
};
int main() {
short a = 1;
unsigned int b = 2;
std::cout << unsigned_less(a, b) << std::endl;
using uint = unsigned int;
using ushort = unsigned short;
std::cout << unsigned_less(ushort(1), int(3)) << std::endl;
std::cout << unsigned_less(int(-1), uint(0)) << std::endl;
std::cout << unsigned_less(int(1), uint(0)) << std::endl;
return 0;
}
Regardez le discours d’Andrei Alexandrescus à la récente conférence D à Berlin sur Design by Introspection.
Dans ce document, il montre comment concevoir une classe int vérifiée au moment de la conception et l'une des fonctionnalités qu'il a évoquées est exactement ceci: comment comparer signé et non signé.
Fondamentalement, vous devez effectuer 2 comparaisons
Si (signé_var <0), retourne unsigned_var Sinon promeut/transforme la commande signed_var en unsigned_var puis compare