Швидко знайдіть, чи є значення в масиві C?


124

У мене є вбудована програма із критичною часовою шкалою ISR, яка потребує повторення через масив розміром 256 (краще 1024, але 256 є мінімальним) і перевірити, чи відповідає значення вмісту масивів. boolБуде встановлено в це правда так.

Мікроконтролер - це 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;
     }
}

Який був би абсолютний найшвидший спосіб зробити це? Використання вбудованої збірки дозволено. Також допускаються інші «менш елегантні» трюки.


28
Чи є спосіб зберігати значення в масиві по-різному? Якщо ви зможете їх сортувати, двійковий пошук, безумовно, буде швидшим. Якщо дані, які потрібно зберігати та шукати, знаходяться в певному діапазоні, вони можуть бути представленими з бітовою картою тощо.
Remo.D

20
@BitBank: вас здивувало б, наскільки покращилися компілятори за останні три десятиліття. ARM цікаво досить компілятор. І я знаю на факт, що ARM на GCC може видавати декілька інструкцій для завантаження (принаймні з 2009 року)
MSalters

8
дивовижне питання, люди забувають, що існують випадки реального світу, коли продуктивність має значення. занадто багато разів на подібні запитання відповідають "просто використовувати stl"
Кік

14
Заголовок "... повторити масив" вводить в оману, оскільки ви просто шукаєте задане значення. Для повторення масиву мається на увазі, що щось потрібно робити при кожному записі. Сортування, якщо вартість може бути амортизовано за багато пошукових запитів, справді є ефективним підходом, незалежним від питань впровадження мови.
хардмат

8
Ви впевнені, що не можете просто використовувати двійковий пошук або хеш-таблицю? Двійковий пошук 256 елементів == 8 порівнянь. Хеш-таблиця == 1 стрибок в середньому (або 1 стрибок максимум, якщо у вас ідеальний хеш). Ви повинні вдатися до оптимізації складання лише після того, як ви 1) мати гідний алгоритм пошуку ( O(1)або O(logN), порівняно з O(N)), і 2) ви профілювали його як вузьке місце.
Груо

Відповіді:


105

У ситуаціях, коли продуктивність має надзвичайно важливе значення, компілятор C, швидше за все, не створить найшвидший код порівняно з тим, що ви можете зробити з налаштованою мовою складання вручну. Я схильний пройти шлях найменшого опору - для таких маленьких процедур я просто записую ASM-код і добре розумію, скільки циклів знадобиться для виконання. Можливо, ви зможете поспілкуватися з кодом C і змусити компілятор створити хороший вихід, але ви, можливо, втратите багато часу, налаштувавши результат таким чином. Компілятори (особливо від Microsoft) за останні кілька років пройшли довгий шлях, але вони все ще не такі розумні, як компілятор між вашими вухами, оскільки ви працюєте над конкретною ситуацією, а не просто загальним випадком. Компілятор може не використовувати певні інструкції (наприклад, LDM), які можуть прискорити це, і це " s навряд чи буде досить розумним, щоб розкрутити цикл. Ось спосіб зробити це, який включає в себе 3 ідеї, про які я згадував у своєму коментарі: Розкручування циклу, попередній вибір кешу та використання інструкції з декількома навантаженнями (ldm). Кількість циклів інструкцій становить приблизно 3 такти на елемент масиву, але це не враховує затримок пам'яті.

Теорія роботи: Дизайн процесора ARM виконує більшість інструкцій за один тактовий цикл, але інструкції виконуються в конвеєрі. Компілятори 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

Оновлення: У коментарях є багато скептиків, які вважають, що мій досвід є анекдотичним / нікчемним і потребує доказів. Я використовував GCC 4.8 (від Android NDK 9C) для отримання наступного виводу з оптимізацією -O2 (усі оптимізації включені, включаючи розкручування циклу) ). Я склав оригінальний код С, представлений у запитанні вище. Ось що 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 для векторизації ініціалізації мого масиву, але пошук лінійних значень, написаний ОП, схожий на те, що створюється 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, ймовірно, дадуть подібні результати.


13
@ LưuVĩnhPhúc - це взагалі вірно, але жорсткі ISR є одним з найбільших винятків, оскільки ви часто знаєте набагато більше, ніж робить компілятор.
сапі

47
Адвокат диявола: чи є якісь кількісні докази того, що цей код швидший?
Олівер Чарльзворт

11
@BitBank: Це недостатньо добре. Ви повинні підкріпити свої докази доказами .
Гонки легкості на орбіті

13
Я навчився свого уроку років тому. Я створив дивовижний оптимізований внутрішній цикл для графічної програми на Pentium, використовуючи U та V труби оптимально. Зменшив це до 6 тактових циклів на цикл (обчислено і виміряно), і я дуже пишався собою. Коли я перевіряв це на тому ж самому, що написано на C, C був швидшим. Я більше ніколи не писав інший рядок асемблера Intel.
Rocketmagnet

14
"скептики в коментарях, які вважають, що мій досвід анекдотичний / нікчемний і вимагає доказів". Не сприймайте їх коментарі надто негативно. Показ доказів просто робить вашу чудову відповідь все набагато кращою.
Коді Грей

87

Існує хитрість для її оптимізації (мене про це запитали на співбесіді один раз):

  • Якщо останній запис у масиві містить значення, яке ви шукаєте, поверніть 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;
}

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


2
@ratchetfreak: OP не надає жодної інформації про те, як, де і коли цей масив розподіляється та ініціалізується, тому я дав відповідь, яка від цього не залежить.
barak manos

3
Масив знаходиться в оперативній пам'яті, але записи заборонені, хоча.
волани

1
приємно, але масив вже не є const, що робить це не безпечним для потоків. Здається, висока плата.
EOF

2
@EOF: Де constколи-небудь згадувалося у питанні?
barak manos

4
@barakmanos: Якщо я передаю вам масив і значення і запитаю, чи є це значення в масиві, я зазвичай не припускаю, що ви модифікуєте масив. Оригінальне запитання не згадує constані теми, ані тематики, але я вважаю, що справедливо згадати цей застереження.
EOF

62

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

Ідеальна хеш-функція

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

  • Зберігайте хеш-функцію досить швидко.
  • Мінімізувати n . Найменший розмір, який ви можете отримати, - це 256 (мінімально досконала хеш-функція), але цього, мабуть, важко досягти, залежно від даних.

Примітка про ефективні хеш-функції, n часто є потужністю 2, що еквівалентно побітовій масці з низьких бітів (AND AND). Приклад хеш-функцій:

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

Потім ви складаєте фіксовану таблицю з n записів, де хеш відображає вхідні значення в індекс i в таблицю. Для дійсних значень запис у таблицю i містить дійсне значення. Для всіх інших записів таблиці переконайтесь, що кожен запис індексу i містить якесь інше недійсне значення, яке не має хеш- я .

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

  1. Хеш х до індексу i (який знаходиться в діапазоні 0..n)
  2. Подивіться запис i в таблиці і побачте, чи містить вона значення x .

Це буде набагато швидше, ніж лінійний пошук 256 або 1024 значень.

Я написав код Python, щоб знайти розумні хеш-функції.

Двійковий пошук

Якщо ви сортуєте свій масив з 256 "дійсних" значень, то ви можете робити двійковий пошук , а не лінійний пошук. Це означає, що ви повинні мати змогу шукати таблицю 256 записів лише за 8 кроків ( log2(256)) або 1024 таблицю запису за 10 кроків. Знову ж таки, це буде набагато швидше, ніж лінійний пошук 256 або 1024 значень.


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

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

3
+1 для двійкового пошуку. Алгоритмічне перепроектування - найкращий спосіб оптимізації.
Rocketmagnet

Популярна думка полягає в тому, що для пошуку ефективного хеш-розпорядження потрібно занадто багато зусиль, тому "найкраща практика" - це двійковий пошук. Однак іноді "найкраща практика" недостатньо хороша. Припустимо, що ви здійснюєте маршрутизацію мережевого трафіку в той момент, коли прибув заголовок пакету (але не його корисне навантаження): використання двійкового пошуку зробить ваш продукт безнадійно повільним. Вбудовані продукти зазвичай мають такі обмеження та вимоги, що те, що є "найкращою практикою", наприклад, у середовищі виконання x86 - "простий вихід" у вбудованих.
Олоф Форшелл

60

Зберігайте таблицю в упорядкованому порядку та використовуйте непровіднаний двійковий пошук 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біти. Близько однієї сотової частини. Цей тест не має ваги!


4
Мені дуже подобається таке рішення. Він може бути модифікований для виконання у фіксованій кількості циклів, щоб уникнути криміналістичних експертиз на основі часу, якщо розташування значення є чутливою інформацією.
OregonTrail

1
@OregonTrail: Криміналістичні експертизи на основі термінів? Весела проблема, але сумний коментар.
Майк Данлаве

16
Ви бачите подібні розкручені петлі в бібліотеках крипт, щоб запобігти тимчасовим атакам en.wikipedia.org/wiki/Timing_attack . Ось хороший приклад github.com/jedisct1/libsodium/blob/… У цьому випадку ми заважаємо зловмисникові вгадати довжину рядка. Зазвичай зловмисник візьме кілька мільйонів зразків виклику функції для здійснення атаки на час.
OregonTrail

3
+1 Чудово! Гарний маленький розкручений пошук. Я цього раніше не бачив. Я можу ним скористатися.
Rocketmagnet

1
@OregonTrail: Я друкую ваш коментар на основі часу. Мені вже не раз доводилося писати криптографічний код, який виконується за фіксовану кількість циклів, щоб уникнути протікання інформації до атак на основі часу.
TonyK

16

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

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

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

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


1
Cortex-M ніде не дуже швидкий .
MSalters

2
Насправді в цьому випадку йому взагалі не потрібна хеш-таблиця. Він хоче лише знати, чи є певний ключ у наборі, він не хоче зіставляти його зі значенням. Тож достатньо, якщо досконала хеш-функція відображає кожне 32-бітове значення або 0, або 1, де "1" можна визначити як "є в наборі".
Девід Онгаро

1
Добре, якщо він зможе отримати ідеальний хеш-генератор для створення такого відображення. Але це був би "надзвичайно щільний набір"; Я дублю, що він може знайти ідеальний хеш-генератор, який це робить. Йому може бути краще спробувати отримати ідеальний хеш, який створює деяку константу K, якщо в наборі, і будь-яке значення, але K, якщо не в множині. Я підозрюю, що важко отримати ідеальний хеш навіть для останнього.
Іра Бакстер

@DavidOngaro table[PerfectHash(value)] == valueдає 1, якщо значення встановлено в наборі, і 0, якщо його немає, і є добре відомі способи створення функції PerfectHash (див., Наприклад, burtleburtle.net/bob/hash/perfect.html ). Спроба знайти хеш-функцію, яка безпосередньо відображає всі значення в наборі в 1, а всі значення, не в наборі до 0, - дурне завдання.
Джим Балтер

@DavidOngaro: досконала хеш-функція має багато "помилкових позитивів", тобто значення, які не є у наборі, мали б такий самий хеш, як значення у наборі. Тому ви повинні мати таблицю, індексовану хеш-значенням, що містить вхідне значення "in-the set". Отже, для перевірки будь-якого заданого вхідного значення ви (а) хеште його; (b) використовувати хеш-значення для пошуку таблиці; (c) перевірити, чи відповідає запис у таблиці вхідному значенню.
Крейг МакКуін

14

Використовуйте хеш-набір. Це дасть час пошуку 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);
}

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


3
Залежить від того, скільки разів потрібно зробити цей пошук, щоб він був ефективним.
maxywb

1
Так, пошук може закінчитися в кінці масиву. І такий тип лінійного хешування має високі коефіцієнти зіткнення - ні в якому разі ви не отримаєте O (1). Хороші хеш-набори не реалізуються так.
Джим Балтер

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

Ви повинні просто зафіксувати його, щоб він обгорнув i навколо.
Джим Балтер

Суть ідеальної хеш-функції полягає в тому, що вона робить один зонд. Період.
Іра Бакстер

10

У цьому випадку, можливо, варто вивчити фільтри Блума . Вони здатні швидко встановити, що значення немає, що є хорошою справою, оскільки більшість 2 ^ 32 можливих значень не є в масиві елементів 1024. Однак є деякі хибні позитиви, які потребують додаткової перевірки.

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


1
Цікаво, що я раніше не бачив фільтрів Bloom.
Rocketmagnet

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

Принаймні, варто тестувати.


1
-1, ARM має індексований режим адреси, тому це безглуздо. Що стосується створення вказівника const, GCC вже помічає, що він не змінюється. Не constдодає нічого.
MSalters

11
@MSalters ОК, я не перевіряв з генеруються кодом, точка повинна була висловити те , що робить його простіше на рівні C, і я думаю , просто управління покажчиками замість покажчика і індекс є простим. Я просто не погоджуюся з тим, що " constнічого не додає": це дуже чітко говорить читачеві, що значення не зміниться. Це фантастична інформація.
розмотайте

9
Це глибоко вбудований код; До цих пір оптимізація включала переміщення коду з флеш-пам'яті в оперативну пам'ять. І все-таки це має бути швидше. На даний момент читабельність - не мета.
MSalters

1
@MSalters "ARM має індексований адресний режим, тому це безглуздо" - ну, якщо ви повністю пропустите точку ... ОП написало: "Я також використовую арифметику вказівника і для циклу". unind не замінив індексацію покажчиками, він просто усунув змінну індексу і, таким чином, додаткове віднімання на кожну ітерацію циклу. Але ОП був мудрим (на відміну від багатьох людей, відповідаючи та коментуючи) і закінчив робити двійковий пошук.
Джим Балтер

6

Інші люди запропонували реорганізувати вашу таблицю, додавши в кінці значення дозорної або сортуючи її, щоб забезпечити двійковий пошук.

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

Перша моя порада: позбудьтеся арифметики вказівника та знижки. Начебто речі

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

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

Наприклад, вищевказаний код може бути складений у цикл, який працює від -256або -255до нуля, індексуючи &the_array[256]. Можливо, речі, які навіть не виражаються у дійсному C, але відповідають архітектурі машини, для якої ви створюєте.

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

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


Петлі над вказівниками є ідіоматичними на C, і хороші оптимізуючі компілятори можуть обробляти їх так само добре, як і індексувати. Але все це суперечить тому, що ОП закінчила двійковий пошук.
Джим Балтер

3

Тут може бути використана векторизація, як це часто відбувається в реалізаціях 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


3

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

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

EDIT

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

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


8
Як це отримує 4 оновлення? Питання стверджує, що це Cortex M4. Річ має 136 КБ оперативної пам’яті, а не 262.144 КБ.
MSalters

1
Дивовижно, скільки опитувань було надано за явно неправильні відповіді, оскільки відповідач пропустив ліс за деревами. Для найбільшого випадку ОП O (log n) << O (n).
msw

3
Мені дуже буяє програмістів, які спалюють смішні обсяги пам'яті, коли є набагато кращі рішення. Кожні 5 років здається, що на моєму ПК не вистачає пам’яті, де 5 років тому цього було багато.
Крейг МакКуїн

1
@CraigMcQueen Діти в ці дні. Витрата пам’яті. Нецензурно! Ще в мої часи ми мали 1 МБ пам’яті та розмір слова в 16 біт. / с
Коул Джонсон

2
Що з суворими критиками? ОП чітко заявляє, що швидкість є абсолютно критичною для цієї частини коду, і StephenQuan вже згадав про "смішний об'єм пам'яті".
Богдан Олександру

1

Переконайтесь, що вказівки ("псевдокод") та дані ("theArray") знаходяться в окремих (ОЗУ) пам'яті, щоб архітектура CM4 Гарвард була використана на весь свій потенціал. З посібника користувача:

введіть тут опис зображення

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


Цікаво, що у Cortex-M7 є додаткові кеші інструкцій / даних, але до цього точно не виходить. en.wikipedia.org/wiki/ARM_Cortex-M#Silicon_customization .
Пітер Кордес

0

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

1) ви могли взагалі видалити лічильник 'я' - просто порівняйте покажчики, тобто

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:

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

Друга частина цієї оптимізації полягає в тому, що цей елемент масиву береться за прямою адресою (обчислюється на етапі компіляції, переконайтеся, що ви використовуєте статичний масив), і не потрібні додаткові опції ADD для обчислення покажчика від базової адреси масиву. Ця оптимізація може не мати суттєвого ефекту, оскільки архітектура AFAIK ARM має особливі функції для прискорення адреси масивів. Але в будь-якому випадку завжди краще знати, що ти зробив все найкраще просто в коді С, прямо так?

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

Якщо ви вважаєте, що відкат масиву з 1024 елементів є занадто великою жертвою для вашого випадку, ви можете вважати "частковий відкат", наприклад, ділення масиву на 2 частини по 512 елементів кожна, або 4x256 тощо.

3) сучасний процесор часто підтримує SIMD ops, наприклад, набір інструкцій ARM NEON - він дозволяє виконувати ті ж операційні системи паралельно. Відверто кажучи, я не пам'ятаю, чи підходить вона для порівняння, але я вважаю, що це може бути, ви повинні це перевірити. Googling показує, що можуть бути і деякі хитрощі, щоб отримати максимальну швидкість, див. Https://stackoverflow.com/a/5734019/1028256

Я сподіваюся, що це може дати вам нові ідеї.


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

@Jim, очевидно, що таку оптимізацію слід зробити першою. Відповіді "дурні" можуть виглядати не настільки нерозумно в деяких випадках використання, наприклад, якщо ви не встигнете сортувати масив. Або якщо швидкість, яку ви отримуєте, все одно недостатня
Міксаз

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

@JimBalter, якби у мене була така проблема, як ОП, я, звичайно, подумав би про використання альгів, таких як двійковий пошук чи щось. Я просто не міг подумати, що ОП це вже не врахував. "у вас немає часу на сортування масиву" означає, що сортування масиву вимагає часу. Якщо вам потрібно зробити це для кожного набору вхідних даних, це може зайняти більше часу, ніж лінійний цикл. "Або якщо швидкість, яку ви отримуєте, все одно недостатня" означає наступне - підказки щодо оптимізації можна використати для прискорення двійкового коду пошуку або чого б то не було
Mixaz

0

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

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

Я створив таку програму, про яку можна прочитати в цій публікації та досягнув дуже швидких результатів. 16000 записів перекладається приблизно в 2 ^ 14 або в середньому 14 порівнянь, щоб знайти значення за допомогою двійкового пошуку. Я явно мав на меті дуже швидкі пошуки - в середньому знаходження значення <= 1,5 пошуку, що призвело до більших вимог оперативної пам'яті. Я вважаю, що при більш консервативному середньому значенні (скажімо, <= 3) можна було б зберегти багато пам'яті. Для порівняння середній випадок для двійкового пошуку ваших 256 або 1024 записів призведе до середньої кількості порівнянь 8 і 10 відповідно.

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

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


0

Це більше схоже на додаток, ніж на відповідь.

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

У половині з них значення, яке шукається, НЕ було присутнє в масиві. Тоді я зрозумів, що можу застосувати "фільтр" перед будь-яким пошуком.

Цей "фільтр" - це просто просте ціле число, обчислене ONCE і використовується в кожному пошуку.

Це на Java, але це досить просто:

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

Отже, перш ніж здійснити двійковий пошук, я перевіряю бінарний фільтр:

// 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 ...

}

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.