Posizione del bit meno significativo impostato


121

Sto cercando un modo efficiente per determinare la posizione del bit meno significativo che è impostato in un numero intero, ad esempio per 0x0FF0 sarebbe 4.

Un'implementazione banale è questa:

unsigned GetLowestBitPos(unsigned value)
{
   assert(value != 0); // handled separately

   unsigned pos = 0;
   while (!(value & 1))
   {
      value >>= 1;
      ++pos;
   }
   return pos;
}

Qualche idea su come spremerne alcuni cicli?

(Nota: questa domanda è per le persone a cui piacciono queste cose, non per le persone che mi dicono che xyzoptimization è il male.)

[modifica] Grazie a tutti per le idee! Ho imparato anche alcune altre cose. Freddo!


while ((value _N >> (++ pos))! = 0);
Thomas

Risposte:


170

Bit Twiddling Hacks offre un'eccellente raccolta di hack, ehm, bit twiddling, con allegata discussione sulle prestazioni / ottimizzazione. La mia soluzione preferita per il tuo problema (da quel sito) è «moltiplica e cerca»:

unsigned int v;  // find the number of trailing zeros in 32-bit v 
int r;           // result goes here
static const int MultiplyDeBruijnBitPosition[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
};
r = MultiplyDeBruijnBitPosition[((uint32_t)((v & -v) * 0x077CB531U)) >> 27];

Riferimenti utili:


18
Perché il voto negativo? Questa è probabilmente l'implementazione più veloce, a seconda della velocità della moltiplicazione. È certamente un codice compatto e il trucco (v & -v) è qualcosa che tutti dovrebbero imparare e ricordare.
Adam Davis

2
+1 molto interessante, quanto costa un'operazione di moltiplicazione rispetto a un'operazione if (X&Y)?
Brian R. Bondy

4
Qualcuno sa come le prestazioni di questo si confrontano con __builtin_ffslo ffsl?
Steven Lu

2
@ Jim Balter, ma il modulo è molto lento rispetto alla moltiplicazione su hardware moderno. Quindi non la definirei una soluzione migliore.
Apriori

2
Mi sembra che entrambi i valori 0x01 e 0x00 risultino nel valore 0 dall'array. Apparentemente questo trucco indicherà che il bit più basso è impostato se viene passato 0!
abelenky

80

Perché non utilizzare gli ffs incorporati ? (Ho preso una pagina man da Linux, ma è più ampiamente disponibile di così.)

ffs (3) - Pagina man di Linux

Nome

ffs - trova il primo bit impostato in una parola

Sinossi

#include <strings.h>
int ffs(int i);
#define _GNU_SOURCE
#include <string.h>
int ffsl(long int i);
int ffsll(long long int i);

Descrizione

La funzione ffs () restituisce la posizione del primo bit (meno significativo) impostato nella parola i. Il bit meno significativo è la posizione 1 e la posizione più significativa, ad esempio 32 o 64. Le funzioni ffsll () e ffsl () fanno lo stesso ma accettano argomenti di dimensioni possibilmente diverse.

Valore di ritorno

Queste funzioni restituiscono la posizione del primo bit impostato, o 0 se nessun bit è impostato in i.

Conforme a

4.3BSD, POSIX.1-2001.

Appunti

I sistemi BSD hanno un prototipo in formato <string.h>.


6
Cordiali saluti, questo viene compilato nel comando assembly corrispondente quando disponibile.
Jérémie

46

C'è un'istruzione di assembly x86 ( bsf) che lo farà. :)

Più ottimizzato ?!

Nota a margine:

L'ottimizzazione a questo livello dipende intrinsecamente dall'architettura. I processori odierni sono troppo complessi (in termini di predizione di branch, cache miss, pipelining) che è così difficile prevedere quale codice viene eseguito più velocemente su quale architettura. Diminuire le operazioni da 32 a 9 o cose del genere potrebbe persino ridurre le prestazioni su alcune architetture. Il codice ottimizzato su una singola architettura potrebbe comportare un codice peggiore nell'altra. Penso che lo ottimizzeresti per una CPU specifica o lo lasceresti così com'è e lascerai che il compilatore scelga ciò che ritiene sia meglio.


20
@dwc: ho capito, ma penso che questa clausola: "Qualche idea su come spremerne alcuni cicli?" rende una risposta del genere perfettamente accettabile!
Mehrdad Afshari

5
+1 La sua risposta dipende necessariamente dalla sua architettura a causa dell'endianness, quindi passare alle istruzioni di montaggio è una risposta perfettamente valida.
Chris Lutz

3
+1 Risposta intelligente, sì, non è C o C ++ ma è lo strumento giusto per il lavoro.
Andrew Hare

1
Aspetta, non importa. Il valore effettivo dell'intero non ha importanza qui. Scusate.
Chris Lutz

2
@Bastian: impostano ZF = 1 se l'operando è zero.
Mehrdad Afshari

43

La maggior parte delle architetture moderne avrà alcune istruzioni per trovare la posizione del bit impostato più basso, o del bit impostato più alto, o per contare il numero di zeri iniziali ecc.

Se hai un'istruzione di questa classe, puoi emulare a buon mercato le altre.

Prenditi un momento per elaborarlo su carta e renditi conto che x & (x-1)cancellerà il bit impostato più basso in x e ( x & ~(x-1) )restituirà solo il bit impostato più basso, indipendentemente dall'architettura, dalla lunghezza della parola, ecc. Sapendo questo, è banale usare l'hardware count-leading -zeroes / bit-set più alto per trovare il bit set più basso se non ci sono istruzioni esplicite per farlo.

Se non esiste alcun supporto hardware rilevante, l'implementazione di moltiplicazione e ricerca degli zero iniziali del conteggio fornita qui o uno di quelli nella pagina Bit Twiddling Hacks può essere banalmente convertita per dare il bit più basso usando le identità di cui sopra e ha il vantaggio di essere senza rami.


18

Weee, un sacco di soluzioni e non un benchmark in vista. Dovreste vergognarvi di voi stessi ;-)

La mia macchina è un Intel i530 (2,9 GHz), con Windows 7 a 64 bit. Ho compilato con una versione a 32 bit di MinGW.

$ gcc --version
gcc.exe (GCC) 4.7.2

$ gcc bench.c -o bench.exe -std=c99 -Wall -O2
$ bench
Naive loop.         Time = 2.91  (Original questioner)
De Bruijn multiply. Time = 1.16  (Tykhyy)
Lookup table.       Time = 0.36  (Andrew Grant)
FFS instruction.    Time = 0.90  (ephemient)
Branch free mask.   Time = 3.48  (Dan / Jim Balter)
Double hack.        Time = 3.41  (DocMax)

$ gcc bench.c -o bench.exe -std=c99 -Wall -O2 -march=native
$ bench
Naive loop.         Time = 2.92
De Bruijn multiply. Time = 0.47
Lookup table.       Time = 0.35
FFS instruction.    Time = 0.68
Branch free mask.   Time = 3.49
Double hack.        Time = 0.92

Il mio codice:

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


#define ARRAY_SIZE 65536
#define NUM_ITERS 5000  // Number of times to process array


int find_first_bits_naive_loop(unsigned nums[ARRAY_SIZE])
{
    int total = 0; // Prevent compiler from optimizing out the code
    for (int j = 0; j < NUM_ITERS; j++) {
        for (int i = 0; i < ARRAY_SIZE; i++) {
            unsigned value = nums[i];
            if (value == 0)
                continue;
            unsigned pos = 0;
            while (!(value & 1))
            {
                value >>= 1;
                ++pos;
            }
            total += pos + 1;
        }
    }

    return total;
}


int find_first_bits_de_bruijn(unsigned nums[ARRAY_SIZE])
{
    static const int MultiplyDeBruijnBitPosition[32] = 
    {
       1, 2, 29, 3, 30, 15, 25, 4, 31, 23, 21, 16, 26, 18, 5, 9, 
       32, 28, 14, 24, 22, 20, 17, 8, 27, 13, 19, 7, 12, 6, 11, 10
    };

    int total = 0; // Prevent compiler from optimizing out the code
    for (int j = 0; j < NUM_ITERS; j++) {
        for (int i = 0; i < ARRAY_SIZE; i++) {
            unsigned int c = nums[i];
            total += MultiplyDeBruijnBitPosition[((unsigned)((c & -c) * 0x077CB531U)) >> 27];
        }
    }

    return total;
}


unsigned char lowestBitTable[256];
int get_lowest_set_bit(unsigned num) {
    unsigned mask = 1;
    for (int cnt = 1; cnt <= 32; cnt++, mask <<= 1) {
        if (num & mask) {
            return cnt;
        }
    }

    return 0;
}
int find_first_bits_lookup_table(unsigned nums[ARRAY_SIZE])
{
    int total = 0; // Prevent compiler from optimizing out the code
    for (int j = 0; j < NUM_ITERS; j++) {
        for (int i = 0; i < ARRAY_SIZE; i++) {
            unsigned int value = nums[i];
            // note that order to check indices will depend whether you are on a big 
            // or little endian machine. This is for little-endian
            unsigned char *bytes = (unsigned char *)&value;
            if (bytes[0])
                total += lowestBitTable[bytes[0]];
            else if (bytes[1])
              total += lowestBitTable[bytes[1]] + 8;
            else if (bytes[2])
              total += lowestBitTable[bytes[2]] + 16;
            else
              total += lowestBitTable[bytes[3]] + 24;
        }
    }

    return total;
}


int find_first_bits_ffs_instruction(unsigned nums[ARRAY_SIZE])
{
    int total = 0; // Prevent compiler from optimizing out the code
    for (int j = 0; j < NUM_ITERS; j++) {
        for (int i = 0; i < ARRAY_SIZE; i++) {
            total +=  __builtin_ffs(nums[i]);
        }
    }

    return total;
}


int find_first_bits_branch_free_mask(unsigned nums[ARRAY_SIZE])
{
    int total = 0; // Prevent compiler from optimizing out the code
    for (int j = 0; j < NUM_ITERS; j++) {
        for (int i = 0; i < ARRAY_SIZE; i++) {
            unsigned value = nums[i];
            int i16 = !(value & 0xffff) << 4;
            value >>= i16;

            int i8 = !(value & 0xff) << 3;
            value >>= i8;

            int i4 = !(value & 0xf) << 2;
            value >>= i4;

            int i2 = !(value & 0x3) << 1;
            value >>= i2;

            int i1 = !(value & 0x1);

            int i0 = (value >> i1) & 1? 0 : -32;

            total += i16 + i8 + i4 + i2 + i1 + i0 + 1;
        }
    }

    return total;
}


int find_first_bits_double_hack(unsigned nums[ARRAY_SIZE])
{
    int total = 0; // Prevent compiler from optimizing out the code
    for (int j = 0; j < NUM_ITERS; j++) {
        for (int i = 0; i < ARRAY_SIZE; i++) {
            unsigned value = nums[i];
            double d = value ^ (value - !!value); 
            total += (((int*)&d)[1]>>20)-1022; 
        }
    }

    return total;
}


int main() {
    unsigned nums[ARRAY_SIZE];
    for (int i = 0; i < ARRAY_SIZE; i++) {
        nums[i] = rand() + (rand() << 15);
    }

    for (int i = 0; i < 256; i++) {
        lowestBitTable[i] = get_lowest_set_bit(i);
    }


    clock_t start_time, end_time;
    int result;

    start_time = clock();
    result = find_first_bits_naive_loop(nums);
    end_time = clock();
    printf("Naive loop.         Time = %.2f, result = %d\n", 
        (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);

    start_time = clock();
    result = find_first_bits_de_bruijn(nums);
    end_time = clock();
    printf("De Bruijn multiply. Time = %.2f, result = %d\n", 
        (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);

    start_time = clock();
    result = find_first_bits_lookup_table(nums);
    end_time = clock();
    printf("Lookup table.       Time = %.2f, result = %d\n", 
        (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);

    start_time = clock();
    result = find_first_bits_ffs_instruction(nums);
    end_time = clock();
    printf("FFS instruction.    Time = %.2f, result = %d\n", 
        (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);

    start_time = clock();
    result = find_first_bits_branch_free_mask(nums);
    end_time = clock();
    printf("Branch free mask.   Time = %.2f, result = %d\n", 
        (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);

    start_time = clock();
    result = find_first_bits_double_hack(nums);
    end_time = clock();
    printf("Double hack.        Time = %.2f, result = %d\n", 
        (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);
}

8
I benchmark sia per de Bruijn che per la ricerca potrebbero essere fuorvianti: seduti in un ciclo così stretto, dopo la prima operazione le tabelle di ricerca per ogni tipo verranno bloccate nella cache L1 fino a dopo l'ultimo ciclo. È improbabile che ciò corrisponda all'utilizzo del mondo reale.
MattW

1
Per gli ingressi con uno zero nel byte basso, ottiene i byte più alti memorizzando / ricaricando invece di spostarsi, a causa del cast del puntatore. (BTW totalmente inutile e lo rende dipendente dall'endian a differenza di un turno non lo farebbe). Ad ogni modo, quindi non solo il microbenchmark non è realistico a causa della cache calda, ma ha anche i predittori di ramo innescati e verifica gli input che prevedono molto bene e fanno sì che la LUT funzioni meno. Molti casi d'uso reali hanno una distribuzione più uniforme dei risultati, non degli input.
Peter Cordes

2
Il tuo ciclo FFS è sfortunatamente rallentato da una falsa dipendenza nell'istruzione BSF che il tuo vecchio compilatore incrostato non evita ( ma il nuovo gcc dovrebbe, lo stesso per popcnt / lzcnt / tzcnt . BSFHa una falsa dipendenza dal suo output (poiché il comportamento effettivo quando input = 0 deve lasciare l'output invariato). gcc sfortunatamente lo trasforma in una dipendenza trasportata dal ciclo non cancellando il registro tra le iterazioni del ciclo. Quindi il ciclo dovrebbe funzionare a uno ogni 5 cicli, colli di bottiglia su BSF (3) + CMOV (2) latenza
Peter Cordes

1
Il tuo benchmark ha rilevato che la LUT ha quasi esattamente il doppio del throughput del metodo FFS, che corrisponde estremamente bene alla mia previsione dell'analisi statica :). Tieni presente che stai misurando il throughtput, non la latenza, perché l'unica dipendenza seriale nel tuo ciclo si somma al totale. Senza la falsa dipendenza, ffs()avrebbe dovuto avere un throughput di uno per clock (3 uops, 1 per BSF e 2 per CMOV, e possono essere eseguiti su porte diverse). Con lo stesso overhead di loop, sono 7 UOP ALU che possono essere eseguiti (sulla CPU) a 3 per clock. L'overhead domina! Fonte: agner.org/optimize
Peter Cordes

1
Sì, l'esecuzione fuori ordine potrebbe sovrapporsi a più iterazioni del ciclo se bsf ecx, [ebx+edx*4]non fosse trattata ecxcome un input che doveva attendere. (ECX è stato scritto l'ultima volta dal CMOV dell'iterazione precedente). Ma la CPU si comporta in questo modo, per implementare il comportamento "lascia la destinazione non modificata se la sorgente è zero" (quindi non è veramente un falso dep come per TZCNT; è richiesta una dipendenza dai dati perché non ci sono ramificazioni + esecuzione speculativa sull'ipotesi che l'input è diverso da zero). Potremmo superarlo aggiungendo una xor ecx,ecxprima di bsf, per rompere la dipendenza da ECX.
Peter Cordes

17

La soluzione più veloce (non intrinseca / non assemblatrice) a questo è trovare il byte più basso e quindi utilizzare quel byte in una tabella di ricerca a 256 voci. Questo ti dà una prestazione nel caso peggiore di quattro istruzioni condizionali e un caso migliore di 1. Non solo questa è la quantità minima di istruzioni, ma la quantità minima di rami che è super-importante sull'hardware moderno.

La tabella (256 voci a 8 bit) dovrebbe contenere l'indice dell'LSB per ogni numero compreso tra 0 e 255. Controlli ogni byte del tuo valore e trovi il byte diverso da zero più basso, quindi utilizza questo valore per cercare l'indice reale.

Ciò richiede 256 byte di memoria, ma se la velocità di questa funzione è così importante, ne vale la pena per 256 byte,

Per esempio

byte lowestBitTable[256] = {
.... // left as an exercise for the reader to generate
};

unsigned GetLowestBitPos(unsigned value)
{
  // note that order to check indices will depend whether you are on a big 
  // or little endian machine. This is for little-endian
  byte* bytes = (byte*)value;
  if (bytes[0])
    return lowestBitTable[bytes[0]];
  else if (bytes[1])
      return lowestBitTable[bytes[1]] + 8;
  else if (bytes[2])
      return lowestBitTable[bytes[2]] + 16;
  else
      return lowestBitTable[bytes[3]] + 24;  
}

1
In realtà è il caso peggiore di tre condizionali :) Ma sì, questo è l'approccio più veloce (e di solito quello che le persone cercano in domande di intervista come questa).
Brian

4
Non vuoi un +8, +16, +24 da qualche parte?
Mark Ransom

7
Qualsiasi tabella di ricerca aumenta la possibilità di perdere la cache e potrebbe incorrere nel costo dell'accesso alla memoria che può essere di diversi ordini di grandezza superiore rispetto all'esecuzione delle istruzioni.
Mehrdad Afshari

1
Vorrei anche usare bit-shift (spostandolo di 8 ogni volta). allora potrebbe essere fatto interamente usando i registri. usando i puntatori, dovrai accedere alla memoria.
Johannes Schaub - litb

1
Soluzione ragionevole, ma tra la possibilità che la tabella di ricerca non sia nella cache (che può essere risolta, come sottolineato) e il numero di rami (potenziale previsione errata del ramo), preferisco di gran lunga la soluzione di moltiplicazione e ricerca (senza rami, tabella di ricerca più piccola). Ovviamente, se puoi usare gli intrinseci o l'assemblaggio in linea, probabilmente sono una scelta migliore. Tuttavia, questa soluzione non è male.

13

OMG ha appena fatto una spirale.

Ciò che manca alla maggior parte di questi esempi è una piccola comprensione di come funziona tutto l'hardware.

Ogni volta che hai un ramo, la CPU deve indovinare quale ramo verrà preso. Il tubo delle istruzioni è caricato con le istruzioni che conducono lungo il percorso ipotizzato. Se la CPU ha indovinato male, il pipe delle istruzioni viene svuotato e l'altro ramo deve essere caricato.

Considera il semplice ciclo while in alto. L'ipotesi sarà di rimanere all'interno del ciclo. Sarà sbagliato almeno una volta quando lascerà il ciclo. Questo scaricherà il tubo delle istruzioni. Questo comportamento è leggermente migliore dell'ipotesi che lascerà il ciclo, nel qual caso svuoterà la pipe di istruzioni ad ogni iterazione.

La quantità di cicli della CPU persi varia notevolmente da un tipo di processore all'altro. Ma puoi aspettarti tra 20 e 150 cicli di CPU persi.

Il prossimo gruppo peggiore è quello in cui pensi di salvare alcune iterazioni dividendo il valore in pezzi più piccoli e aggiungendo molti altri rami. Ciascuno di questi rami aggiunge un'ulteriore opportunità per svuotare il tubo delle istruzioni e costa altri 20-150 cicli di clock.

Consideriamo cosa succede quando cerchi un valore in una tabella. È probabile che il valore non sia attualmente nella cache, almeno non la prima volta che viene chiamata la funzione. Ciò significa che la CPU si blocca mentre il valore viene caricato dalla cache. Anche in questo caso varia da una macchina all'altra. I nuovi chip Intel lo utilizzano effettivamente come un'opportunità per scambiare i thread mentre il thread corrente è in attesa del completamento del caricamento della cache. Questo potrebbe essere facilmente più costoso di un lavaggio del tubo di istruzioni, tuttavia se si esegue questa operazione più volte è probabile che si verifichi solo una volta.

Chiaramente la soluzione del tempo costante più veloce è quella che coinvolge la matematica deterministica. Una soluzione pura ed elegante.

Mi scuso se questo è già stato coperto.

Ogni compilatore che uso, eccetto XCODE AFAIK, ha elementi intrinseci del compilatore sia per il bitscan diretto che per il bitscan inverso. Questi verranno compilati in una singola istruzione di assemblaggio sulla maggior parte dell'hardware senza Cache Miss, senza Branch Miss-Prediction e Nessun altro programmatore ha generato ostacoli.

Per i compilatori Microsoft utilizzare _BitScanForward e _BitScanReverse.
Per GCC utilizzare __builtin_ffs, __builtin_clz, __builtin_ctz.

Inoltre, astenersi dal pubblicare una risposta e potenzialmente fuorviare i nuovi arrivati ​​se non si è adeguatamente informati sull'argomento in discussione.

Scusa, mi sono completamente dimenticato di fornire una soluzione .. Questo è il codice che uso sull'IPAD che non ha istruzioni a livello di assemblaggio per l'attività:

unsigned BitScanLow_BranchFree(unsigned value)
{
    bool bwl = (value & 0x0000ffff) == 0;
    unsigned I1 = (bwl * 15);
    value = (value >> I1) & 0x0000ffff;

    bool bbl = (value & 0x00ff00ff) == 0;
    unsigned I2 = (bbl * 7);
    value = (value >> I2) & 0x00ff00ff;

    bool bnl = (value & 0x0f0f0f0f) == 0;
    unsigned I3 = (bnl * 3);
    value = (value >> I3) & 0x0f0f0f0f;

    bool bsl = (value & 0x33333333) == 0;
    unsigned I4 = (bsl * 1);
    value = (value >> I4) & 0x33333333;

    unsigned result = value + I1 + I2 + I3 + I4 - 1;

    return result;
}

La cosa da capire qui è che non è il confronto a essere costoso, ma il ramo che si verifica dopo il confronto. Il confronto in questo caso è forzato a un valore di 0 o 1 con .. == 0, e il risultato viene utilizzato per combinare la matematica che si sarebbe verificata su entrambi i lati del ramo.

Modificare:

Il codice sopra è completamente rotto. Questo codice funziona ed è ancora privo di branch (se ottimizzato):

int BitScanLow_BranchFree(ui value)
{
    int i16 = !(value & 0xffff) << 4;
    value >>= i16;

    int i8 = !(value & 0xff) << 3;
    value >>= i8;

    int i4 = !(value & 0xf) << 2;
    value >>= i4;

    int i2 = !(value & 0x3) << 1;
    value >>= i2;

    int i1 = !(value & 0x1);

    int i0 = (value >> i1) & 1? 0 : -32;

    return i16 + i8 + i4 + i2 + i1 + i0;
}

Restituisce -1 se viene fornito 0. Se non ti interessa 0 o sei felice di ottenere 31 per 0, rimuovi il calcolo i0, risparmiando un po 'di tempo.


3
L'ho aggiustato per te. Assicurati di testare ciò che pubblichi.
Jim Balter

5
Come puoi chiamarlo "branch-free" quando include un operatore ternario?
BoltBait

2
È una mossa condizionale. Una singola istruzione in linguaggio Assembly che accetta entrambi i valori possibili come parametri ed esegue un'operazione mov basata sulla valutazione del condizionale. E quindi è "Branch Free". non è possibile passare a un altro indirizzo sconosciuto o forse errato.
Dan

FWIW gcc genera rami anche su -O3 godbolt.org/z/gcsUHd
Qix - MONICA ERA MISTREATED

7

Ispirato da questo post simile che prevede la ricerca di un set di bit, offro quanto segue:

unsigned GetLowestBitPos(unsigned value)
{
   double d = value ^ (value - !!value); 
   return (((int*)&d)[1]>>20)-1023; 
}

Professionisti:

  • nessun loop
  • nessuna ramificazione
  • funziona a tempo costante
  • gestisce value = 0 restituendo un risultato altrimenti fuori dai limiti
  • solo due righe di codice

Contro:

  • assume poca endianness come codificato (può essere risolto cambiando le costanti)
  • presume che double sia un reale * 8 IEEE float (IEEE 754)

Aggiornamento: come sottolineato nei commenti, un'unione è un'implementazione più pulita (almeno per C) e sarebbe simile a:

unsigned GetLowestBitPos(unsigned value)
{
    union {
        int i[2];
        double d;
    } temp = { .d = value ^ (value - !!value) };
    return (temp.i[1] >> 20) - 1023;
}

Ciò presuppone int a 32 bit con archiviazione little-endian per tutto (si pensi ai processori x86).


1
Interessante: ho ancora paura di usare i doppi per l'aritmetica dei bit, ma lo terrò a mente
Peterchen

L'utilizzo di frexp () potrebbe renderlo un po 'più portabile
bel

1
La battitura di tipo tramite casting del puntatore non è sicura in C o C ++. Usa memcpy in C ++ o un'unione in C. (O un'unione in C ++ se il tuo compilatore garantisce che è sicuro. Ad esempio, le estensioni GNU per C ++ (supportate da molti compilatori) garantiscono che l'unione dei caratteri di battitura sia sicura.)
Peter Cordes

1
Il vecchio gcc rende anche un codice migliore con un'unione invece di un cast del puntatore: si sposta direttamente da un FP reg (xmm0) a rax (con movq) invece di memorizzare / ricaricare. I nuovi gcc e clang usano movq in entrambi i modi. Vedi godbolt.org/g/x7JBiL per una versione union. È intenzionale eseguire uno spostamento aritmetico di 20? I suoi presupposti dovrebbe anche lista che intè int32_t, e che firmato spostamento a destra è uno spostamento aritmetico (in C ++ è di implementazione definita)
Peter Cordes

1
Anche BTW, Visual Studio (2013 almeno) utilizza anche l'approccio test / setcc / sub. Mi piace di più il cmp / adc.
DocMax

5

Può essere fatto con un caso peggiore di meno di 32 operazioni:

Principio: il controllo di 2 o più bit è efficiente quanto il controllo di 1 bit.

Quindi, ad esempio, non c'è nulla che ti impedisca di controllare quale raggruppamento si trova per primo, quindi controllare ogni bit dal più piccolo al più grande in quel gruppo.

Quindi ...
se controlli 2 bit alla volta hai nel caso peggiore (Nbit / 2) + 1 controlli totali.
se controlli 3 bit alla volta hai nel caso peggiore (Nbit / 3) + 2 controlli totali.
...

L'ideale sarebbe controllare in gruppi di 4. Il che richiederebbe nel peggiore dei casi 11 operazioni invece delle tue 32.

Il caso migliore va dal 1 controllo del tuo algoritmo a 2 controlli se usi questa idea di raggruppamento. Ma quel controllo in più nel migliore dei casi vale la pena per i risparmi nel caso peggiore.

Nota: lo scrivo per intero invece di usare un ciclo perché è più efficiente in questo modo.

int getLowestBitPos(unsigned int value)
{
    //Group 1: Bits 0-3
    if(value&0xf)
    {
        if(value&0x1)
            return 0;
        else if(value&0x2)
            return 1;
        else if(value&0x4)
            return 2;
        else
            return 3;
    }

    //Group 2: Bits 4-7
    if(value&0xf0)
    {
        if(value&0x10)
            return 4;
        else if(value&0x20)
            return 5;
        else if(value&0x40)
            return 6;
        else
            return 7;
    }

    //Group 3: Bits 8-11
    if(value&0xf00)
    {
        if(value&0x100)
            return 8;
        else if(value&0x200)
            return 9;
        else if(value&0x400)
            return 10;
        else
            return 11;
    }

    //Group 4: Bits 12-15
    if(value&0xf000)
    {
        if(value&0x1000)
            return 12;
        else if(value&0x2000)
            return 13;
        else if(value&0x4000)
            return 14;
        else
            return 15;
    }

    //Group 5: Bits 16-19
    if(value&0xf0000)
    {
        if(value&0x10000)
            return 16;
        else if(value&0x20000)
            return 17;
        else if(value&0x40000)
            return 18;
        else
            return 19;
    }

    //Group 6: Bits 20-23
    if(value&0xf00000)
    {
        if(value&0x100000)
            return 20;
        else if(value&0x200000)
            return 21;
        else if(value&0x400000)
            return 22;
        else
            return 23;
    }

    //Group 7: Bits 24-27
    if(value&0xf000000)
    {
        if(value&0x1000000)
            return 24;
        else if(value&0x2000000)
            return 25;
        else if(value&0x4000000)
            return 26;
        else
            return 27;
    }

    //Group 8: Bits 28-31
    if(value&0xf0000000)
    {
        if(value&0x10000000)
            return 28;
        else if(value&0x20000000)
            return 29;
        else if(value&0x40000000)
            return 30;
        else
            return 31;
    }

    return -1;
}

+1 da me. Non è il più veloce ma è più veloce dell'originale, che era il punto ...
Andrew Grant

@ onebyone.livejournal.com: anche se c'era un bug nel codice, il concetto di raggruppamento è il punto che stavo cercando di trasmettere. L'esempio di codice effettivo non ha molta importanza e potrebbe essere reso più compatto ma meno efficiente.
Brian R. Bondy

Mi chiedo solo se c'è una parte davvero negativa della mia risposta, o se alla gente non è piaciuto solo che l'ho scritta per intero?
Brian R. Bondy

@ onebyone.livejournal.com: Quando confronti 2 algoritmi, dovresti confrontarli così come sono, senza dare per scontato che uno sarà magicamente trasformato da una fase di ottimizzazione. Non ho mai affermato che il mio algoritmo fosse "più veloce". Solo che sono meno operazioni.
Brian R. Bondy

@ onebyone.livejournal.com: ... non ho bisogno di profilare il codice sopra per sapere che sono meno operazioni. Lo vedo chiaramente. Non ho mai fatto affermazioni che richiedano una profilazione.
Brian R. Bondy

4

Perché non utilizzare la ricerca binaria ? Questo verrà sempre completato dopo 5 operazioni (assumendo una dimensione int di 4 byte):

if (0x0000FFFF & value) {
    if (0x000000FF & value) {
        if (0x0000000F & value) {
            if (0x00000003 & value) {
                if (0x00000001 & value) {
                    return 1;
                } else {
                    return 2;
                }
            } else {
                if (0x0000004 & value) {
                    return 3;
                } else {
                    return 4;
                }
            }
        } else { ...
    } else { ...
} else { ...

+1 Questo è molto simile alla mia risposta. Il tempo di esecuzione del caso migliore è peggiore del mio suggerimento, ma il tempo di esecuzione del caso peggiore è migliore.
Brian R. Bondy

2

Un altro metodo (divisione del modulo e ricerca) merita una menzione speciale qui dallo stesso collegamento fornito da @ anton-tykhyy. questo metodo è molto simile nelle prestazioni al metodo di moltiplicazione e ricerca di DeBruijn con una leggera ma importante differenza.

divisione e ricerca del modulo

 unsigned int v;  // find the number of trailing zeros in v
    int r;           // put the result in r
    static const int Mod37BitPosition[] = // map a bit value mod 37 to its position
    {
      32, 0, 1, 26, 2, 23, 27, 0, 3, 16, 24, 30, 28, 11, 0, 13, 4,
      7, 17, 0, 25, 22, 31, 15, 29, 10, 12, 6, 0, 21, 14, 9, 5,
      20, 8, 19, 18
    };
    r = Mod37BitPosition[(-v & v) % 37];

Il metodo di divisione e ricerca del modulo restituisce valori diversi per v = 0x00000000 ev = FFFFFFFF mentre il metodo di moltiplicazione e ricerca di DeBruijn restituisce zero su entrambi gli input.

test:-

unsigned int n1=0x00000000, n2=0xFFFFFFFF;

MultiplyDeBruijnBitPosition[((unsigned int )((n1 & -n1) * 0x077CB531U)) >> 27]); /* returns 0 */
MultiplyDeBruijnBitPosition[((unsigned int )((n2 & -n2) * 0x077CB531U)) >> 27]); /* returns 0 */
Mod37BitPosition[(((-(n1) & (n1))) % 37)]); /* returns 32 */
Mod37BitPosition[(((-(n2) & (n2))) % 37)]); /* returns 0 */

1
modè lento. Invece, puoi utilizzare il metodo di moltiplicazione e ricerca originale e sottrarre !vda rper gestire i casi limite.
Eitan T

3
@EitanT un ottimizzatore potrebbe trasformare quella mod in una rapida moltiplicazione come nella gioia degli hacker
phuclv

2

Secondo la pagina BitScan della programmazione degli scacchi e le mie misurazioni, sottrarre e xor è più veloce di negare e mascherare.

(Nota che se hai intenzione di contare gli zeri finali 0, il metodo come lo ho restituito 63mentre il negato e la maschera restituiscono 0.)

Ecco una sottrazione a 64 bit e xor:

unsigned long v;  // find the number of trailing zeros in 64-bit v 
int r;            // result goes here
static const int MultiplyDeBruijnBitPosition[64] = 
{
  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
};
r = MultiplyDeBruijnBitPosition[((uint32_t)((v ^ (v-1)) * 0x03F79D71B4CB0A89U)) >> 58];

Per riferimento, ecco una versione a 64 bit del metodo negazione e maschera:

unsigned long v;  // find the number of trailing zeros in 64-bit v 
int r;            // result goes here
static const int MultiplyDeBruijnBitPosition[64] = 
{
  0, 1, 48, 2, 57, 49, 28, 3, 61, 58, 50, 42, 38, 29, 17, 4,
  62, 55, 59, 36, 53, 51, 43, 22, 45, 39, 33, 30, 24, 18, 12, 5,
  63, 47, 56, 27, 60, 41, 37, 16, 54, 35, 52, 21, 44, 32, 23, 11,
  46, 26, 40, 15, 34, 20, 31, 10, 25, 14, 19, 9, 13, 8, 7, 6
};
r = MultiplyDeBruijnBitPosition[((uint32_t)((v & -v) * 0x03F79D71B4CB0A89U)) >> 58];

Questo (v ^ (v-1))funziona fornito v != 0. In questo caso v == 0restituisce 0xFF .... FF mentre (v & -v)dà zero (anche questo è sbagliato, ma almeno porta ad un risultato ragionevole).
CiaPan

@CiaPan: Questo è un buon punto, lo menzionerò. Immagino che ci sia un diverso numero di De Bruijn che risolverebbe il problema inserendo 0 nel 63 ° indice.
jnm2

Duh, non è questo il problema. 0 e 0x8000000000000000 risultano entrambi 0xFFFFFFFFFFFFFFFFFF dopo v ^ (v-1), quindi non è possibile distinguerli. Nel mio scenario, lo zero non verrà mai inserito.
jnm2

1

È possibile verificare se è impostato uno qualsiasi dei bit di ordine inferiore. In tal caso, osserva l'ordine inferiore dei bit rimanenti. per esempio,:

32bit int - controlla se uno dei primi 16 è impostato. In tal caso, controlla se uno dei primi 8 è impostato. se è così, ....

in caso contrario, controllare se è impostato uno dei 16 superiori.

Essenzialmente è la ricerca binaria.


1

Vedi la mia risposta qui per come farlo con una singola istruzione x86, tranne per il fatto che per trovare il bit impostato meno significativo ti consigliamo l' BSFistruzione ("bit scan forward") invece di quella BSRqui descritta.


1

Ancora un'altra soluzione, non la più veloce possibile, ma sembra abbastanza buona.
Almeno non ha rami. ;)

uint32 x = ...;  // 0x00000001  0x0405a0c0  0x00602000
x |= x <<  1;    // 0x00000003  0x0c0fe1c0  0x00e06000
x |= x <<  2;    // 0x0000000f  0x3c3fe7c0  0x03e1e000
x |= x <<  4;    // 0x000000ff  0xffffffc0  0x3fffe000
x |= x <<  8;    // 0x0000ffff  0xffffffc0  0xffffe000
x |= x << 16;    // 0xffffffff  0xffffffc0  0xffffe000

// now x is filled with '1' from the least significant '1' to bit 31

x = ~x;          // 0x00000000  0x0000003f  0x00001fff

// now we have 1's below the original least significant 1
// let's count them

x = x & 0x55555555 + (x >>  1) & 0x55555555;
                 // 0x00000000  0x0000002a  0x00001aaa

x = x & 0x33333333 + (x >>  2) & 0x33333333;
                 // 0x00000000  0x00000024  0x00001444

x = x & 0x0f0f0f0f + (x >>  4) & 0x0f0f0f0f;
                 // 0x00000000  0x00000006  0x00000508

x = x & 0x00ff00ff + (x >>  8) & 0x00ff00ff;
                 // 0x00000000  0x00000006  0x0000000d

x = x & 0x0000ffff + (x >> 16) & 0x0000ffff;
                 // 0x00000000  0x00000006  0x0000000d
// least sign.bit pos. was:  0           6          13

per ottenere tutti 1i messaggi dall'1 meno significativo a LSB, usa ((x & -x) - 1) << 1invece
phuclv

un modo ancora più veloce:x ^ (x-1)
phuclv

1
unsigned GetLowestBitPos(unsigned value)
{
    if (value & 1) return 1;
    if (value & 2) return 2;
    if (value & 4) return 3;
    if (value & 8) return 4;
    if (value & 16) return 5;
    if (value & 32) return 6;
    if (value & 64) return 7;
    if (value & 128) return 8;
    if (value & 256) return 9;
    if (value & 512) return 10;
    if (value & 1024) return 11;
    if (value & 2048) return 12;
    if (value & 4096) return 13;
    if (value & 8192) return 14;
    if (value & 16384) return 15;
    if (value & 32768) return 16;
    if (value & 65536) return 17;
    if (value & 131072) return 18;
    if (value & 262144) return 19;
    if (value & 524288) return 20;
    if (value & 1048576) return 21;
    if (value & 2097152) return 22;
    if (value & 4194304) return 23;
    if (value & 8388608) return 24;
    if (value & 16777216) return 25;
    if (value & 33554432) return 26;
    if (value & 67108864) return 27;
    if (value & 134217728) return 28;
    if (value & 268435456) return 29;
    if (value & 536870912) return 30;
    return 31;
}

Il 50% di tutti i numeri tornerà sulla prima riga di codice.

Il 75% di tutti i numeri tornerà sulle prime 2 righe di codice.

L'87% di tutti i numeri tornerà nelle prime 3 righe di codice.

Il 94% di tutti i numeri tornerà nelle prime 4 righe di codice.

Il 97% di tutti i numeri tornerà nelle prime 5 righe di codice.

eccetera.

Penso che le persone che si lamentano di quanto sia inefficiente lo scenario peggiore per questo codice non capiscono quanto sia raro che accada questa condizione.


3
E un caso peggiore di

1
Questo non potrebbe almeno essere trasformato in un interruttore ...?
Steven Lu

"Non potrebbe almeno essere trasformato in un interruttore ...?" Hai provato a farlo prima di insinuare che è possibile? Da quando puoi fare calcoli proprio sui casi di un interruttore? È una tabella di ricerca, non una classe.
j riv

1

Ho trovato questo trucco intelligente usando 'maschere magiche' in "L'arte della programmazione, parte 4", che lo fa in tempo O (log (n)) per numero di n bit. [con log (n) spazio extra]. Le soluzioni tipiche che verificano il bit impostato sono O (n) o richiedono O (n) spazio aggiuntivo per una tabella di ricerca, quindi questo è un buon compromesso.

Maschere magiche:

m0 = (...............01010101)  
m1 = (...............00110011)
m2 = (...............00001111)  
m3 = (.......0000000011111111)
....

Idea chiave: numero di zeri finali in x = 1 * [(x & m0) = 0] + 2 * [(x & m1) = 0] + 4 * [(x & m2) = 0] + ...

int lastSetBitPos(const uint64_t x) {
    if (x == 0)  return -1;

    //For 64 bit number, log2(64)-1, ie; 5 masks needed
    int steps = log2(sizeof(x) * 8); assert(steps == 6);
    //magic masks
    uint64_t m[] = { 0x5555555555555555, //     .... 010101
                     0x3333333333333333, //     .....110011
                     0x0f0f0f0f0f0f0f0f, //     ...00001111
                     0x00ff00ff00ff00ff, //0000000011111111 
                     0x0000ffff0000ffff, 
                     0x00000000ffffffff };

    //Firstly extract only the last set bit
    uint64_t y = x & -x;

    int trailZeros = 0, i = 0 , factor = 0;
    while (i < steps) {
        factor = ((y & m[i]) == 0 ) ? 1 : 0;
        trailZeros += factor * pow(2,i);
        ++i;
    }
    return (trailZeros+1);
}

1

Se C ++ 11 è disponibile per te, a volte un compilatore può eseguire l'operazione per te :)

constexpr std::uint64_t lssb(const std::uint64_t value)
{
    return !value ? 0 : (value % 2 ? 1 : lssb(value >> 1) + 1);
}

Il risultato è un indice basato su 1.


1
Intelligente, ma compila in un assemblaggio catastroficamente cattivo quando l'input non è una costante del tempo di compilazione. godbolt.org/g/7ajMyT . (Un ciclo stupido sui bit con gcc, o un'effettiva chiamata di funzione ricorsiva con clang.) Gcc / clang può valutare ffs()in fase di compilazione, quindi non è necessario utilizzarlo affinché la propagazione costante funzioni. (Non deve evitare inline-asm, ovviamente). Se si ha realmente bisogno di qualcosa che funziona come un C ++ 11 constexpr, è ancora possibile utilizzare GNU C __builtin_ffs.
Peter Cordes

0

Questo per quanto riguarda la risposta di @Anton Tykhyy

Ecco la mia implementazione di constexpr C ++ 11 che elimina i cast e rimuove un avviso su VC ++ 17 troncando un risultato a 64 bit a 32 bit:

constexpr uint32_t DeBruijnSequence[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
};
constexpr uint32_t ffs ( uint32_t value )
{
    return  DeBruijnSequence[ 
        (( ( value & ( -static_cast<int32_t>(value) ) ) * 0x077CB531ULL ) & 0xFFFFFFFF)
            >> 27];
}

Per aggirare il problema di 0x1 e 0x0 che restituiscono entrambi 0 puoi fare:

constexpr uint32_t ffs ( uint32_t value )
{
    return (!value) ? 32 : DeBruijnSequence[ 
        (( ( value & ( -static_cast<int32_t>(value) ) ) * 0x077CB531ULL ) & 0xFFFFFFFF)
            >> 27];
}

ma se il compilatore non può o non vuole preelaborare la chiamata, aggiungerà un paio di cicli al calcolo.

Infine, se interessati, ecco un elenco di asserzioni statiche per verificare che il codice faccia ciò che è inteso:

static_assert (ffs(0x1) == 0, "Find First Bit Set Failure.");
static_assert (ffs(0x2) == 1, "Find First Bit Set Failure.");
static_assert (ffs(0x4) == 2, "Find First Bit Set Failure.");
static_assert (ffs(0x8) == 3, "Find First Bit Set Failure.");
static_assert (ffs(0x10) == 4, "Find First Bit Set Failure.");
static_assert (ffs(0x20) == 5, "Find First Bit Set Failure.");
static_assert (ffs(0x40) == 6, "Find First Bit Set Failure.");
static_assert (ffs(0x80) == 7, "Find First Bit Set Failure.");
static_assert (ffs(0x100) == 8, "Find First Bit Set Failure.");
static_assert (ffs(0x200) == 9, "Find First Bit Set Failure.");
static_assert (ffs(0x400) == 10, "Find First Bit Set Failure.");
static_assert (ffs(0x800) == 11, "Find First Bit Set Failure.");
static_assert (ffs(0x1000) == 12, "Find First Bit Set Failure.");
static_assert (ffs(0x2000) == 13, "Find First Bit Set Failure.");
static_assert (ffs(0x4000) == 14, "Find First Bit Set Failure.");
static_assert (ffs(0x8000) == 15, "Find First Bit Set Failure.");
static_assert (ffs(0x10000) == 16, "Find First Bit Set Failure.");
static_assert (ffs(0x20000) == 17, "Find First Bit Set Failure.");
static_assert (ffs(0x40000) == 18, "Find First Bit Set Failure.");
static_assert (ffs(0x80000) == 19, "Find First Bit Set Failure.");
static_assert (ffs(0x100000) == 20, "Find First Bit Set Failure.");
static_assert (ffs(0x200000) == 21, "Find First Bit Set Failure.");
static_assert (ffs(0x400000) == 22, "Find First Bit Set Failure.");
static_assert (ffs(0x800000) == 23, "Find First Bit Set Failure.");
static_assert (ffs(0x1000000) == 24, "Find First Bit Set Failure.");
static_assert (ffs(0x2000000) == 25, "Find First Bit Set Failure.");
static_assert (ffs(0x4000000) == 26, "Find First Bit Set Failure.");
static_assert (ffs(0x8000000) == 27, "Find First Bit Set Failure.");
static_assert (ffs(0x10000000) == 28, "Find First Bit Set Failure.");
static_assert (ffs(0x20000000) == 29, "Find First Bit Set Failure.");
static_assert (ffs(0x40000000) == 30, "Find First Bit Set Failure.");
static_assert (ffs(0x80000000) == 31, "Find First Bit Set Failure.");

0

Ecco una semplice alternativa, anche se trovare i log è un po 'costoso.

if(n == 0)
  return 0;
return log2(n & -n)+1;   //Assuming the bit index starts from 1

-3

di recente ho visto che il premier di Singapore ha pubblicato un programma che ha scritto su Facebook, c'è una riga per menzionarlo ..

La logica è semplicemente "valore e -valore", supponi di avere 0x0FF0, quindi 0FF0 & (F00F + 1), che è uguale a 0x0010, che significa che l'1 più basso è nel 4 ° bit .. :)


1
Questo isola il bit più basso ma non ti dà la sua posizione che è ciò che questa domanda sta chiedendo.
rhashimoto

Non penso che questo funzioni neanche per trovare l'ultima parte.
yyny

value & ~ value è 0.
khw

oops, i miei occhi stanno andando male. Ho scambiato un segno meno per una tilde. ignora il mio commento
khw

-8

Se hai le risorse, puoi sacrificare la memoria per migliorare la velocità:

static const unsigned bitPositions[MAX_INT] = { 0, 0, 1, 0, 2, /* ... */ };

unsigned GetLowestBitPos(unsigned value)
{
    assert(value != 0); // handled separately
    return bitPositions[value];
}

Nota: questa tabella consumerebbe almeno 4 GB (16 GB se lasciamo il tipo restituito come unsigned). Questo è un esempio di scambio di una risorsa limitata (RAM) con un'altra (velocità di esecuzione).

Se la tua funzione deve rimanere portatile ed essere eseguita il più velocemente possibile a qualsiasi costo, questa sarebbe la strada da percorrere. Nella maggior parte delle applicazioni del mondo reale, una tabella da 4 GB non è realistica.


1
L'intervallo dell'ingresso è già specificato dal tipo di parametro - "unsigned" è un valore a 32 bit quindi no, non stai bene.
Brian

3
umm ... il tuo mitico sistema e OS ha un concetto di memoria paginata? Quanto tempo costerà?
Mikeage

14
Questa è una non risposta. La tua soluzione è completamente irrealistica in TUTTE le applicazioni del mondo reale e definirla un "compromesso" è falso. Il tuo mitico sistema che ha 16 GB di RAM da dedicare a una singola funzione semplicemente non esiste. Avresti anche risposto "usa un computer quantistico".
Brian

3
Sacrificare la memoria per la velocità? Una tabella di ricerca da 4 GB + non si adatterà mai alla cache su nessuna macchina attualmente esistente, quindi immagino che sia probabilmente più lenta di quasi tutte le altre risposte qui.

1
Argh. Questa risposta orribile continua a perseguitarmi :)@ Dan: hai ragione riguardo al caching della memoria. Vedi il commento di Mikeage sopra.
James
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.