Позиция установленного младшего бита


121

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

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

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

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

Есть идеи, как выжать из него несколько циклов?

(Примечание: этот вопрос предназначен для людей, которым нравятся подобные вещи, а не для людей, которые говорят мне, что xyzоптимизация - зло.)

[править] Спасибо всем за идеи! Я узнал и кое-что еще. Прохладно!


while ((значение _N >> (++ pos))! = 0);
Thomas

Ответы:


170

Bit 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, но по модулю очень медленно по сравнению с умножением на современном оборудовании. Так что я бы не назвал это лучшим решением.
Apriori

2
Мне кажется, что оба значения 0x01 и 0x00 приводят к значению 0 из массива. Очевидно, этот трюк будет указывать на то, что установлен самый младший бит, если передан 0!
abelenky

80

Почему бы не использовать встроенную ffs ? (Я взял справочную страницу из Linux, но она более доступна, чем это.)

ffs (3) - справочная страница 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
К вашему сведению, это компилируется в соответствующую команду сборки, если она доступна.
Жереми

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 / upper-set-bit, чтобы найти самый младший установленный бит, если нет явной инструкции для этого.

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


18

Множество решений, а не ориентир. Вам должно быть стыдно ;-)

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

9
Тесты как для де Брюйна, так и для поиска могут вводить в заблуждение - сидя в таком тесном цикле, после первой операции таблицы поиска для каждого типа будут закреплены в кэше 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 мопов 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-битных записей) должна содержать индекс младшего разряда для каждого числа в диапазоне 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
Любая таблица поиска увеличивает вероятность промаха кеша и может повлечь за собой затраты на доступ к памяти, которые могут быть на несколько порядков выше, чем выполнение инструкций.
Mehrdad Afshari

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

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

13

OMG это только что пошло по спирали.

Чего не хватает большинству этих примеров, так это небольшого понимания того, как работает все оборудование.

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

Рассмотрим простой цикл while вверху. Предполагается, что нужно оставаться в рамках цикла. Он будет ошибаться хотя бы один раз, когда выйдет из цикла. Это ПРОМЫВИТ трубу инструкций. Такое поведение немного лучше, чем предполагать, что он выйдет из цикла, и в этом случае он будет очищать конвейер команд на каждой итерации.

Количество потерянных циклов ЦП сильно варьируется от одного типа процессора к другому. Но вы можете ожидать от 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 на основе оценки условия. Таким образом, это "Branch Free". нет перехода к другому неизвестному или, возможно, неверному адресу.
Дэн

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, возвращая результат, выходящий за пределы допустимого диапазона
  • всего две строчки кода

Минусы:

  • предполагает небольшой порядок следования байтов в кодировке (можно исправить, изменив константы)
  • предполагает, что double - это настоящий * 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 ++ (поддерживаемые многими компиляторами) действительно гарантируют безопасность объединения типов)
Питер Cordes

1
Более старый gcc также делает более качественный код с объединением вместо приведения указателя: он перемещается напрямую от FP reg (xmm0) к rax (с movq) вместо сохранения / перезагрузки. Новые gcc и clang используют movq в обоих направлениях. См. Godbolt.org/g/x7JBiL для версии для объединения. Вы намеренно делаете арифметический сдвиг на 20? В ваших предположениях также должно быть указано, что intесть int32_t, и этот знаковый сдвиг вправо является арифметическим сдвигом (в C ++ он определяется реализацией)
Питер Кордес

1
Кроме того, кстати, Visual Studio (по крайней мере, 2013) также использует подход test / setcc / sub. Сам мне больше нравится cmp / adc.
DocMax 05

5

Это может быть сделано в худшем случае менее 32 операций:

Принцип: проверка 2 или более бит так же эффективна, как проверка 1 бита.

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

Итак ...
если вы проверяете 2 бита за раз, у вас в худшем случае (Nbits / 2) + 1 проверка всего.
если вы проверяете 3 бита за раз, у вас в худшем случае (Nbit / 3) + 2 проверки всего.
...

Оптимальным было бы проверять группами по 4. Что потребует в худшем случае 11 операций вместо ваших 32.

Если вы используете эту идею группировки, лучший вариант - от 1 проверки ваших алгоритмов до 2 проверок. Но этот лишний 1 чек в лучшем случае того стоит в худшем случае.

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

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. Этот метод очень похож по производительности на метод умножения и поиска ДеБрюйна с небольшим, но важным отличием.

деление по модулю и поиск

 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обрабатывать крайние случаи.
Eitan T

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-битная версия метода отрицания и маски:

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

Вы можете проверить, установлен ли какой-либо из младших битов. Если да, то посмотрите на младшие оставшиеся биты. например,:

32bit 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 или фактический рекурсивный вызов функции с помощью clang.) Gcc / clang может выполнять оценку ffs()во время компиляции, поэтому вам не нужно использовать это для работы с постоянным распространением. (Вы должны избегать инлайн-ассемблера, конечно.) Если вам действительно нужно что - то , что работает как C ++ 11 constexpr, вы можете использовать GNU C __builtin_ffs.
Питер Кордес

0

Это по поводу ответа @Anton Tykhyy

Вот моя реализация constexpr на 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

Недавно я увидел, что премьер Сингапура разместил свою программу на фейсбуке, есть одна строчка, чтобы упомянуть об этом ...

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


1
Это изолирует самый младший бит, но не дает вам его позиции, о которой спрашивает этот вопрос.
Рашимото

Я не думаю, что это работает и для поиска последней части.
yyny

значение & ~ значение равно 0.
кВтч

упс, у меня плохо с глазами. Я принял минус за тильду. не
обращайте

-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
Диапазон ввода уже определен типом параметра - 'unsigned' - это 32-битное значение, поэтому нет, вы не в порядке.
Брайан,

3
ммм ... у вашей мифической системы и ОС есть концепция выгружаемой памяти? Сколько это будет стоить времени?
Mikeage

14
Это не ответ. Ваше решение совершенно нереалистично во ВСЕХ реальных приложениях, и называть его «компромиссом» неискренне. Вашей мифической системы, у которой есть 16 ГБ оперативной памяти для одной функции, просто не существует. Вы бы также ответили «используйте квантовый компьютер».
Брайан

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

1
Argh. Этот ужасный ответ продолжает преследовать меня :)@Dan: Вы правы насчет кеширования памяти. См. Комментарий Майкиджа выше.
e.James
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.