Найти самый значимый бит (самый левый), который установлен в битовом массиве - PullRequest
37 голосов
/ 07 апреля 2010

У меня есть реализация массива битов, где 0-й индекс - это MSB первого байта в массиве, 8-й индекс - это MSB второго байта и т. Д.

Какой быстрый способнайти первый бит, который установлен в этом массиве бит?Все соответствующие решения, которые я искал, находят первый наименее значимый бит, но мне нужен первый самый важный.Итак, учитывая 0x00A1, я хочу 8 (поскольку это 9-й бит слева).

Ответы [ 17 ]

39 голосов
/ 07 апреля 2010

GCC имеет __builtin_clz, который преобразуется в BSR для x86 / x64, CLZ для ARM и т. Д. И эмулирует инструкцию, если оборудование не реализует ее.
Visual C ++ 2005 и выше имеет _BitScanReverse.

21 голосов
/ 30 июля 2015

ТЛ: др; Для 32 битов используйте умножение Брюина .

Это "самый быстрый" портативный алгоритм. Это значительно быстрее и правильнее, чем все другие переносимые 32-битные алгоритмы MSB в этом потоке.

Алгоритм де Брюйна также возвращает правильный результат, когда входное значение равно нулю. Инструкции __builtin_clz и _BitScanReverse возвращают неверные результаты , когда вход равен нулю.

В Windows x86-64 умножение де Брюина выполняется со скоростью, сравнимой с эквивалентной (ошибочной) функцией Windows , с разницей в производительности всего около 3%.

Вот код.

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];
}

Все остальные ответы в этой теме либо работают намного хуже, чем предлагают их авторы, либо неправильно рассчитывают результат, либо и то, и другое. Давайте оценим их все, и давайте проверим, что они делают то, что, как они заявляют, делают.

Вот простая программа C ++ 11 для тестирования всех этих реализаций. Он компилируется чисто в Visual Studio, но должен работать на всех современных компиляторах. Он позволяет запускать тест в режиме производительности (bVerifyResults = false) и в режиме проверки (bVerifyResults = true).

Вот результаты в режиме проверки:

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

«Наркоман производительности» и собственные реализации Microsoft делают разные вещи, когда ввод равен нулю. msbPerformanceJunkie32 выдает -1, а _BitScanReverse от Microsoft выдает случайное число, соответствующее базовой аппаратной инструкции. Также реализация msbPerformanceJunkie32 выдает результат, который отличается от всех остальных ответов.

Вот результаты в режиме производительности, работающем на моем ноутбуке i7-4600, скомпилированном в режиме выпуска:

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            

Версия de Bruijn превосходит другие реализации разумно , потому что она не имеет ответвлений, и поэтому она хорошо работает с входами, которые производят равномерно распределенный набор выходов. Все другие версии медленнее по сравнению с произвольными входами из-за штрафов за неправильное предсказание ветвления на современных процессорах. Функция smbFfs выдает неверные результаты, поэтому ее можно игнорировать.

Некоторые реализации работают на 32-битных входах, а некоторые на 64-битных входах. Шаблон поможет нам сравнить яблоки с яблоками, независимо от размера ввода.

Вот код. Загрузите и запустите тесты самостоятельно, если хотите.

#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
}
19 голосов
/ 23 апреля 2012

Как опытный наркоман, я пробовал множество вариантов для набора MSB, вот самое быстрое, с чем я столкнулся,

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];
}
12 голосов
/ 07 апреля 2010

Есть несколько способов сделать это, и относительная производительность различных реализаций в некоторой степени зависит от машины (я случайно сравнил это в некоторой степени для аналогичной цели). На некоторых машинах есть даже встроенная инструкция для этого (используйте ее, если она доступна, и переносимость может быть решена).

Проверьте некоторые реализации здесь (под «целочисленной базой 2 журнала»). Если вы используете GCC, проверьте функции __builtin_clz и __builtin_clzl (которые делают это для ненулевых целых без знака и длинных без знака соответственно). «Clz» означает «считать ведущими нулями», что является еще одним способом описания той же проблемы.

Конечно, если ваш битовый массив не помещается в подходящее машинное слово, вам нужно перебрать слова в массиве, чтобы найти первое ненулевое слово, а затем выполнить это вычисление только для этого слова.

5 голосов
/ 07 апреля 2010

Посмотрите инструкцию asm BSR (обратное сканирование битов) x86, чтобы найти самый быстрый способ сделать это.Из документа 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).

3 голосов
/ 07 апреля 2010
2 голосов
/ 05 ноября 2011

Если вы используете x86, вы можете обойти практически любое побайтовое или пословное решение, используя операции SSE2 в сочетании с инструкциями find-first-bit, которые (в мире gcc) произносится как "ffs" для младшего бита и "fls" для старшего бита. Извините за проблемы (! @ # $% ^) Форматирования кода "C" в ответе; проверять, выписываться: http://mischasan.wordpress.com/2011/11/03/sse2-bit-trick-ffsfls-for-xmm-registers/

2 голосов
/ 02 июня 2014

Я работал с рядом функций, чтобы получить наиболее значимый бит, но обычно возникают проблемы при перемещении между 32- и 64-разрядными числами или между блоками x86_64 и x86.Функции __builtin_clz, __builtin_clzl и __builtin_clzll хорошо работают для 32/64-разрядных чисел и на машинах x86_64 и x86.Однако необходимы три функции.Я нашел простой MSB, который полагается на сдвиг вправо, который будет обрабатывать все случаи для положительных чисел.По крайней мере, для использования, которое я использую, оно преуспело там, где другие потерпели неудачу:

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

Обозначив ввод как unsigned long long, он может обрабатывать все классы чисел от unsigned char до unsigned long long и даватьстандартное определение, оно совместимо со сборками x86_64 и x86.Регистр для 0 определен как возвращающий 0, но может быть изменен при необходимости.Простой тест и вывод:

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;
}

Вывод:

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

ПРИМЕЧАНИЕ: из соображений скорости, с использованием одной функции для выполнения одной и той же задачи, сосредоточенной вокруг__builtin_clzll все еще быстрее в 6 раз.

1 голос
/ 28 июня 2011

Не самый быстрый, но работает ...

//// 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; 
}
1 голос
/ 08 июля 2010

Два лучших способа сделать это на чистом C:

Сначала выполнить линейный поиск в массиве байтов / слов, чтобы найти первый ненулевой байт / слово, а затем выполнить развернутый двоичный поиск в байте./ слово вы найдете.

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 (кстати, log2 (8)) условные переходы, чтобы получить ответ.На современных машинах x86 последняя будет оптимизирована для условного перемещения.

В качестве альтернативы используйте таблицу поиска, чтобы сопоставить байт с индексом первого установленного бита.

Соответствующая темаВы можете посмотреть это целочисленные функции log2.Насколько я помню, у ffmpeg есть хорошая реализация.

Редактировать: Вы можете фактически сделать вышеупомянутый бинарный поиск в бинарный поиск без ответвлений, но я не уверен, будет ли он более эффективным в этом случае ...

...