Быстро найти, присутствует ли значение в массиве с?

у меня есть встроенное приложение с критическим по времени ISR, которое должно перебирать массив размером 256 (предпочтительно 1024, но 256 является минимальным) и проверять, соответствует ли значение содержимому массивов. А bool будет установлено значение true, это так.

микроконтроллер представляет собой ядро NXP LPC4357, ARM Cortex M4, а компилятор-GCC. У меня уже есть комбинированный уровень оптимизации 2 (3 медленнее) и размещение функции в ОЗУ вместо flash. Я также использую указатель арифметика и А for цикл, который делает подсчет вниз, а не вверх (проверка, если i!=0 - это быстрее, чем проверка, если i<256). В целом, я получаю продолжительность 12,5 МКС, которая должна быть резко сокращена, чтобы быть осуществимой. Это (псевдо) код, который я использую сейчас:

uint32_t i;
uint32_t *array_ptr = &theArray[0];
uint32_t compareVal = 0x1234ABCD;
bool validFlag = false;

for (i=256; i!=0; i--)
{
    if (compareVal == *array_ptr++)
    {
         validFlag = true;
         break;
     }
}

каким будет самый быстрый способ сделать это? Использование встроенной сборки разрешено. Допускаются и другие "менее элегантные" трюки.

14 ответов


в ситуациях, когда производительность имеет первостепенное значение, компилятор C, скорее всего, не будет производить самый быстрый код по сравнению с тем, что вы можете сделать с настроенным вручную языком сборки. Я склонен идти по пути наименьшего сопротивления - для небольших процедур, подобных этой, я просто пишу код asm и хорошо представляю, сколько циклов потребуется для выполнения. Вы можете возиться с кодом C и заставить компилятор генерировать хороший вывод, но вы можете потратить много времени на настройку вывода такой образ. Компиляторы (особенно от Microsoft) прошли долгий путь за последние несколько лет, но они все еще не так умны, как компилятор между вашими ушами, потому что вы работаете над своей конкретной ситуацией, а не только над общим случаем. Компилятор может не использовать определенные инструкции (например, LDM), которые могут ускорить это, и вряд ли он будет достаточно умен, чтобы развернуть цикл. Вот способ сделать это, который включает в себя 3 идеи, упомянутые в моем комментарии: развертывание цикла, предварительная выборка кэша и использование инструкции множественной нагрузки (ldm). Количество циклов инструкций составляет около 3 часов на элемент массива, но это не учитывает задержки памяти.

принцип работы: дизайн процессора ARM выполняет большинство инструкций за один такт, но инструкции выполняются в конвейере. Компиляторы C будут пытаться устранить задержки конвейера, чередуя другие инструкции между ними. При представлении с плотной петлей, как оригинал C код, компилятору будет трудно скрыть задержки, потому что значение, считанное из памяти, должно быть немедленно сопоставлено. Мой код ниже чередуется между 2 наборами из 4 регистров, чтобы значительно уменьшить задержки самой памяти и конвейера, извлекающего данные. Как правило, при работе с большими наборами данных, когда ваш код не использует большинство или все доступные регистры, вы не получаете максимальную производительность.

; r0 = count, r1 = source ptr, r2 = comparison value

   stmfd sp!,{r4-r11}   ; save non-volatile registers
   mov r3,r0,LSR #3     ; loop count = total count / 8
   pld [r1,#128]
   ldmia r1!,{r4-r7}    ; pre load first set
loop_top:
   pld [r1,#128]
   ldmia r1!,{r8-r11}   ; pre load second set
   cmp r4,r2            ; search for match
   cmpne r5,r2          ; use conditional execution to avoid extra branch instructions
   cmpne r6,r2
   cmpne r7,r2
   beq found_it
   ldmia r1!,{r4-r7}    ; use 2 sets of registers to hide load delays
   cmp r8,r2
   cmpne r9,r2
   cmpne r10,r2
   cmpne r11,r2
   beq found_it
   subs r3,r3,#1        ; decrement loop count
   bne loop_top
   mov r0,#0            ; return value = false (not found)
   ldmia sp!,{r4-r11}   ; restore non-volatile registers
   bx lr                ; return
found_it:
   mov r0,#1            ; return true
   ldmia sp!,{r4-r11}
   bx lr

обновление: Есть много скептиков в комментариях, которые думают, что мой опыт является анекдотическим/бесполезным и требует доказательств. Я использовал GCC 4.8 (из Android NDK 9C) для генерации следующего вывода с оптимизацией-O2 (все оптимизации включены в том числе развертывание циклов). Я скомпилировал исходный код C, представленный в вопросе выше. Вот что произвел GCC:

.L9: cmp r3, r0
     beq .L8
.L3: ldr r2, [r3, #4]!
     cmp r2, r1
     bne .L9
     mov r0, #1
.L2: add sp, sp, #1024
     bx  lr
.L8: mov r0, #0
     b .L2

выход GCC не только не разворачивает цикл, но и тратит часы на стойле после LDR. Это требует не менее 8 часов на элемент массива. Он хорошо использует адрес, чтобы знать, когда выходить из цикла, но все волшебные вещи, которые могут делать компиляторы, нигде не найдены в этом коде. Я не запускал код на целевой платформе (у меня его нет), но любой, кто имеет опыт работы с кодом ARM, может видеть, что мой код быстрее.

обновление 2: Я дал Microsoft Visual Studio 2013 SP2 шанс сделать лучше с кодом. Он смог использовать NEON инструкции по векторизации моей инициализации массива, но линейный поиск значений, написанный OP, вышел похожим на то, что сгенерировал GCC (я переименовал метки, чтобы сделать его более читаемым):

loop_top:
   ldr  r3,[r1],#4  
   cmp  r3,r2  
   beq  true_exit
   subs r0,r0,#1 
   bne  loop_top
false_exit: xxx
   bx   lr
true_exit: xxx
   bx   lr

как я уже сказал, У меня нет точного оборудования OP, но я буду тестировать производительность на nVidia Tegra 3 и Tegra 4 из 3 разных версий и скоро опубликую результаты здесь.

обновление 3: Я запустил свой код и скомпилированный код Microsoft ARM на Tegra 3 и Tegra 4 (поверхность RT, поверхность RT 2). Я запустил 1000000 итераций цикла, который не может найти совпадение, чтобы все было в кэше, и его легко измерить.

             My Code       MS Code
Surface RT    297ns         562ns
Surface RT 2  172ns         296ns  

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


есть трюк для его оптимизации (меня однажды спросили об этом на собеседовании):

  • если последняя запись в массиве содержит значение, которое вы ищете, то верните true
  • напишите значение, которое вы ищете, в последнюю запись в массиве
  • повторите массив, пока не встретите значение, которое вы ищете
  • если вы столкнулись с ним до последней записи в массиве, то верните true
  • вернуться ложные

bool check(uint32_t theArray[], uint32_t compareVal)
{
    uint32_t i;
    uint32_t x = theArray[SIZE-1];
    if (x == compareVal)
        return true;
    theArray[SIZE-1] = compareVal;
    for (i = 0; theArray[i] != compareVal; i++);
    theArray[SIZE-1] = x;
    return i != SIZE-1;
}

это дает одну ветвь на итерацию вместо двух ветвей на итерацию.


обновление:

Если вам разрешено выделять массив в SIZE+1, то вы можете избавиться от" последней записи замены " часть:

bool check(uint32_t theArray[], uint32_t compareVal)
{
    uint32_t i;
    theArray[SIZE] = compareVal;
    for (i = 0; theArray[i] != compareVal; i++);
    return i != SIZE;
}

вы также можете избавиться от дополнительной арифметики, встроенной в theArray[i], используя вместо этого следующее:

bool check(uint32_t theArray[], uint32_t compareVal)
{
    uint32_t *arrayPtr;
    theArray[SIZE] = compareVal;
    for (arrayPtr = theArray; *arrayPtr != compareVal; arrayPtr++);
    return arrayPtr != theArray+SIZE;
}

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


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

идеальная хэш-функция

если ваши 256 "допустимых" значений статичны и известны во время компиляции, вы можете использовать идеальный хэш функция. Вам нужно найти хэш-функцию, которая сопоставляет ваше входное значение со значением в диапазоне 0..n, где нет конфликты для всех допустимых значений, о которых вы заботитесь. То есть нет двух" допустимых " значений хэша для одного и того же выходного значения. При поиске хорошей хэш-функции, вы стремитесь:

  • держите хэш-функцию достаточно быстро.
  • свернуть n. Самым маленьким вы можете получить 256 (минимальный идеальный хэш функция), но это, вероятно, трудно достичь, в зависимости от данных.

Примечание Для эффективной хэш-функции n часто сила 2, которая эквивалентна побитовой маске низких битов (и операции). Пример хэш-функций:

  • CRC входных байтов, по модулю n.
  • ((x << i) ^ (x >> j) ^ (x << k) ^ ...) % n (выбирать столько i, j, k, ... при необходимости, с левыми или правыми сдвигами)

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

затем в вашей процедуре прерывания, с вводом x:

  1. хэш x для индекса я (который находится в диапазоне 0..n)
  2. посмотреть запись я в таблице и посмотреть, содержит ли он значение x.

это будет намного быстрее, чем линейный поиск 256 или 1024 значений.

Я написан какой-либо Python-код найти разумные хэш-функции.

бинарный поиск

если вы отсортировать массив из 256 "допустимых" значений, то вы можете сделать бинарный поиск, а не линейный поиск. Это означает, что вы должны иметь возможность искать таблицу 256-entry всего за 8 шагов (log2(256)), или таблица 1024-entry в 10 шагах. Опять же, это будет гораздо быстрее, чем линейный поиск 256 или 1024 значений.


держите таблицу в отсортированном порядке и используйте развернутый двоичный поиск Bentley:

i = 0;
if (key >= a[i+512]) i += 512;
if (key >= a[i+256]) i += 256;
if (key >= a[i+128]) i += 128;
if (key >= a[i+ 64]) i +=  64;
if (key >= a[i+ 32]) i +=  32;
if (key >= a[i+ 16]) i +=  16;
if (key >= a[i+  8]) i +=   8;
if (key >= a[i+  4]) i +=   4;
if (key >= a[i+  2]) i +=   2;
if (key >= a[i+  1]) i +=   1;
return (key == a[i]);

суть в том, что

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

** если вы не привыкли думать в терминах вероятностей, каждая точка принятия решения имеет энтропия, что является средней информацией, которую вы узнаете, выполнив ее. Для >= тесты, вероятность каждой ветви составляет около 0,5, а-log2 (0,5) - 1, так что если вы берете одну ветку, вы узнаете 1 бит, и если вы берете другую ветвь, которую изучаете один бит, и среднее-это просто сумма того, что вы изучаете на каждой ветви, умноженная на вероятность этой ветви. Так что 1*0.5 + 1*0.5 = 1, Так что энтропия >= тест равен 1. Поскольку у вас есть 10 бит, чтобы учиться, требуется 10 ветвей. Вот почему так быстро!

С другой стороны, что если ваш первый тест if (key == a[i+512)? Вероятность быть истинным равна 1/1024, в то время как вероятность ложного равна 1023/1024. Так что, если это правда, вы узнаете все 10 бит! Но если это ложь вы узнаете-log2(1023/1024) = .00141 бит, практически ничего! Так средняя сумма, которую вы узнаете из этого теста 10/1024 + .00141*1023/1024 = .0098 + .00141 = .0112 бит. около сотой части немного. Этот тест не носить свой вес!


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

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

идеальное хеширование-это схема "1-probe max". Можно обобщить эту идею, полагая, что следует торговать простотой вычисления хэш-кода со временем, необходимым для создания K зондов. В конце концов, цель - "наименьшее общее время поиска", а не наименьшее количество зондов или простейшая хеш-функция. Однако я никогда никого не видел. постройте алгоритм хеширования k-probes-max. Я подозреваю, что это можно сделать, но это скорее исследование.

еще одна мысль: если ваш процессор очень быстрый, один зонд в память от идеального хэша, вероятно, доминирует во время выполнения. Если процессор не очень быстрый, то зонды k>1 могут быть практичными.


используйте хэш-набор. Это даст O (1) время поиска.

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

#define HASH(x) (((x >> 16) ^ x) & 1023)
#define HASH_LEN 1024
uint32_t my_hash[HASH_LEN];

int lookup(uint32_t value)
{
    int i = HASH(value);
    while (my_hash[i] != 0 && my_hash[i] != value) i = (i + 1) % HASH_LEN;
    return i;
}

void store(uint32_t value)
{
    int i = lookup(value);
    if (my_hash[i] == 0)
       my_hash[i] = value;
}

bool contains(uint32_t value)
{
    return (my_hash[lookup(value)] == value);
}

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


в этом случае, возможно, стоит исследовать Блум фильтры. Они способны быстро установить, что значение отсутствует, что хорошо, так как большинство из 2^32 возможных значений не находятся в этом массиве 1024 элементов. Однако есть некоторые ложные срабатывания, которые потребуют дополнительной проверки.

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


предполагая, что ваш процессор работает на 204 МГц, что кажется максимальным для LPC4357, а также предполагая, что ваш результат синхронизации отражает средний случай (половина пройденного массива), мы получаем:

  • частота процессора: 204 МГц
  • период цикла: 4,9 НС
  • продолжительность циклов: 12,5 МКС / 4,9 НС = 2551 цикл
  • тактов на итерацию: 2551 / 128 = 19.9

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

Я бы рекомендовал удалить индекс и использовать сравнение указателей вместо этого и сделать все указатели const.

bool arrayContains(const uint32_t *array, size_t length)
{
  const uint32_t * const end = array + length;
  while(array != end)
  {
    if(*array++ == 0x1234ABCD)
      return true;
  }
  return false;
}

это по крайней мере стоит попробовать.


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

вы заявляете: "Я также использую арифметику указателя и цикл for, который делает подсчет вниз, а не вверх (проверка, если i != 0 быстрее, чем проверка, если i < 256)."

мой первый совет: избавьтесь от арифметики указателей и вниз. Такие вещи, как

for (i=0; i<256; i++)
{
    if (compareVal == the_array[i])
    {
       [...]
    }
}

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

например, приведенный выше код может быть скомпилирован в петлю бежит от -256 или -255 к нулю, от индексации &the_array[256]. Возможно, материал, который даже не выражается в допустимом C, но соответствует архитектуре машины, для которой вы генерируете.

так не microoptimize. Вы просто бросаете гаечные ключи в работу вашего оптимизатора. Если вы хотите быть умным, работайте над структурами данных и алгоритмами, но не микрооптимизируйте их выражение. Он просто вернется, чтобы укусить вас, если не на текущем компиляторе / архитектуре, то на следующий.

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


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

  1. создайте маску повторяющегося запроса, равную по длине количеству бит вашей ОС (64-бит, 32-бит и т. д.). В 64-разрядной системе 32-разрядный запрос повторяется дважды.

  2. обработайте список как список из нескольких частей данных сразу, просто приведя список в список большего типа данных и вытащив значения. Для каждого куска XOR с маской, затем XOR с 0b0111...1, затем добавьте 1, затем & с маской 0b1000...0 повторение. Если результат равен 0, то определенно нет совпадения. В противном случае может (обычно с очень высокой вероятностью) быть совпадение, поэтому поиск куска обычно.

пример реализации: https://sourceware.org/cgi-bin/cvsweb.cgi/src/newlib/libc/string/memchr.c?rev=1.3&content-type=text/x-cvsweb-markup&cvsroot=src


Если вы можете разместить домен своих значений с помощью объем доступной памяти для вашего приложения, то самым быстрым решением было бы представить Ваш массив в виде массива битов:

bool theArray[MAX_VALUE]; // of which 1024 values are true, the rest false
uint32_t compareVal = 0x1234ABCD;
bool validFlag = theArray[compareVal];

редактировать

Я поражен количеством критиков. Название этого потока - " Как быстро найти, присутствует ли значение в массиве C?" для которого я буду стоять на своем ответе, потому что он отвечает точно что. Я мог бы утверждать, что это имеет наиболее эффективную хэш-функцию скорости (так как address === value). Я читал комментарии, и я знаю об очевидных предостережениях. Несомненно, эти предостережения ограничивают круг проблем, которые можно использовать для решения, но для тех проблем, которые он решает, он решает очень эффективно.

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


извините, если мой ответ уже был дан ответ - просто я ленивый читатель. Не стесняйтесь downvote тогда ))

1) Вы можете удалить счетчик " i " вообще - просто сравните указатели, т. е.

for (ptr = &the_array[0]; ptr < the_array+1024; ptr++)
{
    if (compareVal == *ptr)
    {
       break;
    }
}
... compare ptr and the_array+1024 here - you do not need validFlag at all.

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

2) Как уже упоминалось в других ответах, почти все современные CPU основаны на RISC, например ARM. Даже современные процессоры Intel X86 используйте ядра RISC внутри, насколько я знаю (компиляция из X86 на лету). Основной оптимизацией для RISC является оптимизация конвейера (а также для Intel и других процессоров), минимизация скачков кода. Одним из видов такой оптимизации (вероятно, основным) является "откат цикла". Это невероятно глупо и эффективно, даже компилятор Intel может сделать это AFAIK. Это выглядит так:

if (compareVal == the_array[0]) { validFlag = true; goto end_of_compare; }
if (compareVal == the_array[1]) { validFlag = true; goto end_of_compare; }
...and so on...
end_of_compare:

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

вторая часть этой оптимизации заключается в том, что элемент массива принимается по прямому адресу (расчет на этапе компиляции, убедитесь, что вы используете статический массив), и не нужно дополнительно добавлять op для вычисления указателя из базового адреса массива. Эта оптимизация может не иметь существенного эффекта, так как архитектура AFAIK ARM имеет специальные функции для ускорения адресации массивов. Но в любом случае всегда лучше знать, что вы сделали все лучшее только в C-коде напрямую, верно?

откат цикла может выглядеть неудобно из - за потери ПЗУ (да, вы правильно разместили его в быстрой части ОЗУ, если ваша плата поддерживает эту функцию), но на самом деле это справедливая плата за скорость, основанная на концепции RISC. Это просто общая точка оптимизации расчета-вы жертвуете пространством ради скорости, и наоборот, в зависимости от ваших требований.

Если вы считаете, что откат для массива из 1024 элементов слишком велик для вашего случая, вы можете рассмотреть "частичный откат", например, разделив массив на 2 части по 512 элементов каждый или 4x256 и так далее.

3) современный процессор часто поддерживает SIMD ops, например ARM NEON instruction set-позволяет выполнять одни и те же операции параллельно. Честно говоря, я не помню, подходит ли он для сравнения, но я чувствую, что это может быть, вы должны проверить это. Googling показывает, что могут быть и некоторые трюки, чтобы получить максимальную скорость, см. https://stackoverflow.com/a/5734019/1028256

Я надеюсь, это может дать вам некоторые новые идеи.


Я большой поклонник хеширования. Проблема, конечно, заключается в том, чтобы найти эффективный алгоритм, который является быстрым и использует минимальный объем памяти (особенно на встроенном процессоре).

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

Я создал такую программу, о которой вы можете прочитать в этот пост и достигнуты очень быстрые результаты. 16000 записей переводится примерно в 2^14 или в среднем 14 сравнений, чтобы найти значение с помощью двоичного поиска. Я явно стремился к очень быстрым поискам - в среднем находя значение в

мой средний поиск требовал около 60 циклов (на ноутбуке с intel i5) с общим алгоритмом (с использованием одного деления на переменную) и 40-45 циклов со специализированным (возможно, с использованием умножения). Это должно перевести в субмикросекундные времена поиска на вашем MCU, в зависимости от тактовой частоты, на которой он выполняется.

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


Это больше похоже на добавление, чем на ответ.

Я как случай в прошлом, но мой массив был постоянным в течение значительного количества поисков.

в половине из них, искомое значение отсутствует в массиве. Затем я понял, что могу применить "фильтр" перед выполнением любого поиска.

этот "фильтр" - это просто целое число, вычисляемое один раз и используется в каждом поиске.

это на Java, но это довольно просто:

binaryfilter = 0;
for (int i = 0; i < array.length; i++)
{
    // just apply "Binary OR Operator" over values.
    binaryfilter = binaryfilter | array[i];
}

Итак, прежде чем выполнять двоичный поиск, я проверяю binaryfilter:

// Check binaryfilter vs value with a "Binary AND Operator"
if ((binaryfilter & valuetosearch) != valuetosearch)
{
    // valuetosearch is not in the array!
    return false;
}
else
{
    // valuetosearch MAYBE in the array, so let's check it out
    // ... do binary search stuff ...

}

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