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