Позиція найменш значущого біта, яка встановлена


120

Я шукаю ефективний спосіб визначити позицію найменш значущого біта, встановленого в цілому, наприклад, для 0x0FF0 це було б 4.

Тривіальна реалізація така:

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

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

Будь-які ідеї, як вичавити з неї кілька циклів?

(Зауважте: це питання стосується людей, які користуються подібними речами, а не для того, щоб люди говорили мені, що ксизоптимізація - це зло.)

[редагувати] Дякую всім за ідеї! Я також навчився ще кількох речей. Класно!


while ((значення _N >> (++ поз))! = 0);
Томас

Відповіді:


170

Біт Twiddling Hacks пропонує чудову колекцію, ер, біт твідінг хаків, з додаванням обговорення продуктивності / оптимізації. Моє улюблене рішення вашої проблеми (з цього сайту) - «множення та пошук»:

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

Корисні посилання:


18
Чому потік? Це, можливо, найшвидша реалізація, залежно від швидкості множення. Це, безумовно, компактний код, і трюк (v & -v) - це те, що кожен повинен вивчити і запам'ятати.
Адам Девіс

2
+1 дуже круто, наскільки дорогою є операція множення, хоча порівняно з операцією if (X&Y)?
Брайан Р. Бонді

4
Хтось знає, як ефективність цього порівняння з __builtin_ffslчи ffsl?
Стівен Лу

2
@Jim Balter, але модуль дуже повільний порівняно з множенням на сучасному обладнанні. Тому я б не назвав це кращим рішенням.
Апріорі

2
Мені здається, що і значення 0x01, і 0x00 призводять до значення 0 з масиву. Мабуть, цей трюк вкаже, що найнижчий біт встановлюється, якщо 0 передано!
абеленький

80

Чому б не використовувати вбудований ffs ? (Я схопив довідкову сторінку з Linux, але вона є більш доступною, ніж це.)

ffs (3) - сторінка man Linux

Ім'я

ffs - знайти перший набір бітів у слові

Конспект

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

Опис

Функція ffs () повертає позицію першого (найменш значущого) біта, встановленого у слові i. Найменш значущим бітом є позиція 1 і найзначніша позиція, наприклад, 32 або 64. Функції ffsll () і ffsl () виконують те саме, але беруть аргументи, можливо, різного розміру.

Повернене значення

Ці функції повертають позицію першого набору бітів, або 0, якщо в i не встановлено жодних бітів.

Відповідно до

4.3BSD, POSIX.1-2001.

Примітки

Системи BSD мають прототип в <string.h>.


6
FYI, це компілюється у відповідну команду складання, коли вона є.
Jérémie

46

Існує інструкція по збірці x86 ( bsf), яка буде виконувати це. :)

Більш оптимізований ?!

Бічна примітка:

Оптимізація на цьому рівні по суті залежить від архітектури. Сьогоднішні процесори занадто складні (з точки зору прогнозування гілок, пропусків кешу, конвеєрного випромінювання), що важко передбачити, який код виконується швидше, за якою архітектурою. Зниження операцій з 32 до 9 або подібні речі можуть навіть знизити продуктивність у деяких архітектурах. Оптимізований код в одній архітектурі може призвести до гіршого коду в іншій. Я думаю, ви або оптимізуєте це під конкретний процесор, або залишите його таким, яким він є, і дозволите компілятору вибрати те, що він вважає кращим.


20
@dwc: Я розумію, але я думаю, що це застереження: "Будь-які ідеї, як вичавити з нього деякі цикли?" робить таку відповідь цілком прийнятною!
Мехрдад Афшарі

5
+1 Його відповідь неодмінно залежить від його архітектури через витривалість, тому відмова до інструкцій по збірці - цілком коректна відповідь.
Кріс Лутц

3
+1 Розумна відповідь, так це не C або C ++, але це правильний інструмент для роботи.
Ендрю Заєць

1
Зачекайте, не забудьте. Фактичне значення цілого числа тут не має значення. Вибачте.
Кріс Лутц

2
@Bastian: Вони встановлюють ZF = 1, якщо операнд дорівнює нулю.
Мехрдад Афшарі

43

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

Якщо у вас є одна інструкція цього класу, ви можете дешево наслідувати іншим.

Знайдіть хвилину, щоб пропрацювати її на папері і зрозумійте, що x & (x-1)очистить найнижчий встановлений біт у x та( x & ~(x-1) ) поверне лише найнижчий встановлений біт, незалежно від архітектури, довжини слова і т. Д. Знаючи це, тривіально використовувати апаратне підрахунок лідерів -zeroes / high-set-bit, щоб знайти найнижчий встановлений біт, якщо для цього немає явної інструкції.

Якщо взагалі відсутня відповідна апаратна підтримка, реалізація множення та пошуку нульових нульових лічильників, наведених тут, або один із них на сторінці Bit Twiddling Hacks, може тривіально перетворитись, щоб отримати найнижчий встановлений біт, використовуючи вищевказані ідентичності та має перевагу в тому, що він є безділом


18

Weee, безліч рішень і не орієнтир на очах. Ви, люди, повинні соромитися себе ;-)

Моя машина - Intel i530 (2,9 ГГц), працює під управлінням Windows 7 64-розрядної. Я компілював 32-бітну версію 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

Мій код:

#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
Орієнтовні показники як для Bruijn, так і для пошуку можуть бути оманливими - сидячи в такому циклі, після першої операції таблиці пошуку кожного типу будуть зафіксовані в кеші L1 до останнього циклу. Це, мабуть, не відповідає реальному використанню.
MattW

1
Для входів з нулем у низькому байті він отримує більш високі байти, зберігаючи / перезавантажуючи замість зсуву, через передачу покажчиків. (абсолютно непотрібна BTW, і робить це залежно від ендіану на відміну від зміни не буде). Так чи інакше, мікробензік нереальний через гарячого кешу, він також передбачує передбачувачі гілок і тестує входи, які дуже добре прогнозують і змушують LUT робити менше роботи. У багатьох реальних випадках використання є більш рівномірний розподіл результатів, а не входів.
Пітер Кордес

2
На жаль, ваш цикл FFS, на жаль, сповільнюється помилковою залежністю в інструкції BSF, якої не уникає ваш старий кінцевий компілятор ( але новіший gcc повинен, те ж саме, і для popcnt / lzcnt / tzcnt .) BSFМає помилкову залежність від його результату (оскільки фактична поведінка коли input = 0 залишає вихід незмінним). gcc, на жаль, перетворює це на циклічну залежність, не очищаючи реєстр між ітераціями циклу. Отже, цикл повинен працювати в один раз на 5 циклів, вузький на BSF (3) + CMOV (2) затримка.
Пітер Кордес

1
Ваш показник показав, що LUT має майже рівно вдвічі пропускну здатність методу FFS, що надзвичайно добре відповідає моєму прогнозу статичного аналізу :). Зауважте, що ви вимірюєте потужність, а не затримку, оскільки єдина послідовна залежність у вашому циклі підсумовується до загальної суми. Без помилкової залежності ffs()повинна була мати пропускну здатність одна на такт (3 уп, 1 для BSF і 2 для CMOV, і вони можуть працювати на різних портах). З цією ж петлею над головою це 7 UU ALU, які можуть працювати (у вашому процесорі) зі швидкістю 3 за годину. Над головою панує! Джерело: agner.org/optimize
Пітер Кордес

1
Так, виконання поза замовленням може перекривати декілька ітерацій циклу, якщо bsf ecx, [ebx+edx*4]не трактувати його ecxяк вхід, якого доведеться чекати. (ECX востаннє був написаний CMOV попереднього ітератону). Але ЦП так поводиться, щоб реалізувати поведінку "залиште призначення незмінним, якщо джерело дорівнює нулю" (тому це справді неправдиве зображення, як це є для TZCNT; потрібна залежність даних, оскільки немає розгалуження + спекулятивного виконання на припущенні що вхід не нульовий). Ми могли б подолати це, додавши xor ecx,ecxдо bsf, щоб розбити залежність від ECX.
Пітер Кордес

17

Найшвидше рішення (не вбудоване / немонтажне) для цього - знайти найнижчий байт, а потім використовувати цей байт у таблиці пошуку 256 записів. Це дає найгірше виконання чотирьох умовних інструкцій та найкращого випадку 1. Це не лише найменша кількість інструкцій, але і найменша кількість галузей, що надзвичайно важливо для сучасного обладнання.

Ваша таблиця (256 8-бітних записів) повинна містити індекс LSB для кожного числа в діапазоні 0-255. Ви перевіряєте кожен байт свого значення і знаходите найнижчий ненульовий байт, а потім використовуєте це значення для пошуку реального індексу.

Для цього потрібно 256-байт оперативної пам’яті, але якщо швидкість цієї функції настільки важлива, то 256-байт того варто,

Напр

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
Насправді це найгірший випадок з трьох умовних умов :) Але так, це найшвидший підхід (і зазвичай те, що люди шукають у таких питаннях для інтерв'ю).
Брайан

4
Чи не хочете ви десь там +8, +16, +24?
Марк Викуп

7
Будь-яка таблиця пошуку збільшує шанси пропустити кеш і може призвести до витрат на пам'ять, які можуть бути на кілька порядків вище, ніж виконання інструкцій.
Мехрдад Афшарі

1
Я б навіть використовувати біт-зсуви (зміщуючи його на 8 кожен раз). тоді можна було б зробити повністю, використовуючи регістри. за допомогою покажчиків вам доведеться отримати доступ до пам'яті.
Йоханнес Шауб - ліб

1
Розумне рішення, але між потенціалом, що таблиця пошуку не знаходиться в кеші (яку можна вирішити, як вказувалося), та кількістю гілок (непередбачуване потенційне розгалуження), я віддаю перевагу рішенню множення та пошуку (немає гілок, менша таблиця пошуку). Звичайно, якщо ви можете використовувати внутрішню техніку або вбудовану збірку, вони, мабуть, кращий вибір. І все-таки це рішення погано.

13

OMG це просто спіралі.

Чого не вистачає більшості цих прикладів, це трохи розуміння того, як працює все обладнання.

У будь-який час у вас є відділення, процесор повинен здогадуватися, яку галузь буде взято. Інструкційна труба завантажена інструкціями, які ведуть вниз по здогаданому шляху. Якщо ЦП помилився неправильно, тоді інструкційна труба промивається, а інша гілка повинна бути завантажена.

Розглянемо просту петлю вгорі. Здогадка буде залишатися в циклі. Буде неправильно хоча б раз, коли вийде з циклу. Це БУДЕ промити інструкційну трубу. Така поведінка трохи краще, ніж здогадуватися, що вона вийде з циклу, і в такому випадку вона буде змивати інструкційну трубку на кожній ітерації.

Кількість втрачених циклів процесора сильно варіюється від одного типу процесора до іншого. Але ви можете очікувати від 20 до 150 втрачених циклів процесора.

Наступною гіршою групою є те, де ви думаєте, що збираєтеся зберегти кілька ітерацій, розділивши значення на менші шматки та додавши ще кілька гілок. Кожна з цих гілок додає додаткову можливість очистити інструкційну трубу і коштувати ще 20-150 тактових циклів.

Давайте розглянемо, що відбувається, коли ви шукаєте значення в таблиці. Цілком ймовірно, що значення наразі не знаходиться в кеші, принаймні, не перший раз, коли ваша функція викликається. Це означає, що процесор зупиняється, коли значення завантажується з кешу. Знову це змінюється від однієї машини до іншої. Нові мікросхеми Intel фактично використовують це як можливість обміну потоками, поки поточний потік очікує завершення завантаження кеша. Це може бути дорожче, ніж промивання інструкційної труби, однак, якщо ви виконуєте цю операцію кілька разів, ймовірно, це відбудеться лише один раз.

Очевидно, що найшвидшим рішенням постійного часу є рішення, яке включає детерміновану математику. Чисте і елегантне рішення.

Мої вибачення, якщо це вже було висвітлено.

Кожен компілятор, який я використовую, за винятком XCODE AFAIK, має властивості компілятора як для бітскана переднього, так і для зворотного бітів. Вони складатимуться до однієї інструкції по збірці на більшості апаратних засобів без кеш-міс, без промахування філії та жодного іншого програміста, що генерує камені спотикання.

Для компіляторів Microsoft використовуйте _BitScanForward & _BitScanReverse.
Для GCC використовуйте __builtin_ffs, __builtin_clz, __builtin_ctz.

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

Вибачте, я повністю забув надати рішення. Це код, який я використовую в IPAD, який не має інструкції щодо рівня складання для цього завдання:

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

Тут потрібно розуміти, що дорога не саме порівняння, а галузь, яка виникає після порівняння. Порівняння в цьому випадку змушене до значення 0 або 1 зі значенням .. == 0, а результат використовується для об'єднання математики, яка мала б місце з будь-якої сторони гілки.

Редагувати:

Код, наведений вище, повністю порушений. Цей код працює і все ще не є відділенням (якщо оптимізовано):

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

Це повертає -1, якщо дано 0. Якщо вам не байдуже 0 або раді отримати 31 за 0, видаліть розрахунок i0, заощадивши шматок часу.


3
Я зафіксував це для вас. Не забудьте перевірити те, що ви публікуєте.
Джим Балтер

5
Як ви можете назвати це "безвіддільним", коли до нього входить термінальний оператор?
BoltBait

2
Це умовний хід. Єдина інструкція мови складання, яка приймає як параметри обидва можливі значення, і виконує mov операцію на основі оцінки умовного. І таким чином є "Відділення безкоштовно". немає переходу на іншу невідому або, можливо, неправильну адресу.
Dan

FWIW gcc генерує гілки навіть на -O3 godbolt.org/z/gcsUHd
Qix - МОНІКА ПОТРІБНО

7

Натхненний цим подібним дописом, який передбачає пошук встановленого біта, я пропоную наступне:

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

Плюси:

  • без петель
  • немає розгалуження
  • працює в постійний час
  • обробляє значення = 0, повертаючи результат в іншому випадку
  • лише два рядки коду

Мінуси:

  • передбачає невелику витривалість як закодовану (можна виправити зміною констант)
  • передбачає, що подвійний - це справжній * 8 IEEE float (IEEE 754)

Оновлення: Як зазначено в коментарях, об'єднання є більш чистим впровадженням (як мінімум для C) і виглядатиме так:

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

Це передбачає 32-бітні вставки з малоеквістичним зберіганням для всього (думаю, процесори x86).


1
Цікаво - я все ще боюся використовувати парні для бітової арифметики, але пам’ятаю, що це
peterchen

Використання frexp () може зробити його трохи більш портативним
aka.nice

1
Набір типів за допомогою покажчика покажчиків не є безпечним для C або C ++. Використовуйте memcpy в C ++ або об'єднання в C. (Або об'єднання в C ++, якщо ваш компілятор гарантує, що це безпечно. Наприклад, розширення GNU до C ++ (підтримується багатьма компіляторами) гарантують безпеку типового накладення.)
Петро Корди

1
Старіший gcc також робить кращий код з об'єднанням замість вказівника: він переходить безпосередньо з FP reg (xmm0) до rax (з movq) замість зберігання / перезавантаження. Новіші gcc та clang використовують movq обома способами. Дивіться godbolt.org/g/x7JBiL для версії об'єднання. Чи навмисно ви робите арифметичний зсув на 20? Ваші припущення також повинні перераховувати, що intє int32_t, і цей підписаний правий зсув є арифметичним зрушенням (у C ++ це визначено реалізацією)
Peter Cordes

1
Також BTW, Visual Studio (принаймні 2013) також використовує тест / setcc / sub підхід. Мені більше подобається cmp / adc.
DocMax

5

Це можна зробити в гіршому випадку менше ніж 32 операції:

Принцип: перевірка на 2 або більше біт так само ефективна, як перевірка на 1 біт.

Так, наприклад, ніщо не заважає вам перевірити, для чого групувати його спочатку, а потім перевірити кожен біт від найменшого до найбільшого в цій групі.

Отже ...
якщо ви перевіряєте 2 біти за один раз, у вас є в гіршому випадку (Nbits / 2) + 1 чек.
якщо ви перевіряєте 3 біти одночасно, у вас є в гіршому випадку (Nbits / 3) + 2 чеків.
...

Оптимальним було б перевірити в групах по 4. Що вимагатиме в гіршому випадку 11 операцій замість 32.

Найкращий випадок - від 1 перевірки алгоритмів до 2 перевірки, якщо ви використовуєте цю ідею групування. Але цей додатковий чек у кращому випадку варто того, щоб заощадити в гіршому випадку.

Примітка: я виписую його повністю, а не використовую цикл, тому що це більш ефективно.

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 від мене. Це не найшвидше, але швидше, ніж оригінал, в чому справа ...
Ендрю Грант,

@ onebyone.livejournal.com: Навіть якщо в коді була помилка, концепція групування - це пункт, на який я намагався потрапити. Фактичний зразок коду не має великого значення, і його можна зробити більш компактним, але менш ефективним.
Брайан Р. Бонді

Мені просто цікаво, чи є справді погана частина моєї відповіді, чи люди не просто так люблять, я написав це повністю?
Брайан Р. Бонді

@ onebyone.livejournal.com: Якщо ви порівнюєте 2 алгоритми, вам слід порівнювати їх такими, які вони є, не припускаючи, що один з них буде магічно перетворений на етапі оптимізації. Я ніколи не стверджував, що мій алгоритм "швидший". Тільки, що це менше операцій.
Брайан Р. Бонді

@ onebyone.livejournal.com: ... Мені не потрібно профілювати вищевказаний код, щоб знати, що це менше операцій. Я це добре бачу. Я ніколи не висловлював жодних претензій, які вимагають профілювання.
Брайан Р. Бонді

4

Чому б не використовувати двійковий пошук ? Це завжди буде завершено після 5 операцій (якщо вважати, що розмір int становить 4 байти):

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 Це дуже схоже на мою відповідь. Кращий час запуску гірший, ніж моя пропозиція, але гірший час виконання - краще.
Брайан Р. Бонді

2

Інший метод (поділ модуля та пошук) заслуговує на особливу згадку тут із того ж посилання, яке надає @ anton-tykhyy. цей метод дуже схожий за ефективністю на метод множення та пошуку DeBruijn з невеликою, але важливою відмінністю.

поділ модуля та пошук

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

метод поділу модуля та спосіб пошуку повертає різні значення для v = 0x00000000 і v = FFFFFFFF, тоді як метод множення DeBruijn і пошук пошуку повертають нуль на обох входах.

тест: -

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повільно. Натомість ви можете використовувати оригінальний метод множення та пошуку та віднімання !vз, rщоб обробити крайові регістри.
Ейтан Т

3
@EitanT оптимізатор цілком може перетворити цей мод у швидке розмноження, як у захопленні хакерів
phuclv

2

Згідно зі сторінкою шахового програмування BitScan та моїми власними вимірами, віднімання та xor швидше, ніж заперечення та маскування.

(Зауважте, ніж якщо ви збираєтеся рахувати прострочені нулі в 0, метод, як у мене є, повертається, 63тоді як негатив і маска повертаються 0.)

Ось 64-бітове віднімання та 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];

Для довідки, ось 64-розрядна версія методу negate and mask:

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

Це (v ^ (v-1))працює за умови v != 0. У випадку, коли v == 0він повертає 0xFF .... FF при цьому (v & -v)дає нуль (що, до речі, помиляється, також, принаймні, буф призводить до розумного результату).
CiaPan

@CiaPan: Це хороший момент, я це згадаю. Я здогадуюсь, є інше число Де Бреййна, яке могло б вирішити це, поставивши 0 в 63-му індексі.
jnm2

Ду, це не де питання. 0 і 0x8000000000000000 обидва призводять до 0xFFFFFFFFFFFFFFFF після v ^ (v-1), тому немає нічого спільного з ними. У моєму сценарії нуль ніколи не буде введено.
jnm2

1

Ви можете перевірити, чи встановлено будь-який біт нижчого порядку. Якщо так, то подивіться на нижній порядок решти бітів. наприклад:

32-бітний int - перевірте, чи встановлено будь-який з перших 16. Якщо так, перевірте, чи встановлено будь-який з перших 8. якщо так, ....

якщо ні, перевірте, чи встановлено будь-який із верхніх 16.

По суті це двійковий пошук.


1

Дивіться тут мою відповідь, як це зробити за допомогою однієї інструкції x86, за винятком того, що для пошуку найменш значущого набору бітів вам потрібна інструкція BSF("біт сканування вперед") замість BSRописаної там.


1

Ще одне рішення, не найшвидше, але здається цілком непоганим.
Принаймні, він не має гілок. ;)

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

щоб отримати всі 1s від найменш значущого 1 до LSB, використовуйте ((x & -x) - 1) << 1замість цього
phuclv

ще швидший шлях: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;
}

50% усіх номерів повернеться в перший рядок коду.

75% усіх номерів повернуться на перші 2 рядки коду.

87% усіх номерів повернуться в перші 3 рядки коду.

94% усіх номерів повернуться в перші 4 рядки коду.

97% усіх номерів повернеться в перші 5 рядків коду.

тощо.

Я думаю, що люди, які скаржаться на те, наскільки неефективним є найгірший сценарій цього коду, не розуміють, наскільки рідко це станеться.


3
І найгірший випадок із 32 непередбачуваних гілок :)

1
Не могли б це принаймні зробити перемикачем ...?
Стівен Лу

"Не могли б це принаймні перетворитись на перемикач ...?" Ви намагалися це зробити, перш ніж зрозуміти, що це можливо? З тих пір, коли ви можете робити розрахунки прямо на випадках перемикання? Це таблиця пошуку, а не клас.
j riv

1

Знайшов цей розумний трюк, використовуючи «магічні маски» в «Мистецтві програмування, частина 4», який робить це в час O (log (n)) для n-бітного числа. [з log (n) додатковим простором]. Типові рішення, що перевіряють встановлений біт, це або O (n), або потрібен O ​​(n) додатковий простір для таблиці пошуку, тому це хороший компроміс.

Чарівні маски:

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

Ключова ідея: Немає зворотних нулів у 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

Якщо C ++ 11 доступний для вас, компілятор іноді може виконати завдання для вас :)

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

Результат - індекс на основі 1.


1
Розумний, але він піддається катастрофічно поганій збірці, коли вхід не є константою часу компіляції. godbolt.org/g/7ajMyT . (Німий цикл над бітами з gcc або фактичний рекурсивний виклик функції з клаксом.) Gcc / clang може оцінюватись ffs()під час компіляції, тому вам не потрібно використовувати це для постійного розповсюдження для роботи. (Ви повинні уникати інлайн-асемблера, звичайно.) Якщо вам дійсно потрібно що - то , що працює як C ++ 11 constexpr, ви можете використовувати GNU C __builtin_ffs.
Пітер Кордес

0

Це стосується відповіді @Anton Tykhyy

Ось моя реалізація contexpr C ++ 11, яка усуває касти і видаляє попередження про VC ++ 17 шляхом обрізання результату на 64 біт до 32 біт:

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

Щоб подолати питання 0x1 та 0x0, що повертаються 0, ви можете зробити:

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

але якщо компілятор не може або не переробить виклик, він додасть до циклу кілька циклів.

Нарешті, якщо ви зацікавлені, ось список статичних тверджень, щоб перевірити, чи код виконує те, що призначено:

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

Ось одна проста альтернатива, навіть якщо пошук журналів трохи затратний.

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

-3

останнім часом я бачу, що прем'єр Сінгапуру розмістив програму, яку він написав у facebook, є один рядок, щоб згадати про це.

Логіка просто "значення & -значення", припустимо, у вас 0x0FF0, тоді 0FF0 & (F00F + 1), що дорівнює 0x0010, це означає, що найнижчий 1 знаходиться в 4-му біті .. :)


1
Це виокремлює найнижчий біт, але не дає тобі його позиції, і саме це питання задає.
рашимото

Я не думаю, що це також працює для пошуку останнього шматочка.
yyny

значення & ~ значення - 0.
khw

ой, у мене очі погані. Я неправильно взяв мінус на тильду.
ігноруй

-8

Якщо у вас є ресурси, ви можете принести в жертву пам'ять, щоб підвищити швидкість:

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

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

Примітка. Ця таблиця споживає щонайменше 4 ГБ (16 ГБ, якщо ми повернемо тип повернення якunsigned ). Це приклад торгівлі одним обмеженим ресурсом (ОЗУ) іншим (швидкість виконання).

Якщо ваша функція повинна залишатися портативною та працювати якомога швидше будь-якою ціною, це був би шлях. У більшості реальних програм таблиці 4 Гб нереально.


1
Діапазон вводу вже визначений типом параметра - "неподписаний" - це 32-бітове значення, так що ні, ви не добре.
Брайан

3
гм ... чи має ваша міфічна система та ОС поняття пропановану пам'ять? Скільки часу це буде коштувати?
Майкгея

14
Це невідповідь. Ваші рішення абсолютно нереалістичні для ВСІХ реальних додатків, і називати це "компромісом" є сумлінним. Вашої міфічної системи, яка має 16 ГБ оперативної пам’яті, щоб присвятити одній функції, просто не існує. Ви б так само відповідали "використовувати квантовий комп'ютер".
Брайан

3
Жертва пам'яті на швидкість? Таблиця пошуку 4 ГБ + ніколи не поміститься в кеші на будь-якій існуючій зараз машині, тому я думаю, що це, мабуть, повільніше, ніж майже всі інші відповіді тут.

1
Арг. Ця жахлива відповідь продовжує переслідувати мене :)@Dan: Ви правильні щодо кешування пам'яті. Дивіться коментар Mikeage вище.
e.James
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.