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

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

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

Микроконтроллер представляет собой ядро NXP LPC4357, ARM Cortex M4, а компилятор - GCC. Я уже объединил уровень оптимизации 2 (3 медленнее) и поместил функцию в оперативную память вместо флэш-памяти. Я также использую арифметику указателей и цикл 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;
     }
}

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

4b9b3361

Ответ 1

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

Теория работы: Конструкция процессора ARM выполняет большинство инструкций за один такт, но инструкции выполняются в конвейере. Компиляторы C попытаются устранить задержки трубопровода путем чередования других инструкций между ними. При представлении жесткой петли, такой как исходный код C, компилятору будет трудно скрыть задержки, потому что значение, считанное из памяти, должно быть немедленно сравнено. Мой код ниже чередуется между двумя наборами из 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

Update: В комментариях есть много скептиков, которые считают, что мой опыт анекдотичен/бесполезен и требует доказательств. Я использовал 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 (Surface RT, Surface RT 2). Я выполнил 1000000 итераций цикла, который не смог найти совпадение, чтобы все было в кеше и его легко измерить.

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

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

Ответ 2

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

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

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

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


UPDATE:

Если вам разрешено выделять массив 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;
}

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

Ответ 3

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

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

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

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

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

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

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

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

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

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

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

Двоичный поиск

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

Ответ 4

Сохраните таблицу в порядке сортировки и используйте развернутый двоичный поиск 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]);

Точка,

  • Если вы знаете, насколько велика таблица, тогда вы знаете, сколько итераций будет, так что вы можете полностью развернуть ее.
  • Тогда нет точечного тестирования для случая == на каждой итерации, потому что, за исключением последней итерации, вероятность этого случая слишком мала, чтобы оправдать трафик времени для него. **
  • Наконец, расширяя таблицу до степени 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 бит. Около сотых бит. Этот тест не несет своего веса!

Ответ 5

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

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

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

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

Ответ 6

Используйте хэш-набор. Это даст время поиска 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);
}

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

Ответ 7

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

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

Ответ 8

Предполагая, что ваш процессор работает на частоте 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;
}

Это, по крайней мере, стоит проверить.

Ответ 9

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

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

Мой первый совет: избавиться от арифметики указателя и подсчета очков. Материал вроде

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

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

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

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

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

Ответ 10

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

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

ИЗМЕНИТЬ

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

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

Ответ 11

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

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

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

Ответ 12

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

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) Как уже упоминалось в других ответах, почти все современные процессоры основаны на 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 отсутствует в массиве), поэтому он как можно быстрее (конечно, не считая оптимизации алгоритмов, таких как хеш-таблицы, отсортированные массивы и и так далее, упомянутые в других ответах, которые могут дать лучшие результаты в зависимости от размера массива. Циклы Откат подход может применяться там также, кстати. Я пишу здесь об этом, я думаю, что не видел в других)

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

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

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

3) современный процессор часто поддерживает SIMD-операции, например, набор инструкций ARM NEON - он позволяет выполнять одни и те же операции параллельно. Честно говоря, я не помню, подходит ли это для сравнения ops, но я чувствую, что это возможно, вы должны это проверить. Googling показывает, что могут быть и некоторые трюки, чтобы получить максимальную скорость, см. fooobar.com/questions/49157/...

Надеюсь, он может дать вам несколько новых идей.

Ответ 13

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

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

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

Этот "фильтр" представляет собой простое целое число, рассчитанное ОДИН РАЗ и используемое при каждом поиске.

Это на 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 check it out
    // ... do binary search stuff ...

}

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

Ответ 14

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

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

Я создал такую ​​программу, о которой вы можете прочитать в этом сообщении, и добился очень быстрых результатов. 16000 записей переводится примерно в 2 ^ 14 или в среднем 14 сравнений, чтобы найти значение, используя двоичный поиск. Я явно стремился к очень быстрому поиску - в среднем находил значение в <= 1.5 lookups, что привело к увеличению требований к ОЗУ. Я считаю, что с более консервативным средним значением (например, = 3) можно было бы сохранить большую память. Для сравнения, средний случай для двоичного поиска на ваших 256 или 1024 элементах приведет к среднему количеству сравнений 8 и 10 соответственно.

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

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

Ответ 15

Убедитесь, что инструкции ("псевдокод") и данные ("theArray") находятся в отдельной (RAM) памяти, чтобы архитектура CM4 Harvard использовалась в полной мере. Из руководства пользователя:

enter image description here

Для оптимизации производительности процессора ARM Cortex-M4 имеет три шины для доступа к командам (код) (I), доступа к данным (D) и доступа к системе (S). Когда инструкции и данные хранятся в отдельной памяти, доступ к коду и данным может осуществляться параллельно за один цикл. Когда код и данные хранятся в одной и той же памяти, инструкции по загрузке или хранению данных могут занять два цикла.