Em situações em que o desempenho é de extrema importância, o compilador C provavelmente não produzirá o código mais rápido comparado ao que você pode fazer com a linguagem assembly ajustada manualmente. Costumo seguir o caminho de menor resistência - para rotinas pequenas como essa, apenas escrevo código ASM e tenho uma boa idéia de quantos ciclos serão necessários para executar. Você pode mexer no código C e fazer com que o compilador gere uma boa saída, mas pode acabar perdendo muito tempo ajustando a saída dessa maneira. Os compiladores (especialmente da Microsoft) percorreram um longo caminho nos últimos anos, mas ainda não são tão inteligentes quanto o compilador entre seus ouvidos, porque você está trabalhando em uma situação específica e não apenas em um caso geral. O compilador pode não fazer uso de determinadas instruções (por exemplo, LDM) que podem acelerar isso, e é improvável que seja inteligente o suficiente para desenrolar o loop. Aqui está uma maneira de fazê-lo, que incorpora as três idéias que mencionei no meu comentário: Desenrolamento de loop, pré-busca de cache e uso da instrução de carregamento múltiplo (ldm). A contagem do ciclo de instruções chega a cerca de 3 relógios por elemento da matriz, mas isso não leva em consideração os atrasos de memória.
Teoria da operação: o design da CPU do ARM executa a maioria das instruções em um ciclo de clock, mas as instruções são executadas em um pipeline. Os compiladores C tentarão eliminar os atrasos do pipeline intercalando outras instruções no meio. Quando apresentado com um loop restrito como o código C original, o compilador terá dificuldade em ocultar os atrasos porque o valor lido da memória deve ser comparado imediatamente. Meu código abaixo alterna entre 2 conjuntos de 4 registros para reduzir significativamente os atrasos da própria memória e o pipeline que busca os dados. Em geral, ao trabalhar com grandes conjuntos de dados e seu código não usar a maioria ou todos os registros disponíveis, você não está obtendo o desempenho máximo.
; 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
Atualização:
Há muitos céticos nos comentários que pensam que minha experiência é anedótica / sem valor e requer provas. Usei o GCC 4.8 (do Android NDK 9C) para gerar a seguinte saída com a otimização -O2 (todas as otimizações ativadas, incluindo desenrolamento de loop ). Eu compilei o código C original apresentado na pergunta acima. Aqui está o que o GCC produziu:
.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
A saída do GCC não apenas desenrola o loop, mas também desperdiça um relógio em uma paralisação após o LDR. Requer pelo menos 8 relógios por elemento da matriz. É bom usar o endereço para saber quando sair do loop, mas todas as coisas mágicas que os compiladores são capazes de fazer não são encontradas em nenhum lugar neste código. Não executei o código na plataforma de destino (não possuo uma), mas qualquer pessoa com experiência no desempenho de código do ARM pode ver que meu código é mais rápido.
Atualização 2:
dei ao Visual Studio 2013 SP2 da Microsoft a chance de fazer melhor com o código. Ele foi capaz de usar as instruções NEON para vetorizar minha inicialização de matriz, mas a pesquisa de valor linear, conforme escrita pelo OP, foi semelhante ao que o GCC gerou (renomeei os rótulos para torná-los mais legíveis):
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
Como eu disse, não possuo o hardware exato do OP, mas testarei o desempenho em uma nVidia Tegra 3 e Tegra 4 das 3 versões diferentes e publicarei os resultados aqui em breve.
Atualização 3:
executei meu código e o código ARM compilado da Microsoft em um Tegra 3 e Tegra 4 (Surface RT, Surface RT 2). Eu executei 1000000 iterações de um loop que não consegue encontrar uma correspondência, para que tudo fique em cache e seja fácil de medir.
My Code MS Code
Surface RT 297ns 562ns
Surface RT 2 172ns 296ns
Nos dois casos, meu código é executado quase duas vezes mais rápido. A maioria das CPUs ARM modernas provavelmente fornecerá resultados semelhantes.