2010-02-12 6 views
14

est-il un moyen simple d'extraire l'exposant d'une puissance de 2 en utilisant uniquement des opérations au niveau du bit?Trouver l'exposant n = 2 ** x en utilisant des opérations au niveau du bit [logarithme en base 2 de n]

EDIT: Bien que la question était à l'origine sur les opérations au niveau du bit, le fil est une bonne lecture même si vous vous demandez « ? Quel est le meilleur moyen de trouver X donnée Y = 2 X en Python » * *

Je suis actuellement en train d'optimiser une routine (Rabin-Miller primality test) qui réduit un nombre pair N dans les formes 2**s * d. Je peux obtenir le 2**s partie par:

two_power_s = N & -N 

mais je ne peux pas trouver un moyen d'une opération pour extraire bitwise juste « s ». Contournements Je teste actuellement sans trop de satisfaction (ils sont tous à peu près lent) sont:

  • en utilisant la fonction logarithme
  • manipulation de la représentation binaire de 2 ** s (c.-à compter les zéros)
  • en boucle sur une division par 2 jusqu'à ce que le résultat est 1

J'utilise python, mais la réponse à cette question devrait être la langue agnostique, je suppose.

+2

Je voulais juste ajouter deux références. Le premier, Fast Bytecodes for Funny Languages ​​de Cliff Cock, http://www.infoq.com/presentations/click-fast-bytecodes-funny-languages ​​du JVM Languages ​​Summit 2008, qui aborde les optimisations utilisées et manquées par les langues «moins courantes» . Le second est état des astuces d'optimisation du compilateur C art http://lambda-the-ultimate.org/node/3674 par Felix von Leitner, présenté à Linux Kongress 2009, il couvre la façon intelligente votre compilateur (environnement) est ces jours-ci, et pourquoi vous devriez être très prudent en supposant que vous devez utiliser des astuces de style Michael Abrash. – mctylr

+0

@mctylr - Merci pour les références (+1) – mac

Répondre

5

Réponse courte

En ce qui concerne python:

  • La méthode la plus rapide de tous pour trouver l'exposant de 2 x ** est en regardant dans un dictionnaire dont hash sont les puissances de 2 (voir "hashlookup" dans le code)
  • la méthode la plus rapide de est au niveau du bit celui qui est appelé "unrolled_bitwise ".
  • Les deux méthodes précédentes ont des limites supérieures bien définies (mais extensibles). La méthode la plus rapide sans les limites supérieures codées en dur (qui évolue aussi loin que python peut gérer les nombres) est "log_e".

Notes préliminaires

  1. Toutes les mesures de vitesse ci-dessous ont été obtenus par timeit.Timer.repeat(testn, cycles)testn a été fixé à 3 et cycles a été ajusté automatiquement par le script pour obtenir des temps de l'ordre de quelques secondes (note : il y avait un bug dans ce mécanisme de réglage automatique qui a été corrigé le 18/02/2010).
  2. Toutes les méthodes peuvent échelle, c'est pourquoi je n'ai pas testé toutes les fonctions pour les différentes puissances de 2
  3. Je n'ai pas réussi à obtenir quelques-unes des méthodes proposées pour travailler (la fonction retourne une mauvaise résultat).Je n'ai pas encore tiem faire un débogage étape par étape session: J'ai inclus le code (commenté) juste au cas où quelqu'un SPOTS l'erreur par l'inspection (ou si vous voulez effectuer le débogage eux-mêmes)

Résultats

func (2 5) **

hashlookup:   0.13s  100% 
lookup:    0.15s  109% 
stringcount:   0.29s  220% 
unrolled_bitwise: 0.36s  272% 
log_e:    0.60s  450% 
bitcounter:   0.64s  479% 
log_2:    0.69s  515% 
ilog:    0.81s  609% 
bitwise:    1.10s  821% 
olgn:    1.42s 1065% 

func (2 31) **

hashlookup:   0.11s  100% 
unrolled_bitwise: 0.26s  229% 
log_e:    0.30s  268% 
stringcount:   0.30s  270% 
log_2:    0.34s  301% 
ilog:    0.41s  363% 
bitwise:    0.87s  778% 
olgn:    1.02s  912% 
bitcounter:   1.42s 1264% 

func (2 128) **

hashlookup:  0.01s  100% 
stringcount: 0.03s  264% 
log_e:   0.04s  315% 
log_2:   0.04s  383% 
olgn:   0.18s 1585% 
bitcounter:  1.41s 12393% 

func (2 1024) **

log_e:   0.00s  100% 
log_2:   0.01s  118% 
stringcount: 0.02s  354% 
olgn:   0.03s  707% 
bitcounter:  1.73s 37695% 

code

import math, sys 

def stringcount(v): 
    """mac"""  
    return len(bin(v)) - 3 

def log_2(v): 
    """mac"""  
    return int(round(math.log(v, 2), 0)) # 2**101 generates 100.999999999 

def log_e(v): 
    """bp on mac"""  
    return int(round(math.log(v)/0.69314718055994529, 0)) # 0.69 == log(2) 

def bitcounter(v): 
    """John Y on mac""" 
    r = 0 
    while v > 1 : 
     v >>= 1 
     r += 1 
    return r 

def olgn(n) : 
    """outis""" 
    if n < 1: 
     return -1 
    low = 0 
    high = sys.getsizeof(n)*8 # not the best upper-bound guesstimate, but... 
    while True: 
     mid = (low+high)//2 
     i = n >> mid 
     if i == 1: 
      return mid 
     if i == 0: 
      high = mid-1 
     else: 
      low = mid+1 

def hashlookup(v): 
    """mac on brone -- limit: v < 2**131""" 
# def prepareTable(max_log2=130) : 
#  hash_table = {} 
#  for p in range(1, max_log2) : 
#   hash_table[2**p] = p 
#  return hash_table 

    global hash_table 
    return hash_table[v] 

def lookup(v): 
    """brone -- limit: v < 2**11""" 
# def prepareTable(max_log2=10) : 
#  log2s_table=[0]*((1<<max_log2)+1) 
#  for i in range(max_log2+1): 
#   log2s_table[1<<i]=i 
#  return tuple(log2s_table) 

    global log2s_table 
    return log2s_table[v] 

def bitwise(v): 
    """Mark Byers -- limit: v < 2**32""" 
    b = (0x2, 0xC, 0xF0, 0xFF00, 0xFFFF0000) 
    S = (1, 2, 4, 8, 16) 
    r = 0 
    for i in range(4, -1, -1) : 
     if (v & b[i]) : 
      v >>= S[i]; 
      r |= S[i]; 
    return r 

def unrolled_bitwise(v): 
    """x4u on Mark Byers -- limit: v < 2**33""" 
    r = 0; 
    if v > 0xffff : 
     v >>= 16 
     r = 16; 
    if v > 0x00ff : 
     v >>= 8 
     r += 8; 
    if v > 0x000f : 
     v >>= 4 
     r += 4; 
    if v > 0x0003 : 
     v >>= 2 
     r += 2; 
    return r + (v >> 1) 

def ilog(v): 
    """Gregory Maxwell - (Original code: B. Terriberry) -- limit: v < 2**32""" 
    ret = 1 
    m = (not not v & 0xFFFF0000) << 4; 
    v >>= m; 
    ret |= m; 
    m = (not not v & 0xFF00) << 3; 
    v >>= m; 
    ret |= m; 
    m = (not not v & 0xF0) << 2; 
    v >>= m; 
    ret |= m; 
    m = (not not v & 0xC) << 1; 
    v >>= m; 
    ret |= m; 
    ret += (not not v & 0x2); 
    return ret - 1; 


# following table is equal to "return hashlookup.prepareTable()" 
hash_table = {...} # numbers have been cut out to avoid cluttering the post 

# following table is equal to "return lookup.prepareTable()" - cached for speed 
log2s_table = (...) # numbers have been cut out to avoid cluttering the post 
+0

Chaque exécution successive au premier de 'logm' peut être accélérée de 25% ¹ en calculant' log2 = log (2) 'en dehors de la fonction, puis en remplaçant' return int (log (v, 2)) 'par' return int (log (v)/log2) '. --- ¹Durée par l'application triviale de 'timeit.timeit()' – badp

+0

@bp - Nice! J'ai changé le code, répété les tests et édité ma réponse en conséquence. – mac

+0

@mac: Sur ma machine, la fonction de compteur n'est pas aussi rapide que logm. De plus, votre fonction de compteur peut être légèrement améliorée en démarrant r à 0 et en bouclant lorsque v> 1 et en revenant simplement quand la boucle est terminée (cela supprime le si à l'intérieur de la boucle). –

4

Il y a une page avec beaucoup de ces types de trucs et hacks. C'est écrit pour C, mais beaucoup d'entre eux devraient aussi fonctionner en Python (bien que la performance soit évidemment différente). Le bit que vous voulez est here et au-delà.

Vous pouvez essayer this par exemple:

register unsigned int r = 0; // result of log2(v) will go here 
for (i = 4; i >= 0; i--) // unroll for speed... 
{ 
    if (v & b[i]) 
    { 
    v >>= S[i]; 
    r |= S[i]; 
    } 
} 

Cela ressemble à cela pourrait être converti en Python assez facilement.

+0

+1 pour le lien! –

+0

Joli lien, merci. Voir ma propre réponse à la question (que je vais poster dans une minute) pour voir comment cela a effectué IRL avec python ... – mac

+0

Le compilateur va bien sûr dérouler ça pour vous, sans oublier le mot-clé 'register' qui est à peu près inutile . – GManNickG

3

Vous pouvez le faire en O (s lg) temps pour les entiers de longueur arbitraire en utilisant un binsearch.

import sys 
def floorlg(n): 
    if n < 1: 
     return -1 
    low=0 
    high=sys.getsizeof(n)*8 # not the best upper-bound guesstimate, but... 
    while True: 
     mid = (low+high)//2 
     i = n >> mid 
     if i == 1: 
      return mid 
     if i == 0: 
      high = mid-1 
     else: 
      low = mid+1 

Pour les entiers de taille fixe, une table de recherche devrait être la solution la plus rapide et probablement la meilleure dans l'ensemble.

+0

Juste une note, aussi près que je peux dire cette fonction se bloque sur les nombres plus grands que 2^11. –

+0

D'oh. La hâte fait des erreurs. Fixé maintenant – outis

+0

N'est-ce pas O (lg lg n)? C'est-à-dire, si n = 2 ** x, alors c'est O (lg x)? –

6

« langue agnostique » et se soucier de la performance sont à peu près les concepts incompatibles.

La plupart des processeurs modernes ont une instruction de CLZ, « compter des zéros à gauche ». Dans GCC vous pouvez obtenir avec __builtin_clz (x) (qui produit également raisonnable, sinon le plus rapide, le code pour les cibles qui manquent clz). Notez que cette CLZ n'est pas définie pour zéro, donc vous aurez besoin d'une branche supplémentaire pour attraper ce cas si cela compte dans votre application.

En CELT (http://celt-codec.org) le CLZ sans branchement que nous utilisons pour les compliants dépourvus de CLZ a été écrit par Timothy B.Terriberry:


int ilog(uint32 _v){ 
    int ret; 
    int m; 
    ret=!!_v; 
    m=!!(_v&0xFFFF0000)<<4; 
    _v>>=m; 
    ret|=m; 
    m=!!(_v&0xFF00)<<3; 
    _v>>=m; 
    ret|=m; 
    m=!!(_v&0xF0)<<2; 
    _v>>=m; 
    ret|=m; 
    m=!!(_v&0xC)<<1; 
    _v>>=m; 
    ret|=m; 
    ret+=!!(_v&0x2); 
    return ret; 
} 

(Les commentaires indiquent que cela a été jugé plus rapide qu'une version de branchement et une version table de consultation)

Mais si la performance est que vous devriez probablement pas critique implantera cette partie de votre code en python.

+0

Merci pour votre message. Ma bibliothèque est juste quelque chose que je développe pour m'amuser (en fait: améliorer mes habiletés de piratage en python) donc je dirais que plus que «la performance critique», c'est «être critique sur la performance». est affecté par divers facteurs dans python). À cet égard, je poste mes conclusions dans un instant. Merci pour votre code de toute façon ... utile! :) – mac

+0

@Gregory Maxwell - J'ai réussi à porter votre code sur python (étonnamment je devais ajouter un '-1' au résultat). En python, il ne semble pas très bien fonctionner. Je me demande comment la performance de 'log_e' se compare à' ilog' en C (voir le code dans ma réponse). Merci pour votre temps! – mac

+0

Intéressant - eh bien, je suppose que c'est un exemple de l'inadéquation de l'impédance entre les environnements de programmation de haut et de bas niveau. Sur mon ordinateur portable core2 1.6ghz prend 38 737 220 μs pour exécuter le ilog() ci-dessus sur 1-2147483647 et accumuler le résultat. 18ns par exécution ... environ 29 horloges. Pas mal (le log2 en virgule flottante plus les lancers est ~ 82ns) mais en utilisant le __builtin_clz() prend seulement 3484684 μs, ou 1.62ns par exécution ... 2.6 horloges, ce qui est cohérent avec l'assemblage produit et le parallélisme interne du cpu (trois boucle instructon en utilisant bsrl et deux ajoute). –

1

Il semble que la plage est connue . Supposons que cela va jusqu'à 1 < < 20, juste pour le rendre plus intéressant:

max_log2=20 

donc faire une liste (en vigueur) mappe un nombre entier à son logarithme en base 2. Ce qui suit fera l'affaire:

log2s_table=[0]*((1<<max_log2)+1) 
for i in range(max_log2+1): 
    log2s_table[1<<i]=i 

(Cela ne fait pas quelque chose d'utile pour les numéros qui ne sont pas des puissances de deux, l'énoncé du problème suggère qu'ils ne doivent pas être manipulés serait assez facile. résoudre ce problème si)

la fonction pour obtenir le logarithme est très simple, et pourrait facilement être inline.

def table(v): 
    return log2s_table[v] 

Je ne peux pas garantir que le code de test je l'ai écrit est exactement le même que celui étant utilisé pour obtenir les timings d'exemple, mais c'est plutôt plus rapide que leCode:

stringcount: 0.43 s. 
table: 0.16 s. 

Puisque toutes les valeurs du tableau sont inférieurs à 256, je me demandais si l'aide d'une chaîne au lieu d'une liste serait plus rapide, ou peut-être un array.array d'octets, mais pas de dés:

string: 0.25 s. 
arr: 0.21 s. 

l'utilisation d'un dict pour faire la recherche est une autre possibilité, en profitant de la manière que des puissances de deux sont en cours de vérification:

log2s_map=dict([(1<<x,x) for x in range(max_log2+1)]) 

def map(v): 
    return log2s_map[v] 

Les résultats de ce ne sont pas aussi bien, si:

map: 0.20 s. 

Et juste pour le plaisir on pourrait également utiliser la méthode hex sur des objets flottants pour obtenir une chaîne qui comprend (comme sa dernière partie) la base 2 exposant le nombre.Ceci est un peu lent à extraire en général, mais si l'exposant n'est jamais va être un chiffre, il pourrait se faire assez sans détour:

def floathex(v): 
    return ord(float(v).hex()[-1])-48 

Ceci est purement pour la valeur de divertissement mais comme il était uncompetetive - si , étonnamment, toujours plus rapide que l'approche bitwise.

il ressemble à l'aide d'une liste est le chemin à parcourir. (Cette approche ne sera pas mise à l'échelle indéfiniment, en raison de la mémoire limitée, mais pour compenser que la vitesse d'exécution ne dépendra pas de max_log2, ou des valeurs d'entrée, de quelque façon que vous remarquerez lors de l'exécution de python code. en ce qui concerne la consommation de mémoire, si je me souviens bien mes internals python, la table prendra environ (1<<max_log2)*4 octets, car le contenu sont tous petits entiers que l'interprète stagiaire automatiquement. Donc, quand max_log2 est 20, qui est à peu près 4Mo.)

+0

@brone - Merci pour cela. J'ai testé votre fonction mais elle ne semble pas très performante, du moins sur ma machine (voir timings dans ma réponse réécrite). Ai-je mal compris quelque chose? – mac

+0

Votre fonction de table fait BEAUCOUP plus de travail que celle présentée ici - il y a une définition de fonction là-dedans! (Idem pour le hashlookup, utilisez dis.dis pour voir la différence entre l'avoir dedans et le sortir.) La préparation de la table est délibérément globale pour que la fonction ne fasse rien sauf pour la recherche de table. En y ajoutant la définition de la fonction, le temps d'exécution a augmenté, bien qu'il soit encore plus rapide que (disons) len (bin (v)) ou math.log (v, 2) (c'est ce à quoi je m'attendrais). Peut-être que si vous postez le code de test, cette énigme pourrait être résolue. –

+0

@brone - Vous aviez raison dans tout ce que vous avez écrit. 1) La définition de la fonction [maintenant commentée] avait un ~ 25% sur chacune des deux fonctions 2) L'approche de recherche - au moins quand les limites supérieures sont connues et le temps pour générer la table de consultation n'est pas considéré ou est étalé au cours de nombreux cycles - est le plus performant 3) Il y avait une erreur dans mon utilitaire de synchronisation que j'ai présenté avec les modifications effectuées sur le 16/2. Devrait être réparé maintenant. Voici le "noyau" de l'utilitaire de synchronisation, si vous souhaitez le vérifier: http://pastebin.com/f7851ef6d Merci beaucoup pour votre temps et votre contribution! – mac

1

Ceci est en fait un commentaire au test de performance affiché par mac. Je publie ceci comme une réponse pour mettre en forme et mettre en retrait le code correct

mac, pourriez-vous essayer une implémentation déroulée du nombre de bits proposée par Mark Byers? Peut-être que c'est juste l'accès au tableau qui le ralentit. En théorie, cette approche devrait être plus rapide que les autres.

Il ressemblerait à quelque chose comme ça, même si je ne suis pas sûr que la mise en forme est bon pour python mais je suppose que vous pouvez voir ce qu'il est censé faire.

def bitwise(v): 
    r = 0; 
    if(v > 0xffff) : v >>= 16; r = 16; 
    if(v > 0x00ff) : v >>= 8; r += 8; 
    if(v > 0x000f) : v >>= 4; r += 4; 
    if(v > 0x0003) : v >>= 2; r += 2; 
    return r + (v >> 1); 

Si le manque d'actions python java d'entiers unsingned il devrait être quelque chose comme ceci:

def bitwise(v): 
    r = 0; 
    if(v & 0xffff0000) : v >>>= 16; r = 16; 
    if(v > 0x00ff) : v >>= 8; r += 8; 
    if(v > 0x000f) : v >>= 4; r += 4; 
    if(v > 0x0003) : v >>= 2; r += 2; 
    return r + (v >> 1); 
+0

@ x4u - Il y avait une faute de frappe dans le code (que j'ai pris la liberté de corriger - merci à John Y pour le signaler). Les résultats de temps maintenant dans la réponse choisie. BTW: votre méthode s'est avérée être la deuxième plus efficace! :) – mac

+0

Merci d'avoir corrigé la faute de frappe et aussi John Y pour l'avoir repéré. C'est intressant mais pas vraiment une surprise que la recherche de hash s'est avérée la plus rapide. L'avantage de l'approche au niveau du bit est qu'elle effectue un vrai log2 pour tous les nombres dans la gamme supportée (pas seulement pour 2 ** x) alors que la recherche de hachage nécessiterait une quantité excessive de mémoire pour le faire. – x4u

Questions connexes