Je recherche un moyen efficace de déterminer la position du bit le moins significatif qui est défini dans un entier, par exemple pour 0x0FF0, ce serait 4.
Une implémentation triviale est la suivante:
unsigned GetLowestBitPos(unsigned value)
{
assert(value != 0); // handled separately
unsigned pos = 0;
while (!(value & 1))
{
value >>= 1;
++pos;
}
return pos;
}
Avez-vous des idées pour en extraire certains cycles?
(Remarque: cette question s'adresse aux personnes qui aiment de telles choses, et non aux personnes qui me disent que l'optimisation xyz est maléfique.)
[modifier] Merci à tous pour les idées! J'ai aussi appris quelques autres choses. Cool!
c++
c
optimization
bit-manipulation
Peterchen
la source
la source
Réponses:
Bit Twiddling Hacks offre une excellente collection de hacks de twiddling, avec une discussion sur les performances / optimisation en annexe. Ma solution préférée pour votre problème (à partir de ce site) est «multiplier et rechercher»:
Références utiles:
la source
__builtin_ffsl
ouffsl
?Pourquoi ne pas utiliser les ffs intégrés ? (J'ai récupéré une page de manuel de Linux, mais elle est plus largement disponible que cela.)
la source
Il existe une instruction d'assemblage x86 (
bsf
) qui le fera. :)Plus optimisé?!
Note latérale:
L'optimisation à ce niveau est intrinsèquement dépendante de l'architecture. Les processeurs actuels sont trop complexes (en termes de prédiction de branchement, d'erreurs de cache, de pipelining) qu'il est si difficile de prédire quel code est exécuté plus rapidement sur quelle architecture. Diminuer les opérations de 32 à 9 ou des choses comme ça peut même diminuer les performances sur certaines architectures. Un code optimisé sur une seule architecture peut entraîner un code plus mauvais dans l'autre. Je pense que vous optimiseriez cela pour un processeur spécifique ou le laisseriez tel quel et laisser le compilateur choisir ce qu'il pense être le meilleur.
la source
La plupart des architectures modernes auront des instructions pour trouver la position du bit de jeu le plus bas, ou du bit de jeu le plus élevé, ou pour compter le nombre de zéros en tête, etc.
Si vous avez une instruction de cette classe, vous pouvez imiter les autres à moindre coût.
Prenez un moment pour le parcourir sur papier et réalisez que
x & (x-1)
cela effacera le bit le plus bas de x et( x & ~(x-1) )
ne renverra que le bit le plus bas, quelle que soit l'architecture, la longueur du mot, etc. -zeroes / bit le plus élevé pour trouver le bit le plus bas s'il n'y a pas d'instruction explicite pour le faire.S'il n'y a pas du tout de support matériel pertinent, l'implémentation de multiplication et de recherche de count-Leading-zeroes donnée ici ou l'un de ceux de la page Bit Twiddling Hacks peut être convertie de manière triviale pour donner le bit le plus bas en utilisant les identités ci-dessus et a l'avantage d'être sans succursales.
la source
Weee, des tas de solutions et pas une référence en vue. Vous devriez avoir honte de vous ;-)
Ma machine est un Intel i530 (2,9 GHz), exécutant Windows 7 64 bits. J'ai compilé avec une version 32 bits de MinGW.
Mon code:
la source
BSF
une fausse dépendance sur sa sortie (puisque le comportement réel lorsque input = 0 doit laisser la sortie inchangée). gcc transforme malheureusement cela en une dépendance portée par la boucle en ne supprimant pas le registre entre les itérations de la boucle. Ainsi, la boucle doit fonctionner à un tous les 5 cycles, goulot d'étranglement sur BSF (3) + CMOV (2) latence.ffs()
aurait dû avoir un débit de un par horloge (3 uops, 1 pour BSF et 2 pour CMOV, et ils peuvent fonctionner sur différents ports). Avec la même surcharge de boucle, ce sont 7 uops ALU qui peuvent fonctionner (sur votre CPU) à 3 par horloge. Les frais généraux dominent! Source: agner.org/optimizebsf ecx, [ebx+edx*4]
n'est pas traitéeecx
comme une entrée à attendre. (ECX a été écrit pour la dernière fois par la CMOV de l'itération précédente). Mais le CPU se comporte de cette façon, pour implémenter le comportement "laisser dest inchangé si la source est zéro" (donc ce n'est pas vraiment un faux dep comme pour TZCNT; une dépendance de données est nécessaire car il n'y a pas de branchement + exécution spéculative sur l'hypothèse que l'entrée est non nulle). Nous pourrions le surmonter en ajoutant unxor ecx,ecx
avant lebsf
, pour briser la dépendance à ECX.La solution la plus rapide (non intrinsèque / non assembleur) à ce problème consiste à rechercher l'octet le plus bas, puis à utiliser cet octet dans une table de recherche à 256 entrées. Cela vous donne une performance dans le pire des cas de quatre instructions conditionnelles et un meilleur cas de 1. Non seulement il s'agit du moins d'instructions, mais aussi du moins de branches, ce qui est très important sur le matériel moderne.
Votre table (256 entrées 8 bits) doit contenir l'index du LSB pour chaque nombre compris entre 0 et 255. Vous vérifiez chaque octet de votre valeur et trouvez l'octet non nul le plus bas, puis utilisez cette valeur pour rechercher l'index réel.
Cela nécessite 256 octets de mémoire, mais si la vitesse de cette fonction est si importante alors que 256 octets en valent la peine,
Par exemple
la source
OMG vient de faire une spirale.
Ce qui manque à la plupart de ces exemples, c'est un peu de compréhension du fonctionnement de tout le matériel.
Chaque fois que vous avez une branche, le CPU doit deviner quelle branche sera prise. Le tube d'instructions est chargé avec les instructions qui mènent sur le chemin deviné. Si le CPU a mal deviné, le tube d'instructions est vidé et l'autre branche doit être chargée.
Considérez la simple boucle while en haut. La supposition sera de rester dans la boucle. Ce sera faux au moins une fois quand il sortira de la boucle. Cela va rincer le tuyau d'instructions. Ce comportement est légèrement meilleur que de supposer qu'il quittera la boucle, auquel cas il viderait le tube d'instructions à chaque itération.
La quantité de cycles CPU perdus varie fortement d'un type de processeur à l'autre. Mais vous pouvez vous attendre entre 20 et 150 cycles CPU perdus.
Le pire groupe suivant est celui où vous pensez que vous allez économiser quelques itérations en divisant la valeur en plus petits morceaux et en ajoutant plusieurs branches supplémentaires. Chacune de ces branches ajoute une opportunité supplémentaire de rincer le tuyau d'instructions et coûte encore 20 à 150 cycles d'horloge.
Voyons ce qui se passe lorsque vous recherchez une valeur dans une table. Il y a de fortes chances que la valeur ne soit pas actuellement en cache, du moins pas la première fois que votre fonction est appelée. Cela signifie que le processeur est bloqué pendant que la valeur est chargée à partir du cache. Là encore, cela varie d'une machine à l'autre. Les nouvelles puces Intel utilisent en fait cette opportunité pour permuter les threads pendant que le thread actuel attend la fin du chargement du cache. Cela pourrait facilement être plus coûteux qu'un rinçage de tuyau d'instructions, mais si vous effectuez cette opération plusieurs fois, elle ne se produira probablement qu'une seule fois.
Il est clair que la solution à temps constant la plus rapide est celle qui implique des mathématiques déterministes. Une solution pure et élégante.
Mes excuses si cela était déjà couvert.
Chaque compilateur que j'utilise, à l'exception de XCODE AFAIK, a des caractéristiques intrinsèques de compilateur pour le scan de bits avant et le scan de bits inverse. Celles-ci seront compilées en une seule instruction d'assemblage sur la plupart des matériels sans cache manquant, sans prédiction de branchement et aucun autre programmeur n'a généré de blocages.
Pour les compilateurs Microsoft, utilisez _BitScanForward et _BitScanReverse.
Pour GCC, utilisez __builtin_ffs, __builtin_clz, __builtin_ctz.
En outre, veuillez vous abstenir de publier une réponse et de tromper les nouveaux arrivants si vous ne connaissez pas suffisamment le sujet discuté.
Désolé j'ai totalement oublié de fournir une solution. C'est le code que j'utilise sur l'IPAD qui n'a pas d'instructions de niveau d'assemblage pour la tâche:
La chose à comprendre ici est que ce n'est pas la comparaison qui coûte cher, mais la branche qui se produit après la comparaison. La comparaison dans ce cas est forcée à une valeur de 0 ou 1 avec le .. == 0, et le résultat est utilisé pour combiner les calculs qui se seraient produits de chaque côté de la branche.
Éditer:
Le code ci-dessus est totalement cassé. Ce code fonctionne et est toujours sans branche (s'il est optimisé):
Cela renvoie -1 s'il est donné 0. Si vous ne vous souciez pas de 0 ou êtes heureux d'obtenir 31 pour 0, supprimez le calcul i0, économisant ainsi un morceau de temps.
la source
-O3
godbolt.org/z/gcsUHdInspiré par cet article similaire qui consiste à rechercher un peu d'ensemble, je propose ce qui suit:
Avantages:
Les inconvénients:
Mettre à jour: comme indiqué dans les commentaires, une union est une implémentation plus propre (au moins pour C) et ressemblerait à:
Cela suppose des entiers 32 bits avec un stockage little-endian pour tout (pensez aux processeurs x86).
la source
int
estint32_t
, et ce décalage à droite signé est un décalage arithmétique (en C ++, il est défini par l'implémentation)Cela peut être fait avec le pire des cas de moins de 32 opérations:
Principe: la vérification de 2 bits ou plus est tout aussi efficace que la vérification de 1 bit.
Ainsi, par exemple, rien ne vous empêche de vérifier dans quel groupe il se trouve en premier, puis de vérifier chaque bit du plus petit au plus grand dans ce groupe.
Donc ...
si vous cochez 2 bits à la fois, vous avez dans le pire des cas (Nbits / 2) + 1 chèque au total.
si vous cochez 3 bits à la fois, vous avez dans le pire des cas (Nbits / 3) + 2 chèques au total.
...
L'optimal serait de vérifier par groupes de 4. Ce qui nécessiterait dans le pire des cas 11 opérations au lieu de 32.
Le meilleur cas va de 1 vérification de vos algorithmes à 2 vérifications si vous utilisez cette idée de regroupement. Mais ce chèque supplémentaire dans le meilleur des cas en vaut la peine pour les pires économies.
Remarque: je l'écris en entier au lieu d'utiliser une boucle car c'est plus efficace de cette façon.
la source
Pourquoi ne pas utiliser la recherche binaire ? Cela se terminera toujours après 5 opérations (en supposant une taille int de 4 octets):
la source
Une autre méthode (division du module et recherche) mérite une mention spéciale ici à partir du même lien fourni par @ anton-tykhyy. cette méthode est très similaire en termes de performances à la méthode de multiplication et de recherche DeBruijn avec une légère mais importante différence.
division du module et recherche
La méthode de division et de recherche du module renvoie des valeurs différentes pour v = 0x00000000 et v = FFFFFFFF tandis que la méthode de multiplication et de recherche DeBruijn renvoie zéro sur les deux entrées.
tester:-
la source
mod
est lent. Au lieu de cela, vous pouvez utiliser la méthode de multiplication et de recherche d'origine et soustraire!v
der
pour gérer les cas extrêmes.Selon la page BitScan de programmation d'échecs et mes propres mesures, soustraire et xor est plus rapide que nier et masquer.
(Notez que si vous comptez les zéros de fin
0
, la méthode telle que je l'ai renvoie63
alors que la négation et le masque retournent0
.)Voici une soustraction et un xor 64 bits:
Pour référence, voici une version 64 bits de la méthode de négation et de masque:
la source
(v ^ (v-1))
fonctionne à conditionv != 0
. Dans le cas oùv == 0
il renvoie 0xFF .... FF tandis que(v & -v)
donne zéro (ce qui est d'ailleurs faux aussi, mais au moins cela conduit à un résultat raisonnable).v ^ (v-1)
, donc il n'y a pas de les distinguer. Dans mon scénario, zéro ne sera jamais entré.Vous pouvez vérifier si l'un des bits d'ordre inférieur est défini. Si tel est le cas, regardez l'ordre inférieur des bits restants. par exemple,:
32 bits int - vérifiez si l'un des 16 premiers est défini. Si tel est le cas, vérifiez si l'un des 8 premiers est défini. si c'est le cas, ....
sinon, vérifiez si l'un des 16 supérieurs est défini.
C'est essentiellement une recherche binaire.
la source
Voir ma réponse ici pour savoir comment le faire avec une seule instruction x86, sauf que pour trouver le bit défini le moins significatif, vous aurez besoin de l'
BSF
instruction ("bit scan forward") au lieu d'êtreBSR
décrite ici.la source
Encore une autre solution, pas la plus rapide possible, mais qui semble assez bonne.
Au moins, il n'a pas de succursales. ;)
la source
1
s du 1 le moins significatif à LSB, utilisez à la((x & -x) - 1) << 1
placex ^ (x-1)
50% de tous les nombres reviendront sur la première ligne de code.
75% de tous les nombres reviendront sur les 2 premières lignes de code.
87% de tous les nombres reviendront dans les 3 premières lignes de code.
94% de tous les nombres reviendront dans les 4 premières lignes de code.
97% de tous les nombres reviendront dans les 5 premières lignes de code.
etc.
Je pense que les gens qui se plaignent de l'inefficacité du pire des cas pour ce code ne comprennent pas à quel point cette condition se produira.
la source
J'ai trouvé cette astuce en utilisant des 'masques magiques' dans "L'art de la programmation, partie 4", qui le fait en temps O (log (n)) pour un nombre de n bits. [avec log (n) espace supplémentaire]. Les solutions typiques vérifiant le bit défini sont soit O (n), soit nécessitent un espace supplémentaire O (n) pour une table de consultation, c'est donc un bon compromis.
Masques magiques:
Idée clé: Nombre de zéros de fin dans x = 1 * [(x & m0) = 0] + 2 * [(x & m1) = 0] + 4 * [(x & m2) = 0] + ...
la source
Si C ++ 11 est disponible pour vous, un compilateur peut parfois faire la tâche pour vous :)
Le résultat est un index basé sur 1.
la source
ffs()
au moment de la compilation, vous n'avez donc pas besoin de l'utiliser pour que la propagation constante fonctionne. (Vous ne devez éviter inline-asm, bien sûr.) Si vous avez vraiment besoin de quelque chose qui fonctionne comme un C ++ 11constexpr
, vous pouvez toujours utiliser GNU C__builtin_ffs
.Ceci concerne la réponse de @Anton Tykhyy
Voici mon implémentation C ++ 11 constexpr supprimant les casts et supprimant un avertissement sur VC ++ 17 en tronquant un résultat 64 bits à 32 bits:
Pour contourner le problème de 0x1 et 0x0 renvoyant tous les deux 0, vous pouvez faire:
mais si le compilateur ne peut pas ou ne prétraite pas l'appel, il ajoutera quelques cycles au calcul.
Enfin, si vous êtes intéressé, voici une liste d'assertions statiques pour vérifier que le code fait ce qui est prévu:
la source
Voici une alternative simple, même si la recherche de journaux est un peu coûteuse.
la source
Récemment, je vois que le premier ministre de Singapour a publié un programme qu'il a écrit sur Facebook, il y a une ligne pour le mentionner.
La logique est simplement "valeur et valeur", supposons que vous ayez 0x0FF0, puis 0FF0 & (F00F + 1), ce qui équivaut à 0x0010, cela signifie que le 1 le plus bas est dans le 4ème bit .. :)
la source
Si vous avez les ressources, vous pouvez sacrifier de la mémoire pour améliorer la vitesse:
Remarque: cette table consommerait au moins 4 Go (16 Go si nous laissons le type de retour comme
unsigned
). Ceci est un exemple d'échange d'une ressource limitée (RAM) contre une autre (vitesse d'exécution).Si votre fonction doit rester portable et fonctionner aussi vite que possible à tout prix, ce serait la voie à suivre. Dans la plupart des applications du monde réel, une table de 4 Go est irréaliste.
la source
:)
@Dan: Vous avez raison à propos de la mise en cache de la mémoire. Voir le commentaire de Mikeage ci-dessus.