Résumé:
Je recherche le moyen le plus rapide de calculer
(int) x / (int) y
sans obtenir une exception pour y==0
. Au lieu de cela, je veux juste un résultat arbitraire.
Contexte:
Lors du codage d'algorithmes de traitement d'image, j'ai souvent besoin de diviser par une valeur alpha (accumulée). La variante la plus simple est le code C simple avec arithmétique entière. Mon problème est que j'obtiens généralement une erreur de division par zéro pour les pixels de résultat avec alpha==0
. Cependant, ce sont exactement les pixels où le résultat n'a aucune importance: je me fiche des valeurs de couleur des pixels avec alpha==0
.
Détails:
Je recherche quelque chose comme:
result = (y==0)? 0 : x/y;
ou
result = x / MAX( y, 1 );
x et y sont des entiers positifs. Le code est exécuté un grand nombre de fois dans une boucle imbriquée, je cherche donc un moyen de me débarrasser du branchement conditionnel.
Lorsque y ne dépasse pas la plage d'octets, je suis satisfait de la solution
unsigned char kill_zero_table[256] = { 1, 1, 2, 3, 4, 5, 6, 7, [...] 255 };
[...]
result = x / kill_zero_table[y];
Mais cela ne fonctionne évidemment pas bien pour les plus grandes gammes.
Je suppose que la dernière question est la suivante: quel est le piratage le plus rapide qui change de 0 à une autre valeur entière, tout en laissant toutes les autres valeurs inchangées?
Clarifications
Je ne suis pas sûr à 100% que le branchement soit trop cher. Cependant, différents compilateurs sont utilisés, donc je préfère le benchmarking avec peu d'optimisations (ce qui est en effet discutable).
Bien sûr, les compilateurs sont excellents en ce qui concerne le twiddling, mais je ne peux pas exprimer le résultat "don't care" en C, donc le compilateur ne pourra jamais utiliser la gamme complète des optimisations.
Le code doit être entièrement compatible C, les principales plates-formes sont Linux 64 bits avec gcc & clang et MacOS.
la source
y += !y
- être ? Aucune branche nécessaire pour calculer cela. Vous pouvez comparer lesx / (y + !y)
contrex / max(y, 1)
et peut - être aussiy ? (x/y) : 0
. Je suppose qu'il n'y aura aucune branche dans l'un ou l'autre, du moins avec les optimisations activées.0
sections alpha sont énormes et contiguës. Il y a un endroit pour bidouiller avec les micro-optimisations, et les opérations par pixel sont exactement cet endroit.Réponses:
Inspiré par certains des commentaires, je me suis débarrassé de la branche sur mon Pentium et de mon
gcc
compilateur en utilisantLe compilateur reconnaît essentiellement qu'il peut utiliser un indicateur de condition du test dans l'addition.
Selon la demande, l'assemblage:
Comme cela s'est avéré être une question et une réponse très populaire, je vais élaborer un peu plus. L'exemple ci-dessus est basé sur un langage de programmation reconnu par un compilateur. Dans le cas ci-dessus, une expression booléenne est utilisée en arithmétique intégrale et l'utilisation d'indicateurs de condition est inventée dans le matériel à cette fin. En condition générale, les drapeaux ne sont accessibles en C qu'en utilisant idiom. C'est pourquoi il est si difficile de créer une bibliothèque d'entiers de précision multiple portable en C sans recourir à l'assemblage (en ligne). Je suppose que la plupart des compilateurs décents comprendront l'idiome ci-dessus.
Une autre façon d'éviter les branches, comme cela a également été remarqué dans certains des commentaires ci-dessus, est l'exécution prédéfinie. J'ai donc pris le premier code de philipp et mon code et je l'ai fait passer par le compilateur d'ARM et le compilateur GCC pour l'architecture ARM, qui propose une exécution prédéfinie. Les deux compilateurs évitent la branche dans les deux exemples de code:
Version de Philipp avec le compilateur ARM:
Version de Philipp avec GCC:
Mon code avec le compilateur ARM:
Mon code avec GCC:
Toutes les versions ont toujours besoin d'une branche vers la routine de division, car cette version de l'ARM ne dispose pas de matériel pour une division, mais le test
y == 0
est entièrement implémenté via une exécution prédéfinie.la source
constexpr
et éviter les moulages inutiles comme celui-ci:template<typename T, typename U> constexpr auto fdiv( T t, U u ) -> decltype(t/(u+!u)) { return t/(u+!u); }
Et si vous voulez255
,(lhs)/(rhs+!rhs) & -!rhs
|
pas dire&
. Oups -( (lhs)/(rhs+!rhs) ) | -!rhs
devrait définir votre valeur sur0xFFFFFFF
ifrhs
is0
, etlhs/rhs
ifrhs!=0
.Voici quelques chiffres concrets, sur Windows utilisant GCC 4.7.2:
Notez que je n'appelle pas intentionnellement
srand()
, donc celarand()
renvoie toujours exactement les mêmes résultats. Notez également que-DCHECK=0
ne compte que les zéros, de sorte que la fréquence d'apparition est évidente.Maintenant, compilez et chronométrez de différentes manières:
affiche la sortie qui peut être résumée dans un tableau:
Si les zéros sont rares, la
-DCHECK=2
version fonctionne mal. Au fur et à mesure que les zéros apparaissent de plus en plus, le-DCHECK=2
boîtier commence à fonctionner nettement mieux. Parmi les autres options, il n'y a vraiment pas beaucoup de différence.Car
-O3
, cependant, c'est une autre histoire:Là, le chèque 2 n'a aucun inconvénient par rapport aux autres chèques, et il conserve les avantages à mesure que les zéros deviennent plus courants.
Vous devriez vraiment mesurer pour voir ce qui se passe avec votre compilateur et vos exemples de données représentatifs, cependant.
la source
d=0
aléatoires, au lieu de le faire presque toujoursd!=0
, et vous verrez plus d'échecs de prédiction de branche. La prédiction de branche est excellente si une branche est presque toujours suivie, ou si la suite d'une branche ou de l'autre est vraiment grumeleuse ...d
itération est la boucle interne, donc lesd == 0
cas sont répartis uniformément. Et est-ce que 50% des cas sontd == 0
réalistes?0.002%
des cas estd==0
réaliste? Ils sont distribués partout, toutes les 65000 itérations vous frappez votred==0
cas. Bien que50%
cela ne se produise pas souvent,10%
ou1%
facilement, ou même90%
ou99%
. Le test tel qu'il est affiché ne teste vraiment que "si vous ne descendez jamais, jamais dans une branche, est-ce que la prédiction de branche rend inutile la suppression de la branche?", Auquel la réponse est "oui, mais ce n'est pas intéressant".Sans connaître la plate-forme, il n'y a aucun moyen de connaître la méthode la plus efficace, cependant, sur un système générique, cela peut être proche de l'optimum (en utilisant la syntaxe de l'assembleur Intel):
(Supposons que le diviseur est dans
ecx
et que le dividende est danseax
)Quatre instructions non ramifiées à cycle unique plus la division. Le quotient sera dans
eax
et le reste seraedx
à la fin. (Ce genre de montre pourquoi vous ne voulez pas envoyer un compilateur pour faire le travail d'un homme).la source
Selon ce lien , vous pouvez simplement bloquer le signal SIGFPE avec
sigaction()
(je ne l'ai pas essayé moi-même, mais je pense que cela devrait fonctionner).C'est l'approche la plus rapide possible si les erreurs de division par zéro sont extrêmement rares: vous ne payez que pour les divisions par zéro, pas pour les divisions valides, le chemin d'exécution normal n'est pas du tout modifié.
Cependant, le système d'exploitation sera impliqué dans chaque exception ignorée, ce qui est coûteux. Je pense que vous devriez avoir au moins mille bonnes divisions par division par zéro que vous ignorez. Si les exceptions sont plus fréquentes que cela, vous paierez probablement plus en ignorant les exceptions qu'en vérifiant chaque valeur avant la division.
la source