2010-04-06 26 views
34

Tengo una implementación de matriz de bits donde el 0 ° índice es el MSB del primer byte en una matriz, el 8 ° es el MSB del segundo byte , etc ...Encontrar el bit más significativo (más a la izquierda) que se establece en una matriz de bits

¿Cuál es una forma rápida de encontrar el primer bit que se establece en esta matriz de bits? Todas las soluciones relacionadas que he buscado encuentran el primer bit menos significativo, pero necesito el primero más significativo. Entonces, dado 0x00A1, quiero 8 (ya que es el noveno bit de la izquierda).

+1

No es el bit 7 del bit más significativo establecido en 0x00a1 (suponiendo que el bit menos significativo es el bit 0)? –

+0

¿Su matriz de bits tiene una longitud arbitraria, o cabe en una palabra de máquina? –

+0

Estaba contando desde la izquierda. En binario obtengo "0000 | 0000 | 1010 | 0001", así que ese es el noveno bit, con el índice 8. Me equivoqué, debería ser 8, no 9. – Claudiu

Respuesta

38

GCC tiene __builtin_clz que se traduce en BSR en x86/x64, CLZ en ARM, etc., y emula la instrucción si el hardware no ponerla en práctica.
Visual C++ 2005 y versiones posteriores tiene _BitScanReverse.

+0

ty para el enlace – Claudiu

+2

Tenga cuidado con el comportamiento indefinido cuando el argumento es 0. – user2357112

+0

Sí. Y en este caso, "comportamiento indefinido" significa "devuelve un número no determinantemente aleatorio" – johnwbyrd

5

Busque la instrucción BSR (Bit scan reverse) x86 asm para obtener la forma más rápida de hacerlo. De doc de Intel: Searches the source operand (second operand) for the most significant set bit (1 bit). If a most significant 1 bit is found, its bit index is stored in the destination operand (first operand).

+0

O, en el PowerPC, cntlwi – Crashworks

11

Hay varias maneras de hacer esto, y el rendimiento relativo de las diferentes implementaciones es algo dependiente de la máquina (I sucede que tiene como punto de referencia esto en cierta medida para un propósito similar) En algunas máquinas incluso hay una instrucción incorporada para esto (use una si está disponible y se puede tratar la portabilidad).

Consulte algunas implementaciones here (en "base de registro entero 2"). Si está utilizando GCC, revise las funciones __builtin_clz y __builtin_clzl (que lo hacen para entradas no firmadas sin cero y largos sin firmar, respectivamente). El "clz" significa "conteo de ceros a la izquierda", que es otra forma de describir el mismo problema.

Por supuesto, si su matriz de bits no cabe en una palabra máquina adecuada, es necesario para repetir palabras en la matriz de encontrar la primera palabra que no sea cero y luego realizar este cálculo sólo en esa palabra.

+0

+1 para señalar que '__builtin_clz' y' __builtin_clzl' no están definidos para 0 entradas (según la copia de seguridad de la [documentación de GCC] (https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html)) –

0

Aquí es un algoritmo simple fuerza, bruta para una matriz de tamaño arbitrario de bytes:

int msb(unsigned char x); // prototype for function that returns 
          // most significant bit set 

unsigned char* p; 

for (p = arr + num_elements; p != arr;) { 
    --p; 
    if (*p != 0) break; 
} 

// p is with pointing to the last byte that has a bit set, or 
// it's pointing to the first byte in the array 

if (*p) { 
    return ((p - arr) * 8) + msb(*p); 
} 

// what do you want to return if no bits are set? 
return -1; 

lo dejaré como un ejercicio para el lector para llegar a una función apropiada msb(), así como la optimización para trabajar en int o long long grietas de datos de tamaño.

0

Um, su etiqueta indica 32 bits pero parece que los valores que está utilizando son de 16 bits. Si significaba 32 bits, entonces creo que la respuesta para 0x00a1 debería ser 24 y no 8.

Suponiendo que está buscando el índice de bit MSB del lado izquierdo y sabe que solo tratará con la uint32_t, aquí está el algoritmo obvio, simple-mente:

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

int main() 
{ 
    uint32_t test_value = 0x00a1; 
    int i; 

    for (i=0; i<32; ++i) 
    { 
     if (test_value & (0x80000000 >> i)) 
     { 
      printf("i = %d\n", i); 
      exit(0); 
     } 
    } 

    return 0; 
} 
+1

sí pero es demasiado lento = ( – Claudiu

2

dos mejores maneras que sé hacer esto en C puro:

Primera lineal buscar el byte/palabra matriz para encontrar el primer byte/palabra que es distinto de cero, y luego hacer un desenrollado búsqueda binaria del byte/palabra que encuentres

if (b>=0x10) 
    if (b>=0x40) 
    if (b>=0x80) return 0; 
    else return 1; 
    else 
    if (b>=0x20) return 2; 
    else return 3; 
else 
    if (b>=0x4) 
    if (b>=0x8) return 4; 
    else return 5; 
    else 
    if (b>=0x2) return 6; 
    else return 7; 

3 (BTW eso es log2 (8)) saltos condicionales para obtener la respuesta. En las modernas máquinas x86, la última se optimizará para un movimiento condicional.

otra alternativa es utilizar una tabla de búsqueda para mapear el byte con el índice del primer bit que se ha configurado.

Un tema relacionado es posible que desee mirar hacia arriba es log2 funciones enteros. Si mal no recuerdo, ffmpeg tiene una buena implementación.

Editar: En realidad puede hacer que la búsqueda binaria arriba en una búsqueda binaria sin sucursales, pero no estoy seguro de si sería más eficiente en este caso ...

1
No

el más rápido, pero funciona. ..

//// C program 
#include <math.h> 

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

#define NUM_OF_HIGHESTBIT(a) ((!(a))   \ 
     ? 0 /* no msb set*/     \ 
     : (1 << POS_OF_HIGHESTBIT(a))) 
// could be changed and optimized, if it is known that the following NEVER holds: a <= 0 



int main() 
{ 
    unsigned a = 5; // 0b101 
    unsigned b = NUM_OF_HIGHESTBIT(a); // 4 since 4 = 0b100 
    return 0; 
} 
2

Aquí está un fragmento de código que explica __builtin_clz()

////// go.c //////// 
#include <stdio.h> 

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) 


int main() 
{ 
    unsigned ui; 

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

    return 0; 
} 
2

Si está utilizando X 86, que puede vencer a prácticamente cualquier solución byte por byte o palabra por palabra utilizando la SSE2 operaciones, combinadas con las instrucciones find-first-bit, que (en el mundo gcc) se pronuncian "ffs" para el bit más bajo y "fls" para el bit más alto. Perdónenme por tener problemas (! @ # $% ^) Formateando el código "C" en una respuesta; echa un vistazo a: http://mischasan.wordpress.com/2011/11/03/sse2-bit-trick-ffsfls-for-xmm-registers/

26

Como un drogadicto rendimiento He probado un montón de variaciones de juego de MSB, el siguiente es el más rápido que he encontrado,

unsigned int msb32(unsigned int x) 
{ 
    static const unsigned int bval[] = 
    {0,1,2,2,3,3,3,3,4,4,4,4,4,4,4,4}; 

    unsigned int r = 0; 
    if (x & 0xFFFF0000) { r += 16/1; x >>= 16/1; } 
    if (x & 0x0000FF00) { r += 16/2; x >>= 16/2; } 
    if (x & 0x000000F0) { r += 16/4; x >>= 16/4; } 
    return r + bval[x]; 
} 
+2

Este código es aproximadamente cuatro veces más lento que la multiplicación de Bruijn, a través de entradas distribuidas al azar. Además, este código produce un resultado que está apagado por una de las otras respuestas, es decir, msb (1) == 1, a diferencia de las otras definiciones, para las cuales 1) == 0. – johnwbyrd

+1

esta es una respuesta horrible, ¿quién votó por alto? – snb

+3

Ese es uno de los defectos de StackOverflow y otros sitios de tipo "la mayoría de las respuestas populares". La respuesta principal siempre es la respuesta que Everyman cree que es correcta. El hombre común no siempre tiene la razón. La sabiduría de la multitud no es un sustituto de la evaluación comparativa. – johnwbyrd

-2
#define FFS(t) \ 
({ \ 
register int n = 0; \ 
      \ 
if (!(0xffff & t)) \ 
    n += 16; \ 
     \ 
if (!((0xff << n) & t)) \ 
    n += 8; \ 
     \ 
if (!((0xf << n) & t)) \ 
    n += 4; \ 
     \ 
if (!((0x3 << n) & t)) \ 
    n += 2; \ 
     \ 
if (!((0x1 << n) & t)) \ 
    n += 1; \ 
     \ 
n; \ 
}) 
+1

¿Qué tal un poco de explicación? ion en esa pieza de código? – Jesse

+1

't' probablemente debería estar entre paréntesis aquí si se trata de una macro. o mejor aún colóquelo en una variable local también para que no siempre se calcule. – Claudiu

+0

solo usa búsqueda binaria, estoy de acuerdo con sus comentarios Claudiu, pero creo que debería haber una forma más eficiente de obtener el resultado, y sin usar clz bsr instrucciones similares –

1

He trabajado con una serie de funciones para obtener el bit más significativo, pero los problemas generalmente surgen moviéndose entre números de 32 y 64 bits o moviéndose entre cajas x86_64 y x86. Las funciones __builtin_clz, __builtin_clzl y __builtin_clzll funcionan bien para números de 32/64 bits y en máquinas x86_64 y x86. Sin embargo, se requieren tres funciones. He encontrado un MSB simple que depende del desplazamiento a la derecha que manejará todos los casos para números positivos. Al menos para el uso que hago de ella, ha tenido éxito donde otros han fallado:

int 
getmsb (unsigned long long x) 
{ 
    int r = 0; 
    if (x < 1) return 0; 
    while (x >>= 1) r++; 
    return r; 
} 

Mediante la designación de entrada como unsigned long long que puede manejar todas las clases de número de unsigned char a unsigned long long y dada la definición estándar, es compatible a través de x86_64 y x86 compilaciones. El caso para 0 se define para devolver 0, pero se puede cambiar según sea necesario. Una prueba y simple de salida son:

int 
main (int argc, char *argv[]) { 

    unsigned char c0 = 0; 
    unsigned char c = 216; 
    unsigned short s = 1021; 
    unsigned int ui = 32768; 
    unsigned long ul = 3297381253; 
    unsigned long long ull = 323543844043; 

    int i = 32767; 

    printf (" %16u MSB : %d\n", c0, getmsb (c0)); 
    printf (" %16u MSB : %d\n", c, getmsb (c)); 
    printf (" %16u MSB : %d\n", s, getmsb (s)); 
    printf (" %16u MSB : %d\n", i, getmsb (i)); 
    printf (" %16u MSB : %d\n", ui, getmsb (ui)); 
    printf (" %16lu MSB : %d\n", ul, getmsb (ul)); 
    printf (" %16llu MSB : %d\n", ull, getmsb (ull)); 

    return 0; 
} 

Salida:

   0 MSB : 0 
      216 MSB : 7 
      1021 MSB : 9 
     32767 MSB : 14 
     32768 MSB : 15 
    3297381253 MSB : 31 
    323543844043 MSB : 38 

NOTA: por consideraciones de velocidad, utilizando una única función para lograr lo mismo en torno a __builtin_clzll es aún más rápido en un factor de sobre 6.

10

tl: dr; Para 32 bits, use de Bruijn multiplication.

Es el algoritmo portátil "fastest". Es sustancialmente más rápido y más correcto que todos los otros algoritmos portátiles de MSB de 32 bits en este hilo.

El algoritmo de Bruijn también devuelve un resultado correcto cuando la entrada es cero.Las instrucciones __builtin_clz y _BitScanReverse return incorrect results cuando la entrada es cero.

en x86-64, de Bruijn multiplicación funciona a una velocidad comparable a las instrucciones de hardware (equivalentes defectuosos), con una diferencia de rendimiento de sólo alrededor del 3%.

Aquí está el código.

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

    v |= v >> 1; // first round down to one less than a power of 2 
    v |= v >> 2; 
    v |= v >> 4; 
    v |= v >> 8; 
    v |= v >> 16; 

    return MultiplyDeBruijnBitPosition[(u32)(v * 0x07C4ACDDU) >> 27]; 
} 

Todas las otras respuestas en este tema, ya sea funcionan mucho peor que sus autores sugieren, o no calculan el resultado correctamente, o ambos. Analicemos todos y verifiquemos que hagan lo que dicen hacer.

Aquí hay un arnés C++ 11 simple para probar todas estas implementaciones. Compila limpio en Visual Studio pero debería funcionar en todos los compiladores modernos. Le permite ejecutar el punto de referencia en el modo de rendimiento (bVerifyResults = false) y en el modo de comprobación (bVerifyResults = true).

Éstos son los resultados en el modo de verificación:

Verification failed for msbNative64: input was 0; output was 818af060; expected 0 
Verification failed for msbFfs: input was 22df; output was 0; expected d 
Verification failed for msbPerformanceJunkie32: input was 0; output was ffffffff; expected 0 
Verification failed for msbNative32: input was 0; output was 9ab07060; expected 0 

El "drogadicto de rendimiento" y las implementaciones nativas de Microsoft hacen cosas diferentes cuando la entrada es cero. msbPerformanceJunkie32 produce -1, y _BitScanReverse de Microsoft produce un número aleatorio, consistente con la instrucción de hardware subyacente. Además, la implementación msbPerformanceJunkie32 produce un resultado que está desactivado por una de todas las otras respuestas.

Éstos son los resultados en el modo de ejecución, que se ejecuta en mi ordenador portátil i7-4600, compilado en modo de lanzamiento:

msbLoop64 took 2.56751 seconds    
msbNative64 took 0.222197 seconds    

msbLoop32 took 1.43456 seconds    
msbFfs took 0.525097 seconds     
msbPerformanceJunkie32 took 1.07939 seconds 
msbDeBruijn32 took 0.224947 seconds   
msbNative32 took 0.218275 seconds    

La versión de Bruijn es mejor que la de otras implementaciones profundamente porque es sin sucursales, y por lo tanto funciona bien contra las entradas que producen un conjunto de salidas uniformemente distribuidas. Todas las demás versiones son más lentas frente a las entradas arbitrarias debido a las penalizaciones de errores de predicción en las CPU modernas. La función smbFfs produce resultados incorrectos, por lo que puede ignorarse.

Algunas de las implementaciones funcionan en entradas de 32 bits, y algunas funcionan en entradas de 64 bits. Una plantilla nos ayudará a comparar manzanas con manzanas, independientemente del tamaño de la entrada.

Aquí está el código. Descargue y ejecute los puntos de referencia usted mismo si lo desea.

#include <iostream> 
#include <chrono> 
#include <random> 
#include <cassert> 
#include <string> 
#include <limits> 

#ifdef _MSC_VER 
#define MICROSOFT_COMPILER 1 
#include <intrin.h> 
#endif // _MSC_VER 

const int iterations = 100000000; 
bool bVerifyResults = false; 
std::random_device rd; 
std::default_random_engine re(rd()); 
typedef unsigned int u32; 
typedef unsigned long long u64; 

class Timer 
{ 
public: 
    Timer() : beg_(clock_::now()) {} 
    void reset() { 
     beg_ = clock_::now(); 
    } 
    double elapsed() const { 
     return std::chrono::duration_cast<second_> 
      (clock_::now() - beg_).count(); 
    } 

private: 
    typedef std::chrono::high_resolution_clock clock_; 
    typedef std::chrono::duration<double, std::ratio<1> > second_; 
    std::chrono::time_point<clock_> beg_; 
}; 

unsigned int msbPerformanceJunkie32(u32 x) 
{ 
    static const unsigned int bval[] = 
    { 0,1,2,2,3,3,3,3,4,4,4,4,4,4,4,4 }; 
    unsigned int r = 0; 
    if (x & 0xFFFF0000) { 
     r += 16/1; 
     x >>= 16/1; 
    } 
    if (x & 0x0000FF00) { 
     r += 16/2; 
     x >>= 16/2; 
    } 
    if (x & 0x000000F0) { 
     r += 16/4; 
     x >>= 16/4; 
    } 
    return r + bval[x]; 
} 

#define FFS(t) \ 
{ \ 
register int n = 0; \ 
if (!(0xffff & t)) \ 
n += 16; \ 
if (!((0xff << n) & t)) \ 
n += 8; \ 
if (!((0xf << n) & t)) \ 
n += 4; \ 
if (!((0x3 << n) & t)) \ 
n += 2; \ 
if (!((0x1 << n) & t)) \ 
n += 1; \ 
return n; \ 
} 

unsigned int msbFfs32(u32 x) 
{ 
    FFS(x); 
} 

unsigned int msbLoop32(u32 x) 
{ 
    int r = 0; 
    if (x < 1) return 0; 
    while (x >>= 1) r++; 
    return r; 
} 

unsigned int msbLoop64(u64 x) 
{ 
    int r = 0; 
    if (x < 1) return 0; 
    while (x >>= 1) r++; 
    return r; 
} 

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

    v |= v >> 1; // first round down to one less than a power of 2 
    v |= v >> 2; 
    v |= v >> 4; 
    v |= v >> 8; 
    v |= v >> 16; 

    return MultiplyDeBruijnBitPosition[(u32)(v * 0x07C4ACDDU) >> 27]; 
} 

#ifdef MICROSOFT_COMPILER 
u32 msbNative32(u32 val) 
{ 
    unsigned long result; 
    _BitScanReverse(&result, val); 
    return result; 
} 
u32 msbNative64(u64 val) 
{ 
    unsigned long result; 
    _BitScanReverse64(&result, val); 
    return result; 
} 
#endif // MICROSOFT_COMPILER 

template <typename InputType> 
void test(unsigned int msbFunc(InputType), 
    const std::string &name, 
    const std::vector<InputType> &inputs, 
    std::vector< unsigned int > &results, 
    bool bIsReference = false 
) 
{ 
    if (bIsReference) 
    { 
     int i = 0; 
     for (int i = 0; i < iterations; i++) 
      results[i] = msbFunc(inputs[i]); 
    } 
    InputType result; 
    if (bVerifyResults) 
    { 
     bool bNotified = false; 
     for (int i = 0; i < iterations; i++) 
     { 
      result = msbFunc(inputs[i]); 
      if ((result != results[i]) && !bNotified) 
      { 
       std::cout << "Verification failed for " << name << ": " 
        << "input was " << std::hex << inputs[i] 
        << "; output was " << result 
        << "; expected " << results[i] 
        << std::endl; 
       bNotified = true; 
      } 
     } 
    } 
    else 
    { 
     Timer t; 
     for (int i = 0; i < iterations; i++) 
     { 
      result = msbFunc(inputs[i]); 
     } 
     double elapsed = t.elapsed(); 
     if (!bIsReference) 
      std::cout << name << " took " << elapsed << " seconds" << std::endl; 
     if (result == -1.0f) 
      std::cout << "this comparison only exists to keep the compiler from " << 
      "optimizing out the benchmark; this branch will never be called"; 
    } 
} 

void main() 
{ 
    std::uniform_int_distribution <u64> dist64(0, 
     std::numeric_limits<u64>::max()); 
    std::uniform_int_distribution <u32> shift64(0, 63); 
    std::vector<u64> inputs64; 
    for (int i = 0; i < iterations; i++) 
    { 
     inputs64.push_back(dist64(re) >> shift64(re)); 
    } 
    std::vector<u32> results64; 
    results64.resize(iterations); 

    test<u64>(msbLoop64, "msbLoop64", inputs64, results64, true); 
    test<u64>(msbLoop64, "msbLoop64", inputs64, results64, false); 
#ifdef MICROSOFT_COMPILER 
    test<u64>(msbNative64, "msbNative64", inputs64, results64, false); 
#endif // MICROSOFT_COMPILER 
    std::cout << std::endl; 

    std::uniform_int_distribution <u32> dist32(0, 
     std::numeric_limits<u32>::max()); 
    std::uniform_int_distribution <u32> shift32(0, 31); 
    std::vector<u32> inputs32; 
    for (int i = 0; i < iterations; i++) 
     inputs32.push_back(dist32(re) >> shift32(re)); 
    std::vector<u32> results32; 
    results32.resize(iterations); 


    test<u32>(msbLoop32, "msbLoop32", inputs32, results32, true); 

    test<u32>(msbLoop32, "msbLoop32", inputs32, results32, false); 
    test<u32>(msbFfs32, "msbFfs", inputs32, results32, false); 
    test<u32>(msbPerformanceJunkie32, "msbPerformanceJunkie32", 
     inputs32, results32, false); 
    test<u32>(msbDeBruijn32, "msbDeBruijn32", inputs32, results32, false); 
#ifdef MICROSOFT_COMPILER 
    test<u32>(msbNative32, "msbNative32", inputs32, results32, false); 
#endif // MICROSOFT_COMPILER 
} 
+0

Buen trabajo, pero actualmente incluye el trabajo de inicialización hecho por 'msbLoop32' en su sincronización, lo que significa que parece dos veces más lento de lo que realmente es. –

+0

También me interesa saber cómo se puede salir multiplicando por una 'v' que es * una menos de * una potencia de 2. El PDF vinculado solo explica por qué la multiplicación corresponde a un cambio cuando es un poder de 2, así que habría pensado agregar 1 sería necesario. –

+0

¿Qué trabajo de inicialización percibes que está haciendo msbLoop32? – johnwbyrd

1

Voy a agregar uno!

typedef unsigned long long u64; 
typedef unsigned int  u32; 
typedef unsigned char  u8; 


u8 findMostSignificantBit (u64 u64Val) 
{ 
    u8 u8Shift; 
    u8 u8Bit = 0; 

    assert (u64Val != 0ULL); 

    for (u8Shift = 32 ; u8Shift != 0 ; u8Shift >>= 1) 
    { 
    u64 u64Temp = u64Val >> u8Shift; 
    if (u64Temp) 
    { 
     u8Bit |= u8Shift; // notice not using += 
     u64Val = u64Temp; 
    } 
    } 

    return u8Bit; 
} 

Por supuesto, esto está trabajando en un número de 64 bits (unsigned long long), y no en una matriz. Además, mucha gente ha señalado a funciones incorporadas de g ++ que no conocía. Que interesante.

De todos modos, esto encuentra el bit más significativo en 6 iteraciones y da una afirmación si pasó 0 a la función. No es la mejor función para usar si tiene acceso a una instrucción del chipset.

También estoy usando | = en lugar de + = porque estas son siempre potencias de dos, y O es (clásicamente) más rápido que la suma. Como solo estoy agregando poderes únicos de 2 juntos, nunca me volqué.

Esta es una búsqueda binaria que significa que siempre encuentra el resultado en 6 iteraciones.

De nuevo, esto es mejor:

u8 findMostSignificantBit2 (u64 u64Val) 
{ 
    assert (u64Val != 0ULL); 

    return (u8) (__builtin_ctzll(u64Val)); 
} 
Cuestiones relacionadas