Qual è il modo più veloce / più efficiente per trovare il bit impostato più alto (msb) in un numero intero in C?


119

Se ho un numero intero n e voglio conoscere la posizione del bit più significativo (cioè, se il bit meno significativo è a destra, voglio conoscere la posizione del bit più a sinistra che è un 1), qual è il metodo più rapido / più efficiente per scoprirlo?

So che POSIX supporta un ffs()metodo in strings.h per trovare il primo bit impostato, ma non sembra esserci un fls()metodo corrispondente .

C'è un modo davvero ovvio per farlo che mi manca?

E nei casi in cui non è possibile utilizzare le funzioni POSIX per la portabilità?

Modifica: che ne dici di una soluzione che funziona su architetture a 32 e 64 bit (molti degli elenchi di codice sembrano funzionare solo su int a 32 bit).


ci sono alcune implementazioni qui: graphics.stanford.edu/~seander/bithacks.html#ZerosOnRightLinear (Modifica: dopo aver riletto la tua domanda, mi rendo conto che il collegamento sopra è per trovare il bit impostato più a destra, non più a sinistra come richiesto, anche se senza un senso della dimensione della parola, è difficile rispondere)
spender


Quello conta gli zeri a destra ; la domanda riguardava gli zeri a sinistra. Almeno, in una rapida occhiata non lo vedo lì.
Darius Bacon

2
vuoi specificamente il numero di bit 'n' o sarebbe sufficiente 2 ^ n?
Alnitak

1
Guarda gli algoritmi "Log Base 2" - come dice Anderson nell'articolo: "La base logaritmica 2 di un intero è la stessa della posizione del set di bit più alto (o set di bit più significativo, MSB)"
Michael Burr

Risposte:


64

GCC ha :

 - Funzione incorporata: int __builtin_clz (unsigned int x)
     Restituisce il numero di bit 0 iniziali in X, a partire dal massimo
     posizione bit significativa. Se X è 0, il risultato è indefinito.

 - Funzione incorporata: int __builtin_clzl (unsigned long)
     Simile a `__builtin_clz ', eccetto che il tipo di argomento è` unsigned
     lungo'.

 - Funzione incorporata: int __builtin_clzll (unsigned long long)
     Simile a `__builtin_clz ', eccetto che il tipo di argomento è` unsigned
     lungo lungo'.

Mi aspetto che vengano tradotti in qualcosa di ragionevolmente efficiente per la tua piattaforma attuale, sia che si tratti di uno di quegli algoritmi stravaganti o di una singola istruzione.


Un trucco utile se il tuo input può essere zero è __builtin_clz(x | 1): impostare incondizionatamente il bit basso senza modificare nessun altro rende l'output 31per x=0, senza modificare l'output per qualsiasi altro input.

Per evitare di doverlo fare, l'altra opzione sono gli intrinseci specifici della piattaforma come ARM GCC __clz(senza intestazione necessaria) o x86 _lzcnt_u32su CPU che supportano l' lzcntistruzione. (Attenzione che lzcntdecodifica come bsrsulle CPU più vecchie invece di dare errori, il che dà 31-lzcnt per ingressi diversi da zero.)

Sfortunatamente non c'è modo di sfruttare in modo portabile le varie istruzioni CLZ su piattaforme non x86 che definiscono il risultato per input = 0 come 32 o 64 (a seconda della larghezza dell'operando). Anche x86 lzcntfa questo, mentre bsrproduce un bit-index che il compilatore deve capovolgere a meno che tu non usi 31-__builtin_clz(x).

(Il "risultato indefinito" non è C Undefined Behavior, solo un valore che non è definito. In realtà è qualunque cosa fosse nel registro di destinazione quando l'istruzione è stata eseguita. AMD lo documenta, Intel no, ma le CPU Intel implementano quel comportamento . Ma non è quello che era in precedenza nella variabile C a cui stai assegnando, di solito non è così che funzionano le cose quando gcc trasforma C in asm. Vedi anche Perché è importante interrompere la "dipendenza dell'output" di LZCNT? )



1
Il comportamento indefinito su zero consente loro di compilare una singola istruzione BSR su x86, anche quando LZCNT non è disponibile. Questo è un grande vantaggio per __builtin_ctzover ffs, che compila un BSF e un CMOV per gestire il caso di input era zero. Su architetture senza un'implementazione abbastanza breve (ad esempio il vecchio ARM senza l' clzistruzione), gcc emette una chiamata a una funzione di supporto libgcc.
Peter Cordes

41

Supponendo che tu sia su x86 e giochi per un po 'di assemblatore in linea, Intel fornisce BSRun'istruzione ("bit scan reverse"). È veloce su alcuni x86 (microcodificato su altri). Dal manuale:

Cerca nell'operando sorgente il bit impostato più significativo (1 bit). Se viene trovato un 1 bit più significativo, il suo indice di bit viene memorizzato nell'operando di destinazione. L'operando sorgente può essere un registro o una posizione di memoria; l'operando di destinazione è un registro. L'indice di bit è un offset senza segno dal bit 0 dell'operando di origine. Se l'operando di origine del contenuto è 0, il contenuto dell'operando di destinazione non è definito.

(Se sei su PowerPC c'è cntlzun'istruzione simile ("count leading zeros").)

Codice di esempio per gcc:

#include <iostream>

int main (int,char**)
{
  int n=1;
  for (;;++n) {
    int msb;
    asm("bsrl %1,%0" : "=r"(msb) : "r"(n));
    std::cout << n << " : " << msb << std::endl;
  }
  return 0;
}

Vedi anche questo tutorial sull'assemblatore inline , che mostra (sezione 9.4) che è notevolmente più veloce del codice in loop.


4
In realtà questa istruzione è solitamente microcodificata in un ciclo ed è piuttosto lenta.
rlbond

2
Quale ? BSR o CNTLZ? Mentre leggo il x86-timing.pdf a cui si fa riferimento sopra, BSR è lento solo sui Netburst Pentiums. Non so nulla di PowerPC però.
sabato

5
... OK, a un'analisi più attenta fai che "BSR è veloce solo su P3 / Pentium-M / Core2 x86". Lento su Netburst e AMD.
sabato

1
Solo un avvertimento: i tuoi ultimi due collegamenti sono morti.
Baum mit Augen

2
@rlbond: eh, BSR su P4 Prescott è di 2 uops con 16 cicli di latenza (!), con uno per 4c di throughput. Ma su Netburst precedente, è solo una latenza di 4 cicli (ancora 2 uops) e una per 2c di throughput. (fonte: agner.org/optimize ). Sulla maggior parte delle CPU, ha anche una dipendenza dal suo output che gcc non tiene conto (quando l'input è zero, il comportamento effettivo è di lasciare la destinazione invariata). Questo può portare a problemi come stackoverflow.com/questions/25078285/… . IDK perché gcc ha mancato BSR quando lo ha risolto.
Peter Cordes

38

Poiché 2 ^ N è un numero intero con solo l'N-esimo bit impostato (1 << N), trovare la posizione (N) del bit impostato più alto è la base 2 logaritmica dell'intero di quell'intero.

http://graphics.stanford.edu/~seander/bithacks.html#IntegerLogObvious

unsigned int v;
unsigned r = 0;

while (v >>= 1) {
    r++;
}

Questo algoritmo "ovvio" potrebbe non essere trasparente a tutti, ma quando ti accorgi che il codice si sposta ripetutamente a destra di un bit finché il bit più a sinistra non è stato spostato (nota che C tratta qualsiasi valore diverso da zero come vero) e restituisce il numero di turni, ha perfettamente senso. Significa anche che funziona anche quando è impostato più di un bit: il risultato è sempre per il bit più significativo.

Se scorri verso il basso su quella pagina, ci sono variazioni più veloci e complesse. Tuttavia, se sai di avere a che fare con numeri con molti zeri iniziali, l'approccio ingenuo può fornire una velocità accettabile, poiché lo spostamento di bit è piuttosto veloce in C e l'algoritmo semplice non richiede l'indicizzazione di un array.

NOTA: quando si utilizzano valori a 64 bit, essere estremamente cauti nell'utilizzo di algoritmi estremamente intelligenti; molti di loro funzionano correttamente solo per valori a 32 bit.


2
@Johan L'esecuzione di un debugger può aiutare a spiegare perché il ciclo termina. Fondamentalmente, è perché l'espressione nella condizione restituisce 0 (che viene considerato falso) una volta che l'ultimo bit 1 è stato spostato da destra.
Quinn Taylor,

2
Bella idea di utilizzare il risultato finale in questo modo :)
Johan

6
nota: deve essere senza segno, per interi con segno lo spostamento a destra fallisce per numeri negativi.
Xantix

2
Xantix: lo spostamento in C / C ++ è un cambiamento logico, quindi funziona bene. Per Java, JavaScript o D, è necessario utilizzare l'operatore di spostamento logico >>>. Più probabilmente il comparatore != 0e un certo numero di parentesi non specificato.
Inseguimento

8
@Chase: No, non lo è. È un cambiamento logico per non firmato . Per firmato , può o non può essere uno spostamento logico (e di solito è aritmetico, in effetti).
Tim Čas

17

Questo dovrebbe essere velocissimo:

int msb(unsigned int v) {
  static const int pos[32] = {0, 1, 28, 2, 29, 14, 24, 3,
    30, 22, 20, 15, 25, 17, 4, 8, 31, 27, 13, 23, 21, 19,
    16, 7, 26, 12, 18, 6, 11, 5, 10, 9};
  v |= v >> 1;
  v |= v >> 2;
  v |= v >> 4;
  v |= v >> 8;
  v |= v >> 16;
  v = (v >> 1) + 1;
  return pos[(v * 0x077CB531UL) >> 27];
}

25
Spostamenti di 7 bit, 5 o istruzioni, un multiplty e un potenziale errore di cache. :) L'hai confrontato o hai guardato l'assembler generato? Si potrebbe finire piuttosto lento, a seconda di come gran parte di essa il compilatore può eliminare.
jalf

5
Sono nuovo qui. Non ottengo voti negativi ragazzi. Ho fornito l'unica risposta con il codice sorgente che funziona davvero.
Protagonista

9
Il "possibile errore nella cache" è probabilmente dovuto a questo codice che richiede l'accesso alla sua tabella di ricerca. Se quella tabella non viene memorizzata nella cache quando viene chiamata, ci sarà uno stallo durante il recupero. Ciò potrebbe rendere le prestazioni nel caso peggiore di gran lunga peggiori rispetto alle soluzioni che non utilizzano una LUT.
rilassarsi il

13
non proprio il punto. Utilizza molta più cache di dati del necessario (anche più di una riga di cache) e più cache di istruzioni del necessario. È probabile che si verifichino errori nella cache che avrebbero potuto essere evitati la prima volta che si chiama la funzione e inquineranno la cache più del necessario, quindi dopo la chiamata, altro codice potrebbe riscontrare più errori del necessario. Le LUT spesso non valgono la pena perché i mancati riscontri nella cache sono costosi. Ma ho solo detto che era qualcosa che avrei voluto confrontare prima di affermare che era "velocissimo". Non che sia sicuramente un problema.
jalf

6
La tabella ha 32 voci e ogni valore è <255 (127), quindi definisci la tabella come di tipo unsigned char e si adatterà a una singola riga di cache L1 a 32 byte. E il tutto rientra in due righe della cache.
ChuckCottrill

16

È un po 'come trovare una specie di registro di numeri interi. Ci sono piccoli trucchi, ma ho creato il mio strumento per questo. L'obiettivo ovviamente è la velocità.

La mia realizzazione è che la CPU ha già un rilevatore di bit automatico, utilizzato per la conversione da intero a float! Quindi usa quello.

double ff=(double)(v|1);
return ((*(1+(uint32_t *)&ff))>>20)-1023;  // assumes x86 endianness

Questa versione converte il valore in un doppio, quindi legge l'esponente, che ti dice dove si trovava il bit. Lo spostamento e la sottrazione di fantasia consiste nell'estrarre le parti corrette dal valore IEEE.

È leggermente più veloce usare i float, ma un float può darti solo le prime posizioni a 24 bit a causa della sua minore precisione.


Per fare ciò in modo sicuro, senza un comportamento indefinito in C ++ o C, usa memcpyinvece di casting del puntatore per il tipo di gioco di parole. I compilatori sanno come incorporarlo in modo efficiente.

// static_assert(sizeof(double) == 2 * sizeof(uint32_t), "double isn't 8-byte IEEE binary64");
// and also static_assert something about FLT_ENDIAN?

double ff=(double)(v|1);

uint32_t tmp;
memcpy(&tmp, ((const char*)&ff)+sizeof(uint32_t), sizeof(uint32_t));
return (tmp>>20)-1023;

Oppure in C99 e versioni successive, usa un file union {double d; uint32_t u[2];};. Ma si noti che in C ++, il punning di tipo union è supportato solo su alcuni compilatori come estensione, non in ISO C ++.


Questo di solito sarà più lento di un intrinseco specifico della piattaforma per un'istruzione di conteggio degli zeri iniziali, ma l'ISO C portatile non ha tale funzione. Alcune CPU mancano anche di un'istruzione di conteggio zero iniziale, ma alcune di queste possono convertire in modo efficiente gli interi double. La battitura a macchina di uno schema di bit FP per riportarla a un numero intero può essere lenta, tuttavia (ad esempio su PowerPC richiede una memorizzazione / ricarica e di solito causa uno stallo caricamento-hit-store).

Questo algoritmo potrebbe essere utile per le implementazioni SIMD, perché meno CPU hanno SIMD lzcnt. x86 ha ricevuto questa istruzione solo con AVX512CD


2
Sì. E gcc farà cose brutte con codice come questo con -O2 a causa delle ottimizzazioni di alias di tipo.
MSN

4
il casting tra intero e virgola mobile può essere sorprendentemente costoso su CPU x86
jalf

1
Sì, i costi della FPU sono alti. Ma le misurazioni del tempo effettivo hanno mostrato che questo era più veloce delle operazioni all-bit o soprattutto di qualsiasi loop. Provalo e prendi il più veloce è sempre il miglior consiglio. Non ho avuto problemi con GCC e -O2 con questo però.
SPWorley

1
Non è questo comportamento indefinito (leggere un valore tramite un puntatore di un tipo incompatibile)?
dreamlax

3
Hacker's Delight spiega come correggere l'errore nei float a 32 bit in 5-3 Counting Leading 0's. Ecco il loro codice, che utilizza un'unione anonima per sovrapporre asFloat e asInt: k = k & ~ (k >> 1); asFloat = (float) k + 0,5f; n = 158 - (asInt >> 23); (e sì, questo si basa sul comportamento definito dall'implementazione)
D Coetzee

11

Kaz Kylheku qui

Ho confrontato due approcci per questo su numeri di 63 bit (il tipo lungo lungo su gcc x86_64), rimanendo lontano dal bit di segno.

(Mi capita di aver bisogno di questo "trova il bit più alto" per qualcosa, vedi.)

Ho implementato la ricerca binaria basata sui dati (strettamente basata su una delle risposte precedenti). Ho anche implementato manualmente un albero decisionale completamente srotolato, che è solo codice con operandi immediati. Nessun loop, nessuna tabella.

L'albero decisionale (più alto_bit_unrolled) valutato per essere il 69% più veloce, ad eccezione del caso n = 0 per il quale la ricerca binaria ha un test esplicito.

Il test speciale della ricerca binaria per il caso 0 è solo il 48% più veloce dell'albero decisionale, che non ha un test speciale.

Compilatore, macchina: (GCC 4.5.2, -O3, x86-64, 2867 Mhz Intel Core i5).

int highest_bit_unrolled(long long n)
{
  if (n & 0x7FFFFFFF00000000) {
    if (n & 0x7FFF000000000000) {
      if (n & 0x7F00000000000000) {
        if (n & 0x7000000000000000) {
          if (n & 0x4000000000000000)
            return 63;
          else
            return (n & 0x2000000000000000) ? 62 : 61;
        } else {
          if (n & 0x0C00000000000000)
            return (n & 0x0800000000000000) ? 60 : 59;
          else
            return (n & 0x0200000000000000) ? 58 : 57;
        }
      } else {
        if (n & 0x00F0000000000000) {
          if (n & 0x00C0000000000000)
            return (n & 0x0080000000000000) ? 56 : 55;
          else
            return (n & 0x0020000000000000) ? 54 : 53;
        } else {
          if (n & 0x000C000000000000)
            return (n & 0x0008000000000000) ? 52 : 51;
          else
            return (n & 0x0002000000000000) ? 50 : 49;
        }
      }
    } else {
      if (n & 0x0000FF0000000000) {
        if (n & 0x0000F00000000000) {
          if (n & 0x0000C00000000000)
            return (n & 0x0000800000000000) ? 48 : 47;
          else
            return (n & 0x0000200000000000) ? 46 : 45;
        } else {
          if (n & 0x00000C0000000000)
            return (n & 0x0000080000000000) ? 44 : 43;
          else
            return (n & 0x0000020000000000) ? 42 : 41;
        }
      } else {
        if (n & 0x000000F000000000) {
          if (n & 0x000000C000000000)
            return (n & 0x0000008000000000) ? 40 : 39;
          else
            return (n & 0x0000002000000000) ? 38 : 37;
        } else {
          if (n & 0x0000000C00000000)
            return (n & 0x0000000800000000) ? 36 : 35;
          else
            return (n & 0x0000000200000000) ? 34 : 33;
        }
      }
    }
  } else {
    if (n & 0x00000000FFFF0000) {
      if (n & 0x00000000FF000000) {
        if (n & 0x00000000F0000000) {
          if (n & 0x00000000C0000000)
            return (n & 0x0000000080000000) ? 32 : 31;
          else
            return (n & 0x0000000020000000) ? 30 : 29;
        } else {
          if (n & 0x000000000C000000)
            return (n & 0x0000000008000000) ? 28 : 27;
          else
            return (n & 0x0000000002000000) ? 26 : 25;
        }
      } else {
        if (n & 0x0000000000F00000) {
          if (n & 0x0000000000C00000)
            return (n & 0x0000000000800000) ? 24 : 23;
          else
            return (n & 0x0000000000200000) ? 22 : 21;
        } else {
          if (n & 0x00000000000C0000)
            return (n & 0x0000000000080000) ? 20 : 19;
          else
            return (n & 0x0000000000020000) ? 18 : 17;
        }
      }
    } else {
      if (n & 0x000000000000FF00) {
        if (n & 0x000000000000F000) {
          if (n & 0x000000000000C000)
            return (n & 0x0000000000008000) ? 16 : 15;
          else
            return (n & 0x0000000000002000) ? 14 : 13;
        } else {
          if (n & 0x0000000000000C00)
            return (n & 0x0000000000000800) ? 12 : 11;
          else
            return (n & 0x0000000000000200) ? 10 : 9;
        }
      } else {
        if (n & 0x00000000000000F0) {
          if (n & 0x00000000000000C0)
            return (n & 0x0000000000000080) ? 8 : 7;
          else
            return (n & 0x0000000000000020) ? 6 : 5;
        } else {
          if (n & 0x000000000000000C)
            return (n & 0x0000000000000008) ? 4 : 3;
          else
            return (n & 0x0000000000000002) ? 2 : (n ? 1 : 0);
        }
      }
    }
  }
}

int highest_bit(long long n)
{
  const long long mask[] = {
    0x000000007FFFFFFF,
    0x000000000000FFFF,
    0x00000000000000FF,
    0x000000000000000F,
    0x0000000000000003,
    0x0000000000000001
  };
  int hi = 64;
  int lo = 0;
  int i = 0;

  if (n == 0)
    return 0;

  for (i = 0; i < sizeof mask / sizeof mask[0]; i++) {
    int mi = lo + (hi - lo) / 2;

    if ((n >> mi) != 0)
      lo = mi;
    else if ((n & (mask[i] << lo)) != 0)
      hi = mi;
  }

  return lo + 1;
}

Programma di test rapido e sporco:

#include <stdio.h>
#include <time.h>
#include <stdlib.h>

int highest_bit_unrolled(long long n);
int highest_bit(long long n);

main(int argc, char **argv)
{
  long long n = strtoull(argv[1], NULL, 0);
  int b1, b2;
  long i;
  clock_t start = clock(), mid, end;

  for (i = 0; i < 1000000000; i++)
    b1 = highest_bit_unrolled(n);

  mid = clock();

  for (i = 0; i < 1000000000; i++)
    b2 = highest_bit(n);

  end = clock();

  printf("highest bit of 0x%llx/%lld = %d, %d\n", n, n, b1, b2);

  printf("time1 = %d\n", (int) (mid - start));
  printf("time2 = %d\n", (int) (end - mid));
  return 0;
}

Usando solo -O2, la differenza diventa maggiore. L'albero decisionale è quasi quattro volte più veloce.

Ho anche confrontato il codice ingenuo del cambio di bit:

int highest_bit_shift(long long n)
{
  int i = 0;
  for (; n; n >>= 1, i++)
    ; /* empty */
  return i;
}

Questo è veloce solo per piccoli numeri, come ci si aspetterebbe. Nel determinare che il bit più alto è 1 per n == 1, il benchmark è stato più veloce dell'80%. Tuttavia, metà dei numeri scelti a caso nello spazio a 63 bit ha il 63 ° bit impostato!

Sull'ingresso 0x3FFFFFFFFFFFFFFF, la versione dell'albero decisionale è un po 'più veloce di quanto non sia su 1 e mostra di essere 1120% più veloce (12,2 volte) rispetto al bit shifter.

Confronterò anche l'albero decisionale con i builtin di GCC e proverò anche una combinazione di input piuttosto che ripetere con lo stesso numero. Potrebbe esserci qualche predizione del ramo bloccato in corso e forse alcuni scenari di memorizzazione nella cache non realistici che lo rendono artificialmente più veloce nelle ripetizioni.


9
Non sto dicendo che questo non sia buono, ma il tuo programma di test qui verifica solo lo stesso numero, che dopo 2-3 iterazioni avrà impostato i predittori di ramo alla loro posizione finale e successivamente faranno previsioni di ramo perfette. La cosa buona è che con una distribuzione totalmente casuale la metà dei numeri avrà una previsione quasi perfetta, vale a dire bit63.
Surt


6
unsigned int
msb32(register unsigned int x)
{
        x |= (x >> 1);
        x |= (x >> 2);
        x |= (x >> 4);
        x |= (x >> 8);
        x |= (x >> 16);
        return(x & ~(x >> 1));
}

1 registro, 13 istruzioni. Che tu ci creda o no, di solito è più veloce dell'istruzione BSR menzionata sopra, che opera in tempo lineare. Questo è il tempo logaritmico.

Da http://aggregate.org/MAGIC/#Most%20Significant%201%20Bit


7
Il codice sopra non risponde alla domanda. Restituisce un numero intero senza segno in cui il bit on più significativo in x rimane acceso e tutti gli altri bit sono disattivati. La domanda era di restituire la posizione del bit più significativo.
Protagonista

3
È quindi possibile utilizzare un approccio di sequenza De Bruijn per trovare l'indice del bit impostato. :-)
R .. GitHub SMETTA DI AIUTARE IL GHIACCIO

5
@Protagonist, ha detto in un commento che uno dei due è sufficiente.
rlbond

Questo (dalla stessa pagina) farebbe quello che ti serve, ma richiede una funzione aggiuntiva. aggregate.org/MAGIC/#Log2%20of%20an%20Integer
Quinn Taylor

1
BSR è veloce con le CPU Intel almeno dal Core2. LZCNT è veloce sulle CPU AMD e gcc lo usa __builtin_clzse è abilitato con -march=nativeo qualcosa del genere (poiché è veloce su ogni CPU che lo supporta). Anche su CPU come la famiglia AMD Bulldozer dove BSR è "lento", non è così lento: 7 m-op con 4 cicli di latenza e uno per 4c di throughput. Su Atom, BSR è molto lento: 16 cicli. Su Silvermont, sono 10 uops con 10 cicli di latenza. Questa potrebbe essere una latenza leggermente inferiore rispetto a BSR su Silvermont, ma IDK.
Peter Cordes

6

Ecco alcuni (semplici) benchmark, degli algoritmi attualmente forniti in questa pagina ...

Gli algoritmi non sono stati testati su tutti gli input di unsigned int; quindi controlla prima, prima di usare ciecamente qualcosa;)

Sulla mia macchina clz (__builtin_clz) e asm funzionano meglio. asm sembra ancora più veloce di clz ... ma potrebbe essere dovuto al semplice benchmark ...

//////// go.c ///////////////////////////////
// compile with:  gcc go.c -o go -lm
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

/***************** math ********************/

#define POS_OF_HIGHESTBITmath(a) /* 0th position is the Least-Signif-Bit */    \
  ((unsigned) log2(a))         /* thus: do not use if a <= 0 */  

#define NUM_OF_HIGHESTBITmath(a) ((a)               \
                  ? (1U << POS_OF_HIGHESTBITmath(a))    \
                  : 0)



/***************** clz ********************/

unsigned NUM_BITS_U = ((sizeof(unsigned) << 3) - 1);
#define POS_OF_HIGHESTBITclz(a) (NUM_BITS_U - __builtin_clz(a)) /* only works for a != 0 */

#define NUM_OF_HIGHESTBITclz(a) ((a)                    \
                 ? (1U << POS_OF_HIGHESTBITclz(a))  \
                 : 0)


/***************** i2f ********************/

double FF;
#define POS_OF_HIGHESTBITi2f(a) (FF = (double)(ui|1), ((*(1+(unsigned*)&FF))>>20)-1023)


#define NUM_OF_HIGHESTBITi2f(a) ((a)                    \
                 ? (1U << POS_OF_HIGHESTBITi2f(a))  \
                 : 0)




/***************** asm ********************/

unsigned OUT;
#define POS_OF_HIGHESTBITasm(a) (({asm("bsrl %1,%0" : "=r"(OUT) : "r"(a));}), OUT)

#define NUM_OF_HIGHESTBITasm(a) ((a)                    \
                 ? (1U << POS_OF_HIGHESTBITasm(a))  \
                 : 0)




/***************** bitshift1 ********************/

#define NUM_OF_HIGHESTBITbitshift1(a) (({   \
  OUT = a;                  \
  OUT |= (OUT >> 1);                \
  OUT |= (OUT >> 2);                \
  OUT |= (OUT >> 4);                \
  OUT |= (OUT >> 8);                \
  OUT |= (OUT >> 16);               \
      }), (OUT & ~(OUT >> 1)))          \



/***************** bitshift2 ********************/
int POS[32] = {0, 1, 28, 2, 29, 14, 24, 3,
             30, 22, 20, 15, 25, 17, 4, 8, 31, 27, 13, 23, 21, 19,
             16, 7, 26, 12, 18, 6, 11, 5, 10, 9};

#define POS_OF_HIGHESTBITbitshift2(a) (({   \
  OUT = a;                  \
  OUT |= OUT >> 1;              \
  OUT |= OUT >> 2;              \
  OUT |= OUT >> 4;              \
  OUT |= OUT >> 8;              \
  OUT |= OUT >> 16;             \
  OUT = (OUT >> 1) + 1;             \
      }), POS[(OUT * 0x077CB531UL) >> 27])

#define NUM_OF_HIGHESTBITbitshift2(a) ((a)              \
                       ? (1U << POS_OF_HIGHESTBITbitshift2(a)) \
                       : 0)



#define LOOPS 100000000U

int main()
{
  time_t start, end;
  unsigned ui;
  unsigned n;

  /********* Checking the first few unsigned values (you'll need to check all if you want to use an algorithm here) **************/
  printf("math\n");
  for (ui = 0U; ui < 18; ++ui)
    printf("%i\t%i\n", ui, NUM_OF_HIGHESTBITmath(ui));

  printf("\n\n");

  printf("clz\n");
  for (ui = 0U; ui < 18U; ++ui)
    printf("%i\t%i\n", ui, NUM_OF_HIGHESTBITclz(ui));

  printf("\n\n");

  printf("i2f\n");
  for (ui = 0U; ui < 18U; ++ui)
    printf("%i\t%i\n", ui, NUM_OF_HIGHESTBITi2f(ui));

  printf("\n\n");

  printf("asm\n");
  for (ui = 0U; ui < 18U; ++ui) {
    printf("%i\t%i\n", ui, NUM_OF_HIGHESTBITasm(ui));
  }

  printf("\n\n");

  printf("bitshift1\n");
  for (ui = 0U; ui < 18U; ++ui) {
    printf("%i\t%i\n", ui, NUM_OF_HIGHESTBITbitshift1(ui));
  }

  printf("\n\n");

  printf("bitshift2\n");
  for (ui = 0U; ui < 18U; ++ui) {
    printf("%i\t%i\n", ui, NUM_OF_HIGHESTBITbitshift2(ui));
  }

  printf("\n\nPlease wait...\n\n");


  /************************* Simple clock() benchmark ******************/
  start = clock();
  for (ui = 0; ui < LOOPS; ++ui)
    n = NUM_OF_HIGHESTBITmath(ui);
  end = clock();
  printf("math:\t%e\n", (double)(end-start)/CLOCKS_PER_SEC);

  start = clock();
  for (ui = 0; ui < LOOPS; ++ui)
    n = NUM_OF_HIGHESTBITclz(ui);
  end = clock();
  printf("clz:\t%e\n", (double)(end-start)/CLOCKS_PER_SEC);

  start = clock();
  for (ui = 0; ui < LOOPS; ++ui)
    n = NUM_OF_HIGHESTBITi2f(ui);
  end = clock();
  printf("i2f:\t%e\n", (double)(end-start)/CLOCKS_PER_SEC);

  start = clock();
  for (ui = 0; ui < LOOPS; ++ui)
    n = NUM_OF_HIGHESTBITasm(ui);
  end = clock();
  printf("asm:\t%e\n", (double)(end-start)/CLOCKS_PER_SEC);

  start = clock();
  for (ui = 0; ui < LOOPS; ++ui)
    n = NUM_OF_HIGHESTBITbitshift1(ui);
  end = clock();
  printf("bitshift1:\t%e\n", (double)(end-start)/CLOCKS_PER_SEC);

  start = clock();
  for (ui = 0; ui < LOOPS; ++ui)
    n = NUM_OF_HIGHESTBITbitshift2(ui);
  end = clock();
  printf("bitshift2\t%e\n", (double)(end-start)/CLOCKS_PER_SEC);

  printf("\nThe lower, the better. Take note that a negative exponent is good! ;)\n");

  return EXIT_SUCCESS;
}

6

Anche se probabilmente utilizzerei questo metodo solo se avessi assolutamente bisogno delle migliori prestazioni possibili (ad esempio per scrivere una sorta di gioco da tavolo AI che coinvolge i bitboard), la soluzione più efficiente è utilizzare ASM in linea. Vedere la sezione Ottimizzazioni di questo post del blog per il codice con una spiegazione.

[...], l' bsrlistruzione di assemblaggio calcola la posizione del bit più significativo. Quindi, potremmo usare questa asmaffermazione:

asm ("bsrl %1, %0" 
     : "=r" (position) 
     : "r" (number));

Per espandere: la soluzione del ciclo standard (spostando a sinistra e selezionando MSB) è probabilmente la più leggibile. Come in tutti i casi che coinvolgono un po 'di giochetti, la velocità di ASM non può essere battuta, anche se non ha senso ingombrare il codice a meno che non sia necessario. Gli hack sono una soluzione intermedia: in un modo o nell'altro.
Noldorin

Direi che prendere il logaritmo sarebbe una soluzione perfettamente leggibile (controlla l'sm generato per vedere se il compilatore può ottimizzarlo per usare questa istruzione asm)
jalf

A volte la soluzione ASM inline è più lenta, a seconda dell'implementazione nel microcodice della CPU.
rlbond

5
@rlbound: quasi non riesco a crederci, anche se posso sbagliarmi. Su qualsiasi CPU moderna si potrebbe pensare che verrebbe tradotto in una singola istruzione ...
Noldorin

3
@Noldorin è un po 'in ritardo ma .. Per definizione è una singola istruzione, ma se è microcodificata come suggerisce rlbond, allora quella singola istruzione potrebbe decodificare internamente a un intero gruppo di µops. Questo tende ad essere il caso delle microarchitetture AMD e Intel Atom, ma sulle normali microarchitetture Intel è una singola operazione fino in fondo.
Harold

4

Avevo bisogno di una routine per farlo e prima di cercare sul web (e trovare questa pagina) ho escogitato la mia soluzione basata su una ricerca binaria. Anche se sono sicuro che qualcuno l'abbia già fatto! Funziona a tempo costante e può essere più veloce della soluzione "ovvia" pubblicata, anche se non sto facendo grandi affermazioni, ma solo per interesse.

int highest_bit(unsigned int a) {
  static const unsigned int maskv[] = { 0xffff, 0xff, 0xf, 0x3, 0x1 };
  const unsigned int *mask = maskv;
  int l, h;

  if (a == 0) return -1;

  l = 0;
  h = 32;

  do {
    int m = l + (h - l) / 2;

    if ((a >> m) != 0) l = m;
    else if ((a & (*mask << l)) != 0) h = m;

    mask++;
  } while (l < h - 1);

  return l;
}

4

questa è una sorta di ricerca binaria, funziona con tutti i tipi di interi (senza segno!)

#include <climits>
#define UINT (unsigned int)
#define UINT_BIT (CHAR_BIT*sizeof(UINT))

int msb(UINT x)
{
    if(0 == x)
        return -1;

    int c = 0;

    for(UINT i=UINT_BIT>>1; 0<i; i>>=1)
    if(static_cast<UINT>(x >> i))
    {
        x >>= i;
        c |= i;
    }

    return c;
}

per completare:

#include <climits>
#define UINT unsigned int
#define UINT_BIT (CHAR_BIT*sizeof(UINT))

int lsb(UINT x)
{
    if(0 == x)
        return -1;

    int c = UINT_BIT-1;

    for(UINT i=UINT_BIT>>1; 0<i; i>>=1)
    if(static_cast<UINT>(x << i))
    {
        x <<= i;
        c ^= i;
    }

    return c;
}

4
Si prega di considerare di non utilizzare ALL_CAPS per so typedefqualsiasi cosa tranne le macro del preprocessore. Questa è una convenzione ampiamente accettata.
underscore_d

4

Alcune risposte eccessivamente complesse qui. La tecnica Debruin dovrebbe essere usata solo quando l'input è già una potenza di due, altrimenti c'è un modo migliore. Per una potenza di 2 input, Debruin è il più veloce in assoluto, persino più veloce di _BitScanReversequalsiasi processore che ho testato. Tuttavia, nel caso generale, _BitScanReverse(o qualunque sia l'intrinseco viene chiamato nel compilatore) è il più veloce (su alcune CPU può essere microcodificato).

Se la funzione intrinseca non è un'opzione, ecco una soluzione software ottimale per l'elaborazione degli input generali.

u8  inline log2 (u32 val)  {
    u8  k = 0;
    if (val > 0x0000FFFFu) { val >>= 16; k  = 16; }
    if (val > 0x000000FFu) { val >>= 8;  k |= 8;  }
    if (val > 0x0000000Fu) { val >>= 4;  k |= 4;  }
    if (val > 0x00000003u) { val >>= 2;  k |= 2;  }
    k |= (val & 2) >> 1;
    return k;
}

Nota che questa versione non richiede una ricerca di Debruin alla fine, a differenza della maggior parte delle altre risposte. Calcola la posizione in atto.

Le tabelle possono essere preferibili, tuttavia, se le chiami ripetutamente abbastanza volte, il rischio di un errore nella cache viene eclissato dalla velocità di una tabella.

u8 kTableLog2[256] = {
0,0,1,1,2,2,2,2,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,
5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,
6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7
};

u8 log2_table(u32 val)  {
    u8  k = 0;
    if (val > 0x0000FFFFuL) { val >>= 16; k  = 16; }
    if (val > 0x000000FFuL) { val >>=  8; k |=  8; }
    k |= kTableLog2[val]; // precompute the Log2 of the low byte

    return k;
}

Questo dovrebbe produrre il rendimento più alto di qualsiasi risposta software qui fornita, ma se lo chiami solo occasionalmente, preferisci una soluzione senza tabelle come il mio primo frammento.


1
Alcune delle risposte sono senza rami, ma probabilmente verranno compilati con rami condizionali. Hai eseguito ripetutamente il benchmarking con lo stesso valore, o un modello semplice o qualcosa del genere? La previsione errata del ramo è un killer per le prestazioni. stackoverflow.com/questions/11227809/…
Peter Cordes

3

Come sottolineano le risposte precedenti, esistono diversi modi per determinare il bit più significativo. Tuttavia, come è stato anche sottolineato, è probabile che i metodi siano unici per i registri a 32 bit o 64 bit. La pagina bithacks stanford.edu fornisce soluzioni che funzionano sia per l'elaborazione a 32 bit che a 64 bit. Con un po 'di lavoro, possono essere combinati per fornire un solido approccio cross-architecture per ottenere l'MSB. La soluzione a cui sono arrivato che compilata / funzionava su computer a 64 e 32 bit è stata:

#if defined(__LP64__) || defined(_LP64)
# define BUILD_64   1
#endif

#include <stdio.h>
#include <stdint.h>  /* for uint32_t */

/* CHAR_BIT  (or include limits.h) */
#ifndef CHAR_BIT
#define CHAR_BIT  8
#endif  /* CHAR_BIT */

/* 
 * Find the log base 2 of an integer with the MSB N set in O(N)
 * operations. (on 64bit & 32bit architectures)
 */
int
getmsb (uint32_t word)
{
    int r = 0;
    if (word < 1)
        return 0;
#ifdef BUILD_64
    union { uint32_t u[2]; double d; } t;  // temp
    t.u[__FLOAT_WORD_ORDER==LITTLE_ENDIAN] = 0x43300000;
    t.u[__FLOAT_WORD_ORDER!=LITTLE_ENDIAN] = word;
    t.d -= 4503599627370496.0;
    r = (t.u[__FLOAT_WORD_ORDER==LITTLE_ENDIAN] >> 20) - 0x3FF;
#else
    while (word >>= 1)
    {
        r++;
    }
#endif  /* BUILD_64 */
    return r;
}

Non era int r; originariamente definito sopra la #ifdef BUILD_64bandiera? In tal caso non sarebbe necessaria una ridefinizione all'interno del condizionale.
David C. Rankin

3

Una versione in C che utilizza approssimazioni successive:

unsigned int getMsb(unsigned int n)
{
  unsigned int msb  = sizeof(n) * 4;
  unsigned int step = msb;
  while (step > 1)
 {
    step /=2;
    if (n>>msb)
     msb += step;
   else
     msb -= step;
 }
  if (n>>msb)
    msb++;
  return (msb - 1);
}

Vantaggio: il tempo di esecuzione è costante indipendentemente dal numero fornito, poiché il numero di loop è sempre lo stesso. (4 cicli quando si utilizza "unsigned int")


Se lo scrivi con un operatore ternario ( msb += (n>>msb) ? step : -step;), è probabile che più compilatori creino branchless asm, evitando errori di previsione di branch su ogni passaggio ( stackoverflow.com/questions/11227809/… ).
Peter Cordes

3

So che questa domanda è molto vecchia, ma solo dopo aver implementato personalmente una funzione msb () , ho scoperto che la maggior parte delle soluzioni presentate qui e su altri siti Web non sono necessariamente le più efficienti, almeno per la mia definizione personale di efficienza (vedere anche Aggiornamento sotto ). Ecco perché:

La maggior parte delle soluzioni (specialmente quelle che impiegano una sorta di schema di ricerca binaria o l'approccio ingenuo che esegue una scansione lineare da destra a sinistra) sembrano trascurare il fatto che per numeri binari arbitrari, non ce ne sono molti che iniziano con una sequenza molto lunga di zeri. Infatti, per qualsiasi larghezza di bit, metà di tutti i numeri interi inizia con 1 e un quarto inizia con 01 . Vedi dove sto andando? La mia tesi è che una scansione lineare che parte dalla posizione del bit più significativo fino a quella meno significativa (da sinistra a destra) non è così "lineare" come potrebbe sembrare a prima vista.

Si può mostrare 1 , che per qualsiasi larghezza di bit, il numero medio di bit che devono essere testati è al massimo 2. Ciò si traduce in una complessità temporale ammortizzata di O (1) rispetto al numero di bit (!) .

Ovviamente, il caso peggiore è ancora O (n) , peggiore dell'O (log (n)) che ottieni con approcci simili alla ricerca binaria, ma poiché ci sono così pochi casi peggiori, sono trascurabili per la maggior parte delle applicazioni ( Aggiorna : non esattamente: potrebbero essercene pochi, ma potrebbero verificarsi con alta probabilità - vedere Aggiornamento sotto).

Ecco l'approccio "ingenuo" che ho trovato, che almeno sulla mia macchina batte la maggior parte degli altri approcci (gli schemi di ricerca binaria per int a 32 bit richiedono sempre log 2 (32) = 5 passaggi, mentre questo stupido algoritmo richiede meno di 2 in media) - mi dispiace per questo essere C ++ e non puro C:

template <typename T>
auto msb(T n) -> int
{
    static_assert(std::is_integral<T>::value && !std::is_signed<T>::value,
        "msb<T>(): T must be an unsigned integral type.");

    for (T i = std::numeric_limits<T>::digits - 1, mask = 1 << i; i >= 0; --i, mask >>= 1)
    {
        if ((n & mask) != 0)
            return i;
    }

    return 0;
}

Aggiornamento : mentre quello che ho scritto qui è perfettamente vero per gli interi arbitrari , dove ogni combinazione di bit è ugualmente probabile (il mio test di velocità ha semplicemente misurato quanto tempo ci è voluto per determinare l'MSB per tutti gli interi a 32 bit), i numeri interi della vita reale, per quale tale funzione verrà chiamata, di solito segue uno schema diverso: nel mio codice, ad esempio, questa funzione viene utilizzata per determinare se la dimensione di un oggetto è una potenza di 2 o per trovare la potenza successiva di 2 maggiore o uguale di una dimensione dell'oggetto . La mia ipotesi è che la maggior parte delle applicazioni che utilizzano MSB coinvolgano numeri che sono molto più piccoli del numero massimo che un intero può rappresentare (le dimensioni degli oggetti raramente utilizzano tutti i bit in un size_t). In questo caso, la mia soluzione sarà effettivamente eseguire peggio di un approccio di ricerca binaria - così quest'ultimo dovrebbe probabilmente essere preferito, anche se la mia soluzione sarà loop più velocemente attraverso tutti i numeri interi.
TL; DR: Gli interi della vita reale avranno probabilmente un pregiudizio verso il caso peggiore di questo semplice algoritmo, che alla fine lo farà peggiorare, nonostante il fatto che sia ammortizzato O (1) per numeri interi veramente arbitrari.

1 L'argomento è questo (bozza): Sia n il numero di bit (larghezza di bit). Ci sono un totale di 2 n numeri interi che possono essere rappresentati con n bit. Ci sono 2 n - 1 numeri interi che iniziano con 1 (il primo 1 è fisso, i restanti n - 1 bit possono essere qualsiasi cosa). Questi numeri interi richiedono una sola interazione del ciclo per determinare il MSB. Inoltre, ci sono 2 n - 2 numeri interi che iniziano con 01 , che richiedono 2 iterazioni, 2 n - 3 numeri interi che iniziano con 001 , che richiedono 3 iterazioni e così via.

Se sommiamo tutte le iterazioni richieste per tutti i possibili numeri interi e le dividiamo per 2 n , il numero totale di interi, otteniamo il numero medio di iterazioni necessarie per determinare l'MSB per interi a n bit:

(1 * 2 n - 1 + 2 * 2 n - 2 + 3 * 2 n - 3 + ... + n) / 2 n

Questa serie di iterazioni medie è effettivamente convergente e ha un limite di 2 per n verso l'infinito

Pertanto, l'algoritmo ingenuo da sinistra a destra ha in realtà una complessità temporale costante ammortizzata di O (1) per qualsiasi numero di bit.


2
Non penso che sia necessariamente un presupposto equo che gli input alle funzioni msb tendano ad essere distribuiti uniformemente. In pratica, questi ingressi tendono ad essere registri di interrupt o bitboard o qualche altra struttura di dati con valori distribuiti in modo non uniforme. Per un benchmark equo, penso sia più sicuro presumere che gli output (non gli input) saranno distribuiti uniformemente.
johnwbyrd

3

ci ha dato log2. Ciò elimina la necessità di tutte le log2implementazioni di salsa speciale che vedi in questa pagina. Puoi utilizzare l' log2implementazione dello standard in questo modo:

const auto n = 13UL;
const auto Index = (unsigned long)log2(n);

printf("MSB is: %u\n", Index); // Prints 3 (zero offset)

Una ndelle 0ULesigenze da cui guardarsi pure, perché:

-∞ viene restituito e FE_DIVBYZERO viene generato

Ho scritto un esempio di questo controllo che imposta arbitrariamente Indexa ULONG_MAXqui: https://ideone.com/u26vsi


Il corollario all'unica risposta gcc di ephemient è:

const auto n = 13UL;
unsigned long Index;

_BitScanReverse(&Index, n);
printf("MSB is: %u\n", Index); // Prints 3 (zero offset)

La documentazione per gli_BitScanReverse stati che Indexè:

Caricato con la posizione del primo bit impostato (1) trovato

In pratica ho scoperto che se nè 0ULche Indexè impostato0UL , proprio come sarebbe per un ndi 1UL. Ma l'unica cosa garantita nella documentazione in caso di un ndi 0ULè che il reso è:

0 se non sono stati trovati bit impostati

Pertanto, analogamente log2all'implementazione preferibile sopra, il ritorno dovrebbe essere controllato impostando Indexun valore contrassegnato in questo caso. Ho nuovamente scritto un esempio di utilizzo ULONG_MAXper questo valore di flag qui: http://rextester.com/GCU61409


No, _BitScanReverserestituisce 0 solo se l'input era 0. Questo è come l' BSRistruzione x86 , che imposta ZF in base solo all'input, non all'output. È interessante che MS definisca i documenti come lasciati indexnon impostati quando non 1viene trovato alcun bit; che corrisponde anche al comportamento x86 asm di bsr. (AMD documenta che lascia il registro di destinazione non modificato su src = 0, ma Intel dice solo un output non definito anche se le loro CPU implementano il comportamento leave-unmodified.) Questo è diverso da x86 lzcnt, che dà 32per not-found.
Peter Cordes

@PeterCordes _BitScanReverseutilizza l'indicizzazione a base zero, quindi se nè 1, l'indice del bit impostato è in realtà 0. Sfortunatamente, come dici tu se nè 0, anche l'output è 0 :( Questo significa che non c'è modo di usare il ritorno a distinguere tra n1 o 0. È quello che stavo cercando di comunicare. Pensi che ci sia un modo migliore per dirlo?
Jonathan Mee

Penso che tu stia parlando di come si imposta Index. Non è il valore di ritorno . Restituisce un valore booleano falso se l'input era zero (ed è per questo che Index viene passato per riferimento invece di essere restituito normalmente). godbolt.org/g/gQKJdE . E ho controllato: nonostante la formulazione dei documenti di MS, _BitScanReversenon lascia l'indice disattivato n==0: ottieni solo il valore che era nel registro che le è capitato di usare. (Che nel tuo caso era probabilmente lo stesso registro utilizzato in Indexseguito, portandoti a vedere a 0).
Peter Cordes

Questa domanda non è contrassegnata con c ++.
technosaurus

@technosaurus Grazie, mi sono dimenticato. Dato che la domanda è C che abbiamo effettivamente avutolog2 da C99.
Jonathan Mee

2

Pensa agli operatori bit per bit.

La prima volta ho capito male la domanda. Dovresti produrre un int con il bit più a sinistra impostato (gli altri zero). Supponendo che cmp sia impostato su quel valore:

position = sizeof(int)*8
while(!(n & cmp)){ 
   n <<=1;
   position--;
}

Cosa intendi per convertire in una stringa? La definizione di ffs accetta un int e restituisce un int. Dove sarebbe la conversione? E a cosa servirebbe la conversione se cerchiamo frammenti in una parola?
dreamlax

Non sapevo di quella funzione.
Vasil

Il 8dovrebbe essere CHAR_BIT. Questo è molto improbabile che sia il modo più veloce, perché la previsione errata del ramo si verificherà all'uscita dal ciclo a meno che non venga utilizzata ripetutamente con lo stesso input. Inoltre, per input piccoli (molti zeri), deve essere ripetuto molto. Questo è come il modo di riserva che useresti come versione facile da verificare in uno unit test per confrontare le versioni ottimizzate.
Peter Cordes

2

Espandendo il benchmark di Josh ... si può migliorare il clz come segue

/***************** clz2 ********************/

#define NUM_OF_HIGHESTBITclz2(a) ((a)                              \
                  ? (((1U) << (sizeof(unsigned)*8-1)) >> __builtin_clz(a)) \
                  : 0)

Per quanto riguarda l'asm: nota che ci sono bsr e bsrl (questa è la versione "lunga"). quello normale potrebbe essere un po 'più veloce.


1

Nota che quello che stai cercando di fare è calcolare il log2 intero di un numero intero,

#include <stdio.h>
#include <stdlib.h>

unsigned int
Log2(unsigned long x)
{
    unsigned long n = x;
    int bits = sizeof(x)*8;
    int step = 1; int k=0;
    for( step = 1; step < bits; ) {
        n |= (n >> step);
        step *= 2; ++k;
    }
    //printf("%ld %ld\n",x, (x - (n >> 1)) );
    return(x - (n >> 1));
}

Tieni presente che puoi provare a cercare più di 1 bit alla volta.

unsigned int
Log2_a(unsigned long x)
{
    unsigned long n = x;
    int bits = sizeof(x)*8;
    int step = 1;
    int step2 = 0;
    //observe that you can move 8 bits at a time, and there is a pattern...
    //if( x>1<<step2+8 ) { step2+=8;
        //if( x>1<<step2+8 ) { step2+=8;
            //if( x>1<<step2+8 ) { step2+=8;
            //}
        //}
    //}
    for( step2=0; x>1L<<step2+8; ) {
        step2+=8;
    }
    //printf("step2 %d\n",step2);
    for( step = 0; x>1L<<(step+step2); ) {
        step+=1;
        //printf("step %d\n",step+step2);
    }
    printf("log2(%ld) %d\n",x,step+step2);
    return(step+step2);
}

Questo approccio utilizza una ricerca binaria

unsigned int
Log2_b(unsigned long x)
{
    unsigned long n = x;
    unsigned int bits = sizeof(x)*8;
    unsigned int hbit = bits-1;
    unsigned int lbit = 0;
    unsigned long guess = bits/2;
    int found = 0;

    while ( hbit-lbit>1 ) {
        //printf("log2(%ld) %d<%d<%d\n",x,lbit,guess,hbit);
        //when value between guess..lbit
        if( (x<=(1L<<guess)) ) {
           //printf("%ld < 1<<%d %ld\n",x,guess,1L<<guess);
            hbit=guess;
            guess=(hbit+lbit)/2;
            //printf("log2(%ld) %d<%d<%d\n",x,lbit,guess,hbit);
        }
        //when value between hbit..guess
        //else
        if( (x>(1L<<guess)) ) {
            //printf("%ld > 1<<%d %ld\n",x,guess,1L<<guess);
            lbit=guess;
            guess=(hbit+lbit)/2;
            //printf("log2(%ld) %d<%d<%d\n",x,lbit,guess,hbit);
        }
    }
    if( (x>(1L<<guess)) ) ++guess;
    printf("log2(x%ld)=r%d\n",x,guess);
    return(guess);
}

Un altro metodo di ricerca binaria, forse più leggibile,

unsigned int
Log2_c(unsigned long x)
{
    unsigned long v = x;
    unsigned int bits = sizeof(x)*8;
    unsigned int step = bits;
    unsigned int res = 0;
    for( step = bits/2; step>0; )
    {
        //printf("log2(%ld) v %d >> step %d = %ld\n",x,v,step,v>>step);
        while ( v>>step ) {
            v>>=step;
            res+=step;
            //printf("log2(%ld) step %d res %d v>>step %ld\n",x,step,res,v);
        }
        step /= 2;
    }
    if( (x>(1L<<res)) ) ++res;
    printf("log2(x%ld)=r%ld\n",x,res);
    return(res);
}

E poiché vorrai testarli,

int main()
{
    unsigned long int x = 3;
    for( x=2; x<1000000000; x*=2 ) {
        //printf("x %ld, x+1 %ld, log2(x+1) %d\n",x,x+1,Log2(x+1));
        printf("x %ld, x+1 %ld, log2_a(x+1) %d\n",x,x+1,Log2_a(x+1));
        printf("x %ld, x+1 %ld, log2_b(x+1) %d\n",x,x+1,Log2_b(x+1));
        printf("x %ld, x+1 %ld, log2_c(x+1) %d\n",x,x+1,Log2_c(x+1));
    }
    return(0);
}

1

Inserendo questo dato che è "ancora un altro" approccio, sembra essere diverso dagli altri già forniti.

restituisce -1if x==0, altrimenti floor( log2(x)) (risultato massimo 31)

Riduci il problema da 32 a 4 bit, quindi utilizza una tabella. Forse inelegante, ma pragmatico.

Questo è ciò che uso quando non voglio utilizzare a __builtin_clzcausa di problemi di portabilità.

Per renderlo più compatto, si potrebbe invece utilizzare un loop per ridurre, aggiungendo 4 ar ogni volta, max 7 iterazioni. O qualche ibrido, come (per 64 bit): loop per ridurre a 8, test per ridurre a 4.

int log2floor( unsigned x ){
   static const signed char wtab[16] = {-1,0,1,1, 2,2,2,2, 3,3,3,3,3,3,3,3};
   int r = 0;
   unsigned xk = x >> 16;
   if( xk != 0 ){
       r = 16;
       x = xk;
   }
   // x is 0 .. 0xFFFF
   xk = x >> 8;
   if( xk != 0){
       r += 8;
       x = xk;
   }
   // x is 0 .. 0xFF
   xk = x >> 4;
   if( xk != 0){
       r += 4;
       x = xk;
   }
   // now x is 0..15; x=0 only if originally zero.
   return r + wtab[x];
}

1

Woaw, quelle erano molte risposte. Non mi dispiace di aver risposto a una vecchia domanda.

int result = 0;//could be a char or int8_t instead
if(value){//this assumes the value is 64bit
    if(0xFFFFFFFF00000000&value){  value>>=(1<<5); result|=(1<<5);  }//if it is 32bit then remove this line
    if(0x00000000FFFF0000&value){  value>>=(1<<4); result|=(1<<4);  }//and remove the 32msb
    if(0x000000000000FF00&value){  value>>=(1<<3); result|=(1<<3);  }
    if(0x00000000000000F0&value){  value>>=(1<<2); result|=(1<<2);  }
    if(0x000000000000000C&value){  value>>=(1<<1); result|=(1<<1);  }
    if(0x0000000000000002&value){  result|=(1<<0);  }
}else{
  result=-1;
}

Questa risposta è abbastanza simile a un'altra risposta ... vabbè.


Scrivere gli importi del turno 1<<kè un bel tocco. E le maschere? (1 << (1<<k-1)-1<< (1<<k-1)? ( most optimal? Confronta un superlativo?)
barba grigia

@greybeard Se guardi le modifiche di questa domanda vedrai quando ho aggiunto la parte "ottimale". Ho dimenticato di rimuoverlo perché ho cambiato la mia risposta. Inoltre non sono sicuro del motivo per cui parli delle maschere? (Quali maschere? Non ti sto seguendo)
Harry Svensson

(Le maschere (bit) sono valori usati per selezionare / cancellare i bit selettivamente / usati in &e &~.) È possibile sostituire le costanti esadecimali con simili ((type)1<<(1<<k))-1<<(1<<k).
barba grigia

Oh giusto, sto usando le maschere, me ne ero completamente dimenticato. Ho risposto a questo un paio di mesi fa ... - Hmmm, beh, dato che è stato valutato durante la compilazione, dico che è equivalente ai valori esadecimali. Tuttavia, uno è criptico e uno è esadecimale.
Harry Svensson

0

Il codice:

    // x>=1;
    unsigned func(unsigned x) {
    double d = x ;
    int p= (*reinterpret_cast<long long*>(&d) >> 52) - 1023;
    printf( "The left-most non zero bit of %d is bit %d\n", x, p);
    }

Oppure ottieni la parte intera dell'istruzione FPU FYL2X (Y * Log2 X) impostando Y = 1


uhhhhh. che cosa? come funziona questa funzione? è in qualche modo portatile?
underscore_d

I codici nella finestra sono portatili. La funzione FYL2X () è un'istruzione fpu, ma può essere convertita e trovata in alcune librerie FPU / math.
jemin

@underscore_d Funziona perché i numeri in virgola mobile sono normalizzati ... la conversione in doppio sposta i bit di mantissa per eliminare gli zeri iniziali, e questo codice estrae l'esponente e lo regola per determinare il numero di bit spostati. Certamente non è indipendente dall'architettura, ma probabilmente funzionerà su qualsiasi macchina incontrerai.
Jim Balter

Questa è una versione alternativa di questa risposta , vedere lì per i commenti su prestazioni e portabilità. (In particolare la non portabilità del casting del puntatore per il gioco di parole.) Usa la matematica degli indirizzi per ricaricare solo gli alti 32 bit di double, il che è probabilmente buono se in realtà memorizza / ricarica invece di digitare in qualche altro modo, ad es. con movqun'istruzione come potresti arrivare qui su x86.
Peter Cordes

Notate anche il mio [commento a quella risposta], dove offro il terribile avvertimento che questo metodo fornisce la risposta sbagliata per i valori (almeno) nell'intervallo [7FFFFFFFFFFFFE00 - 7FFFFFFFFFFFFFFF].
Glenn Slayden

0

Un altro poster ha fornito una tabella di ricerca utilizzando una ricerca a livello di byte . Nel caso in cui si desideri ottenere un po 'più di prestazioni (al costo di 32 KB di memoria invece di solo 256 voci di ricerca), ecco una soluzione che utilizza una tabella di ricerca a 15 bit , in C # 7 per .NET .

La parte interessante è inizializzare la tabella. Poiché si tratta di un blocco relativamente piccolo che vogliamo per la durata del processo, alloco memoria non gestita per questo utilizzando Marshal.AllocHGlobal. Come puoi vedere, per le massime prestazioni, l'intero esempio è scritto come nativo:

readonly static byte[] msb_tab_15;

// Initialize a table of 32768 bytes with the bit position (counting from LSB=0)
// of the highest 'set' (non-zero) bit of its corresponding 16-bit index value.
// The table is compressed by half, so use (value >> 1) for indexing.
static MyStaticInit()
{
    var p = new byte[0x8000];

    for (byte n = 0; n < 16; n++)
        for (int c = (1 << n) >> 1, i = 0; i < c; i++)
            p[c + i] = n;

    msb_tab_15 = p;
}

La tabella richiede un'inizializzazione una tantum tramite il codice sopra. È di sola lettura, quindi una singola copia globale può essere condivisa per l'accesso simultaneo. Con questa tabella puoi cercare rapidamente il registro dei numeri interi 2 , che è quello che stiamo cercando qui, per tutte le varie larghezze di interi (8, 16, 32 e 64 bit).

Si noti che la voce di tabella per 0, l'unico numero intero per il quale la nozione di "bit impostato più alto" non è definita, riceve il valore -1. Questa distinzione è necessaria per la corretta gestione delle parole superiori con valore 0 nel codice sottostante. Senza ulteriori indugi, ecco il codice per ciascuna delle varie primitive intere:

Versione ulong (64 bit)

/// <summary> Index of the highest set bit in 'v', or -1 for value '0' </summary>
public static int HighestOne(this ulong v)
{
    if ((long)v <= 0)
        return (int)((v >> 57) & 0x40) - 1;      // handles cases v==0 and MSB==63

    int j = /**/ (int)((0xFFFFFFFFU - v /****/) >> 58) & 0x20;
    j |= /*****/ (int)((0x0000FFFFU - (v >> j)) >> 59) & 0x10;
    return j + msb_tab_15[v >> (j + 1)];
}

Versione uint (32 bit)

/// <summary> Index of the highest set bit in 'v', or -1 for value '0' </summary>
public static int HighestOne(uint v)
{
    if ((int)v <= 0)
        return (int)((v >> 26) & 0x20) - 1;     // handles cases v==0 and MSB==31

    int j = (int)((0x0000FFFFU - v) >> 27) & 0x10;
    return j + msb_tab_15[v >> (j + 1)];
}

Vari sovraccarichi per quanto sopra

public static int HighestOne(long v) => HighestOne((ulong)v);
public static int HighestOne(int v) => HighestOne((uint)v);
public static int HighestOne(ushort v) => msb_tab_15[v >> 1];
public static int HighestOne(short v) => msb_tab_15[(ushort)v >> 1];
public static int HighestOne(char ch) => msb_tab_15[ch >> 1];
public static int HighestOne(sbyte v) => msb_tab_15[(byte)v >> 1];
public static int HighestOne(byte v) => msb_tab_15[v >> 1];

Questa è una soluzione completa e funzionante che rappresenta le migliori prestazioni su .NET 4.7.2 per numerose alternative che ho confrontato con un cablaggio specializzato per test delle prestazioni. Alcuni di questi sono menzionati di seguito. I parametri di test erano una densità uniforme di tutte le 65 posizioni di bit, cioè 0 ... 31/63 più il valore 0(che produce il risultato -1). I bit sotto la posizione dell'indice di destinazione sono stati riempiti in modo casuale. I test erano solo x64 , modalità di rilascio, con ottimizzazioni JIT abilitate.




Questa è la fine della mia risposta formale qui; Quello che segue sono alcune note casuali e collegamenti al codice sorgente per candidati di test alternativi associati al test che ho eseguito per convalidare le prestazioni e la correttezza del codice sopra.


La versione fornita sopra, codificata come Tab16A è stata un vincitore costante in molte esecuzioni. Questi vari candidati, in forma attiva / zero, possono essere trovati qui , qui e qui .

 1 candidati.HighestOne_Tab16A 622.496
 2 candidati HighestOne_Tab16C 628,234
 3 candidati.HighestOne_Tab8A 649,146
 4 candidati.HighestOne_Tab8B 656,847
 5 candidati.HighestOne_Tab16B 657,147
 6 candidati.HighestOne_Tab16D 659,650
 7 _highest_one_bit_UNMANAGED.HighestOne_U 702,900
 8 de_Bruijn.IndexOfMSB 709,672
 9 _old_2.HighestOne_Old2 715,810
10 _test_A.HighestOne8 757,188
11 _old_1.HighestOne_Old1 757,925
12 _test_A.HighestOne5 (non sicuro) 760,387
13 _test_B.HighestOne8 (non sicuro) 763,904
14 _test_A.HighestOne3 (non sicuro) 766.433
15 _test_A.HighestOne1 (non sicuro) 767,321
16 _test_A.HighestOne4 (non sicuro) 771,702
17 _test_B.HighestOne2 (non sicuro) 772,136
18 _test_B.HighestOne1 (non sicuro) 772.527
19 _test_B.HighestOne3 (non sicuro) 774,140
20 _test_A.HighestOne7 (non sicuro) 774,581
21 _test_B.HighestOne7 (non sicuro) 775.463
22 _test_A.HighestOne2 (non sicuro) 776,865
23 candidati.HighestOne_NoTab 777,698
24 _test_B.HighestOne6 (non sicuro) 779,481
25 _test_A.HighestOne6 (non sicuro) 781,553
26 _test_B.HighestOne4 (non sicuro) 785,504
27 _test_B.HighestOne5 (non sicuro) 789,797
28 _test_A.HighestOne0 (non sicuro) 809,566
29 _test_B.HighestOne0 (non sicuro) 814.990
30 _highest_one_bit.HighestOne 824,345
30 _bitarray_ext.RtlFindMostSignificantBit 894.069
31 candidati.HighestOne_Naive 898,865

Notevole è che la terribile prestazione di ntdll.dll!RtlFindMostSignificantBitvia P / Invoke:

[DllImport("ntdll.dll"), SuppressUnmanagedCodeSecurity, SecuritySafeCritical]
public static extern int RtlFindMostSignificantBit(ulong ul);

È davvero un peccato, perché ecco l'intera funzione effettiva:

    RtlFindMostSignificantBit:
        bsr rdx, rcx  
        mov eax,0FFFFFFFFh  
        movzx ecx, dl  
        cmovne      eax,ecx  
        ret

Non riesco a immaginare le scarse prestazioni originate da queste cinque linee, quindi le penalità per la transizione gestita / nativa devono essere la colpa. Sono stato anche sorpreso dal fatto che il test abbia davvero favorito le shorttabelle di ricerca diretta da 32 KB (e 64 KB) (16 bit) rispetto alle tabelle di ricerca da 128 byte (e 256 byte) byte(8 bit). Ho pensato che quanto segue sarebbe stato più competitivo con le ricerche a 16 bit, ma quest'ultimo ha costantemente superato questo:

public static int HighestOne_Tab8A(ulong v)
{
    if ((long)v <= 0)
        return (int)((v >> 57) & 64) - 1;

    int j;
    j =  /**/ (int)((0xFFFFFFFFU - v) >> 58) & 32;
    j += /**/ (int)((0x0000FFFFU - (v >> j)) >> 59) & 16;
    j += /**/ (int)((0x000000FFU - (v >> j)) >> 60) & 8;
    return j + msb_tab_8[v >> j];
}

L'ultima cosa che sottolineerò è che sono rimasto piuttosto scioccato dal fatto che il mio metodo deBruijn non fosse andato meglio. Questo è il metodo che avevo utilizzato in precedenza in modo pervasivo:

const ulong N_bsf64 = 0x07EDD5E59A4E28C2,
            N_bsr64 = 0x03F79D71B4CB0A89;

readonly public static sbyte[]
bsf64 =
{
    63,  0, 58,  1, 59, 47, 53,  2, 60, 39, 48, 27, 54, 33, 42,  3,
    61, 51, 37, 40, 49, 18, 28, 20, 55, 30, 34, 11, 43, 14, 22,  4,
    62, 57, 46, 52, 38, 26, 32, 41, 50, 36, 17, 19, 29, 10, 13, 21,
    56, 45, 25, 31, 35, 16,  9, 12, 44, 24, 15,  8, 23,  7,  6,  5,
},
bsr64 =
{
     0, 47,  1, 56, 48, 27,  2, 60, 57, 49, 41, 37, 28, 16,  3, 61,
    54, 58, 35, 52, 50, 42, 21, 44, 38, 32, 29, 23, 17, 11,  4, 62,
    46, 55, 26, 59, 40, 36, 15, 53, 34, 51, 20, 43, 31, 22, 10, 45,
    25, 39, 14, 33, 19, 30,  9, 24, 13, 18,  8, 12,  7,  6,  5, 63,
};

public static int IndexOfLSB(ulong v) =>
    v != 0 ? bsf64[((v & (ulong)-(long)v) * N_bsf64) >> 58] : -1;

public static int IndexOfMSB(ulong v)
{
    if ((long)v <= 0)
        return (int)((v >> 57) & 64) - 1;

    v |= v >> 1; v |= v >> 2;  v |= v >> 4;   // does anybody know a better
    v |= v >> 8; v |= v >> 16; v |= v >> 32;  // way than these 12 ops?
    return bsr64[(v * N_bsr64) >> 58];
}

Si discute molto di quanto siano ottimi e superiori i metodi deBruijn a questa domanda SO , e io ero tendenzialmente d'accordo. La mia ipotesi è che, mentre sia il metodo deBruijn che quello della tabella di ricerca diretta (che ho trovato essere più veloci) devono entrambi eseguire una ricerca nella tabella, ed entrambi hanno una ramificazione minima, solo deBruijn ha un'operazione di moltiplicazione a 64 bit. Ho solo testato le IndexOfMSBfunzioni qui - non il deBruijn - IndexOfLSBma mi aspetto che quest'ultimo abbia molte più possibilità poiché ha così tante operazioni in meno (vedi sopra), e probabilmente continuerò a usarlo per LSB.


1
La cache L1D sulle moderne CPU x86 è di soli 32 KB. È probabile che una LUT grande sia peggiore di una LUT piccola a meno che non si utilizzino ripetutamente gli stessi valori. In caso contrario, si verificheranno frequenti errori di cache.
Peter Cordes

0

Il mio umile metodo è molto semplice:

MSB (x) = INT [Log (x) / Log (2)]

Traduzione: L'MSB di x è il valore intero di (Log di Base x diviso per Log of Base 2).

Questo può essere adattato facilmente e rapidamente a qualsiasi linguaggio di programmazione. Provalo sulla tua calcolatrice per vedere di persona che funziona.


Funziona se tutto ciò che ti interessa è l'efficienza degli sviluppatori. Se vuoi l'efficienza di runtime, hai bisogno di un algoritmo alternativo.
Mikko Rantalainen

Questo può fallire a causa di un errore di arrotondamento. Ad esempio, in CPython 2 e 3, int(math.log((1 << 48) - 1) / math.log(2))è 48.
benrg

0

Ecco una soluzione veloce per C che funziona in GCC e Clang ; pronto per essere copiato e incollato.

#include <limits.h>

unsigned int fls(const unsigned int value)
{
    return (unsigned int)1 << ((sizeof(unsigned int) * CHAR_BIT) - __builtin_clz(value) - 1);
}

unsigned long flsl(const unsigned long value)
{
    return (unsigned long)1 << ((sizeof(unsigned long) * CHAR_BIT) - __builtin_clzl(value) - 1);
}

unsigned long long flsll(const unsigned long long value)
{
    return (unsigned long long)1 << ((sizeof(unsigned long long) * CHAR_BIT) - __builtin_clzll(value) - 1);
}

E una versione leggermente migliorata per C ++ .

#include <climits>

constexpr unsigned int fls(const unsigned int value)
{
    return (unsigned int)1 << ((sizeof(unsigned int) * CHAR_BIT) - __builtin_clz(value) - 1);
}

constexpr unsigned long fls(const unsigned long value)
{
    return (unsigned long)1 << ((sizeof(unsigned long) * CHAR_BIT) - __builtin_clzl(value) - 1);
}

constexpr unsigned long long fls(const unsigned long long value)
{
    return (unsigned long long)1 << ((sizeof(unsigned long long) * CHAR_BIT) - __builtin_clzll(value) - 1);
}

Il codice presume che valuenon lo sarà 0. Se vuoi consentire 0, devi modificarlo.


0

Presumo che la tua domanda sia per un numero intero (chiamato v di seguito) e non per un numero intero senza segno.

int v = 612635685; // whatever value you wish

unsigned int get_msb(int v)
{
    int r = 31;                         // maximum number of iteration until integer has been totally left shifted out, considering that first bit is index 0. Also we could use (sizeof(int)) << 3 - 1 instead of 31 to make it work on any platform.

    while (!(v & 0x80000000) && r--) {   // mask of the highest bit
        v <<= 1;                        // multiply integer by 2.
    }
    return r;                           // will even return -1 if no bit was set, allowing error catch
}

Se vuoi farlo funzionare senza tener conto del segno puoi aggiungere un 'v << = 1;' prima del ciclo (e modificare il valore r in 30 di conseguenza). Per favore fatemi sapere se ho dimenticato qualcosa. Non l'ho testato ma dovrebbe funzionare bene.


v <<= 1è un comportamento undefined (UB) quando v < 0.
chux - Ripristina Monica il

0x8000000, forse intendi uno 0 in più lì.
MM
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.