В ситуациях, когда производительность имеет первостепенное значение, компилятор C, скорее всего, не создаст самый быстрый код по сравнению с тем, что вы можете сделать с помощью настроенного вручную языка ассемблера. Я предпочитаю идти по пути наименьшего сопротивления - для таких небольших подпрограмм я просто пишу asm-код и хорошо представляю, сколько циклов потребуется для выполнения. Вы можете повозиться с кодом C и заставить компилятор генерировать хороший вывод, но в конечном итоге вы можете потратить много времени на настройку вывода таким образом. Компиляторы (особенно от Microsoft) прошли долгий путь за последние несколько лет, но они все еще не так умны, как компилятор между вашими ушами, потому что вы работаете над своей конкретной ситуацией, а не только с общим случаем. Компилятор может не использовать определенные инструкции (например, LDM), которые могут ускорить это, и это ' s вряд ли будет достаточно умен, чтобы развернуть петлю. Вот способ сделать это, который включает в себя 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:
я запустил свой код и скомпилированный 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, вероятно, дадут аналогичные результаты.