성능이 가장 중요한 상황에서 C 컴파일러는 수동으로 튜닝 된 어셈블리 언어로 할 수있는 작업에 비해 가장 빠른 코드를 생성하지 못할 가능성이 높습니다. 나는 최소한의 저항의 길을 택하는 경향이 있습니다. 이와 같은 작은 루틴의 경우, 저는 asm 코드를 작성하고 실행하는 데 얼마나 많은 사이클이 소요 될지 좋은 아이디어를 가지고 있습니다. C 코드를 조작하고 컴파일러가 좋은 출력을 생성하도록 할 수는 있지만 출력을 조정하는 데 많은 시간을 낭비하게 될 수 있습니다. 컴파일러 (특히 Microsoft)는 지난 몇 년 동안 먼 길을 걸어 왔지만 일반적인 경우가 아닌 특정 상황에서 작업하기 때문에 여전히 귀 사이의 컴파일러만큼 똑똑하지 않습니다. 컴파일러는이 속도를 높일 수있는 특정 명령어 (예 : LDM)를 사용하지 않을 수 있습니다. 루프를 풀기에 충분히 똑똑하지 않을 것입니다. 내 의견에서 언급 한 3 가지 아이디어를 통합하는 방법은 다음과 같습니다. 루프 풀기, 캐시 프리 페치 및 다중로드 (ldm) 명령어 사용. 명령어 사이클 수는 어레이 요소 당 약 3 클럭으로 나오지만 메모리 지연은 고려하지 않습니다.
작동 이론 : ARM의 CPU 설계는 대부분의 명령어를 한 클럭 주기로 실행하지만 명령어는 파이프 라인에서 실행됩니다. C 컴파일러는 중간에 다른 명령어를 삽입하여 파이프 라인 지연을 제거하려고합니다. 원본 C 코드와 같은 타이트한 루프가 제공되면 컴파일러는 메모리에서 읽은 값을 즉시 비교해야하기 때문에 지연을 숨기는 데 어려움을 겪습니다. 아래의 코드는 메모리 자체의 지연과 데이터를 가져 오는 파이프 라인의 지연을 크게 줄이기 위해 4 개의 레지스터로 구성된 2 세트를 번갈아 사용합니다. 일반적으로 대용량 데이터 세트로 작업 할 때 코드가 사용 가능한 레지스터의 대부분 또는 전부를 사용하지 않으면 최대 성능을 얻지 못합니다.
; 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의 정확한 하드웨어를 소유하고 있지 않지만, 3 가지 버전의 nVidia Tegra 3 및 Tegra 4에서 성능을 테스트하고 곧 여기에 결과를 게시 할 것입니다.
업데이트 3 :
Tegra 3 및 Tegra 4 (Surface RT, Surface RT 2)에서 내 코드와 Microsoft의 컴파일 된 ARM 코드를 실행했습니다. 모든 것이 캐시에 있고 측정하기 쉽도록 일치 항목을 찾지 못하는 루프를 1000000 번 반복했습니다.
My Code MS Code
Surface RT 297ns 562ns
Surface RT 2 172ns 296ns
두 경우 모두 내 코드가 거의 두 배 빠르게 실행됩니다. 대부분의 최신 ARM CPU는 비슷한 결과를 제공 할 것입니다.