Trong các tình huống mà hiệu suất là vô cùng quan trọng, trình biên dịch C rất có thể sẽ không tạo ra mã nhanh nhất so với những gì bạn có thể làm với ngôn ngữ lắp ráp được điều chỉnh bằng tay. Tôi có xu hướng đi theo con đường ít kháng cự nhất - đối với các thói quen nhỏ như thế này, tôi chỉ viết mã asm và có một ý tưởng tốt là sẽ mất bao nhiêu chu kỳ để thực hiện. Bạn có thể sử dụng mã C và có được trình biên dịch để tạo đầu ra tốt, nhưng cuối cùng bạn có thể lãng phí rất nhiều thời gian để điều chỉnh đầu ra theo cách đó. Trình biên dịch (đặc biệt là từ Microsoft) đã đi một chặng đường dài trong vài năm qua, nhưng chúng vẫn không thông minh bằng trình biên dịch giữa hai tai của bạn vì bạn đang làm việc trong tình huống cụ thể của mình chứ không chỉ là trường hợp chung. Trình biên dịch có thể không sử dụng các hướng dẫn nhất định (ví dụ LDM) có thể tăng tốc độ này và nó ' s không đủ thông minh để bỏ qua vòng lặp. Đây là một cách để làm điều đó kết hợp 3 ý tưởng mà tôi đã đề cập trong nhận xét của mình: Hủy bỏ vòng lặp, tìm nạp trước bộ đệm và sử dụng hướng dẫn nhiều tải (ldm). Số lượng chu kỳ hướng dẫn xuất hiện khoảng 3 đồng hồ trên mỗi phần tử mảng, nhưng điều này không tính đến độ trễ bộ nhớ.
Lý thuyết vận hành: Thiết kế CPU của ARM thực hiện hầu hết các lệnh trong một chu kỳ xung nhịp, nhưng các hướng dẫn được thực thi trong một đường ống. Trình biên dịch C sẽ cố gắng loại bỏ sự chậm trễ đường ống bằng cách xen kẽ các hướng dẫn khác ở giữa. Khi được trình bày với một vòng lặp chặt chẽ như mã C ban đầu, trình biên dịch sẽ khó có thể che giấu sự chậm trễ vì giá trị đọc từ bộ nhớ phải được so sánh ngay lập tức. Mã của tôi dưới đây xen kẽ giữa 2 bộ 4 thanh ghi để giảm đáng kể độ trễ của bộ nhớ và đường ống tìm nạp dữ liệu. Nói chung, khi làm việc với các tập dữ liệu lớn và mã của bạn không sử dụng hầu hết hoặc tất cả các thanh ghi có sẵn, thì bạn sẽ không đạt được hiệu suất tối đa.
; 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
Cập nhật:
Có rất nhiều người hoài nghi trong các ý kiến cho rằng kinh nghiệm của tôi là giai thoại / vô giá trị và yêu cầu bằng chứng. Tôi đã sử dụng GCC 4.8 (từ Android NDK 9C) để tạo đầu ra sau với tối ưu hóa -O2 (tất cả các tối ưu hóa được bật bao gồm cả không kiểm soát vòng lặp ). Tôi đã biên dịch mã C ban đầu được trình bày trong câu hỏi trên. Đây là những gì GCC sản xuất:
.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
Đầu ra của GCC không chỉ không kiểm soát vòng lặp mà còn lãng phí đồng hồ trên một gian hàng sau LDR. Nó đòi hỏi ít nhất 8 đồng hồ cho mỗi phần tử mảng. Nó thực hiện tốt việc sử dụng địa chỉ để biết khi nào thoát khỏi vòng lặp, nhưng tất cả các trình biên dịch những điều kỳ diệu có khả năng thực hiện đều không tìm thấy trong mã này. Tôi chưa chạy mã trên nền tảng đích (tôi không sở hữu mã này), nhưng bất kỳ ai có kinh nghiệm về hiệu suất mã ARM đều có thể thấy mã của tôi nhanh hơn.
Cập nhật 2:
Tôi đã cho Microsoft Visual Studio 2013 SP2 cơ hội để làm tốt hơn với mã. Nó có thể sử dụng các hướng dẫn NEON để vectơ khởi tạo mảng của tôi, nhưng tìm kiếm giá trị tuyến tính được viết bởi OP xuất hiện tương tự như những gì GCC đã tạo (tôi đổi tên các nhãn để dễ đọc hơn):
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
Như tôi đã nói, tôi không sở hữu phần cứng chính xác của OP, nhưng tôi sẽ thử nghiệm hiệu năng trên nVidia Tegra 3 và Tegra 4 của 3 phiên bản khác nhau và sớm đăng kết quả tại đây.
Cập nhật 3:
Tôi đã chạy mã của tôi và mã ARM được biên dịch của Microsoft trên Tegra 3 và Tegra 4 (Surface RT, Surface RT 2). Tôi đã chạy 1000000 lần lặp của một vòng lặp mà không tìm thấy kết quả khớp để mọi thứ đều nằm trong bộ đệm và thật dễ dàng để đo.
My Code MS Code
Surface RT 297ns 562ns
Surface RT 2 172ns 296ns
Trong cả hai trường hợp, mã của tôi chạy nhanh gần gấp đôi. Hầu hết các CPU ARM hiện đại có thể sẽ cho kết quả tương tự.