
Halo, Habr! Pada artikel ini saya ingin berbicara tentang pekerjaan floating point untuk prosesor dengan arsitektur ARM. Saya pikir artikel ini akan berguna terutama bagi mereka yang port OS mereka ke arsitektur ARM dan pada saat yang sama mereka membutuhkan dukungan untuk perangkat keras floating point (yang kami lakukan untuk
Embox , yang sebelumnya menggunakan implementasi perangkat lunak operasi floating point).
Jadi mari kita mulai.
Bendera kompiler
Untuk mendukung floating point, Anda harus memberikan flag yang benar ke compiler.
Googling cepat kami mengarah pada gagasan bahwa dua opsi sangat penting: -mfloat-abi dan -mfpu. Opsi -mfloat-abi menetapkan
ABI untuk operasi floating point dan dapat memiliki salah satu dari tiga nilai: 'soft', 'softfp' dan 'hard'. Opsi 'lunak', seperti namanya, memberi tahu kompiler untuk menggunakan panggilan fungsi bawaan untuk memprogram titik mengambang (opsi ini digunakan sebelumnya). Dua 'softfp' dan 'hard' yang tersisa akan dipertimbangkan sedikit kemudian, setelah mempertimbangkan opsi -mfpu.
Bendera -Mfpu dan versi VFP
Opsi -mfpu, seperti yang tertulis dalam
dokumentasi online gcc , memungkinkan Anda menentukan jenis perangkat keras dan dapat mengambil opsi berikut:
'otomatis', 'vfpv2', 'vfpv3', 'vfpv3-fp16', 'vfpv3-d16', 'vfpv3-d16-fp16', 'vfpv3xd', 'vfpv3xd-fp16', 'neon-vfpv', 'neon-vfp3' -fp16 ',' vfpv4 ',' vfpv4-d16 ',' fpv4-sp-d16 ',' neon-vfpv4 ',' fpv5-d16 ',' fpv5-sp-d16 ',' fp-armv8 ',' neon -fp-armv8 'dan' crypto-neon-fp-armv8 '. Dan 'neon' sama dengan 'neon-vfpv3', dan 'vfp' adalah 'vfpv2'.
Kompiler saya (arm-none-eabi-gcc (15: 5.4.1 + svn241155-1) 5.4.1 20160919) menghasilkan daftar yang sedikit berbeda, tetapi ini tidak mengubah esensinya. Bagaimanapun, kita perlu memahami bagaimana flag ini atau itu mempengaruhi kompiler, dan tentu saja, flag mana yang harus digunakan kapan.
Saya mulai memahami platform berdasarkan pada prosesor IMX6, tetapi kami akan menundanya untuk sementara waktu, karena neon coprocessor memiliki fitur yang akan saya bahas nanti, dan kami akan mulai dengan case yang lebih sederhana - dari platform
integrator / cp ,
Saya tidak memiliki papan itu sendiri, jadi debugging dilakukan pada emulator qemu. Dalam qemu, platform Interator / cp didasarkan pada prosesor
ARM926EJ-S , yang pada gilirannya mendukung coprocessor
VFP9-S . Coprocessor ini mematuhi Vector Floating-point Architecture versi 2 (VFPv2). Oleh karena itu, Anda perlu mengatur -mfpu = vfpv2, tetapi opsi ini tidak ada dalam daftar opsi dari kompiler saya. Di
Internet, saya bertemu opsi kompilasi dengan flag -mcpu = arm926ej-s -mfpu = vfpv3-d16, memasangnya, dan semuanya dikompilasi untuk saya. Ketika saya mulai, saya menerima
pengecualian instruksi yang tidak ditentukan , yang dapat diprediksi, karena coprocessor
dimatikan .
Untuk mengaktifkan coprocessor, Anda perlu mengatur bit EN [30] dalam register
FPEXC . Ini dilakukan dengan menggunakan perintah VMSR.
asm volatile ("VMSR FPEXC, %0" : : "r" (1 << 30);
Sebenarnya, perintah VMSR diproses oleh coprocessor dan melempar pengecualian jika coprocessor tidak dihidupkan, tetapi mengakses register
ini tidak menyebabkannya . Benar, tidak seperti yang lain, akses ke register ini hanya dimungkinkan dalam
mode istimewa .
Setelah coprocessor diizinkan bekerja, tes kami untuk fungsi matematika mulai berlalu. Tetapi ketika saya mengaktifkan optimasi (-O2), pengecualian instruksi yang tidak disebutkan sebelumnya dinaikkan. Dan itu muncul pada instruksi
vmov yang dipanggil dalam kode sebelumnya, tetapi dieksekusi dengan sukses (tanpa kecuali). Akhirnya, saya menemukan pada akhir halaman kalimat "Instruksi yang menyalin konstanta langsung tersedia di VFPv3" (yaitu, operasi dengan konstanta didukung mulai dari VFPv3). Dan saya memutuskan untuk memeriksa versi mana yang dirilis di emulator saya. Versi ini dicatat dalam register
FPSID . Dari dokumentasi itu berikut bahwa nilai register harus 0x41011090. Ini sesuai dengan 1 di bidang arsitektur [19..16] yaitu VFPv2. Sebenarnya, setelah mencetak di startup, saya dapat ini
unit: initializing embox.arch.arm.fpu.vfp9_s: VPF info: Hardware FP support Implementer = 0x41 (ARM) Subarch: VFPv2 Part number = 0x10 Variant = 0x09 Revision = 0x00
Setelah dengan hati-hati membaca bahwa 'vfp' adalah alias 'vfpv2', saya menetapkan flag yang benar, itu berfungsi. Kembali ke
halaman di mana saya melihat kombinasi flag -mcpu = arm926ej-s -mfpu = vfpv3-d16, saya perhatikan bahwa saya tidak cukup berhati-hati, karena -mfloat-abi = soft muncul di daftar flag. Artinya, tidak ada dukungan perangkat keras dalam hal ini. Lebih tepatnya, -mfpu hanya penting jika nilai selain 'lunak' disetel ke -mfloat-abi.
Assembler
Saatnya berbicara tentang assembler. Setelah semua, saya perlu membuat dukungan runtime, dan, misalnya, kompiler, tentu saja, tidak tahu tentang pengalihan konteks.
Daftar
Mari kita mulai dengan deskripsi register. VFP memungkinkan Anda untuk melakukan operasi dengan angka floating-point 32-bit (s0..s31) dan 64-bit (d0..d15) .Korespondensi antara register ini ditunjukkan pada gambar di bawah ini.

Q0-Q15 adalah register 128-bit dari versi lama untuk bekerja dengan SIMD, lebih lanjut tentangnya nanti.
Sistem komando
Tentu saja, paling sering pekerjaan dengan register VFP harus diberikan kepada kompiler, tetapi setidaknya Anda harus menulis konteks switch secara manual. Jika Anda sudah memiliki pemahaman perkiraan tentang sintaksis instruksi assembler untuk bekerja dengan register tujuan umum, berurusan dengan instruksi baru seharusnya tidak sulit. Paling sering, awalan "v" hanya ditambahkan.
vmov d0, r0, r1 /* r0 r1, .. d0 64 , r0-1 32 */ vmov r0, r1, d0 vadd d0, d1, d2 vldr d0, r0 vstm r0!, {d0-d15} vldm r0!, {d0-d15}
Dan sebagainya. Daftar lengkap perintah dapat ditemukan di
situs web ARM .
Dan tentu saja, jangan lupa tentang versi VFP sehingga tidak ada situasi seperti yang dijelaskan di atas.
Tandai -mfloat-abi 'softfp' dan 'hard'
Kembali ke -mfloat-abi. Jika Anda membaca
dokumentasinya , kita akan melihat:
'softfp' memungkinkan pembuatan kode menggunakan instruksi floating-point perangkat keras, tetapi masih menggunakan konvensi pemanggilan soft-float. 'hard' memungkinkan pembuatan instruksi floating-point dan menggunakan konvensi pemanggilan khusus FPU.
Artinya, kita berbicara tentang meneruskan argumen ke suatu fungsi. Tapi setidaknya tidak jelas bagi saya apa perbedaan antara konvensi panggilan “soft-float” dan “FPU-specific”. Dengan asumsi bahwa hard case menggunakan register floating point, dan case softfp menggunakan register integer, saya menemukan konfirmasi pada
wiki debian . Dan meskipun ini adalah untuk NEON coprocessors, tetapi itu tidak masalah. Hal lain yang menarik adalah bahwa dengan opsi softfp, kompiler dapat, tetapi tidak diharuskan untuk menggunakan dukungan perangkat keras:
“Compiler dapat membuat pilihan cerdas tentang kapan dan apakah ia menghasilkan instruksi FPU yang ditiru atau nyata tergantung pada tipe FPU yang dipilih (-mfpu =)“
Untuk kejelasan yang lebih baik, saya memutuskan untuk bereksperimen, dan sangat terkejut, karena dengan optimasi -O0 dimatikan, perbedaannya sangat kecil dan tidak berlaku untuk tempat-tempat di mana floating point sebenarnya digunakan. Menebak bahwa kompiler hanya mendorong semua yang ada di stack, daripada menggunakan register, saya menyalakan optimasi -O2 dan sekali lagi terkejut, karena dengan optimasi kompiler mulai menggunakan register floating point hardware untuk opsi hard dan sotffp, dan perbedaannya adalah bagaimana dan dalam kasus -O0 itu sangat tidak signifikan. Sebagai hasilnya, untuk saya sendiri, saya menjelaskan ini dengan fakta bahwa kompiler menyelesaikan
masalah yang terkait dengan fakta bahwa jika Anda menyalin data antara register titik mengambang dan bilangan bulat, kinerja akan turun secara signifikan. Dan kompiler, ketika mengoptimalkan, mulai menggunakan semua sumber daya yang tersedia.
Ketika ditanya flag mana yang menggunakan 'softfp' atau 'hard', saya menjawab sendiri sebagai berikut: di mana ada bagian yang sudah dikompilasi dengan flag 'softfp', Anda harus menggunakan 'hard'. Jika ada, maka Anda perlu menggunakan 'softfp'.
Sakelar konteks
Karena Embox mendukung multitasking preemptive, untuk bekerja dengan benar dalam runtime, tentu saja, implementasi pengalihan konteks diperlukan. Untuk ini, perlu menyimpan register coprocessor. Ada beberapa nuansa. Pertama: ternyata
perintah operasi stack untuk floating point (vstm / vldm) tidak mendukung semua mode . Kedua: operasi ini tidak mendukung kerja dengan lebih dari enam belas register 64-bit. Jika Anda perlu memuat / menyimpan lebih banyak register sekaligus, Anda perlu menggunakan dua instruksi.
Saya akan memberikan satu lagi optimasi kecil. Bahkan, menyimpan dan mengembalikan 256 byte register VFP setiap kali tidak diperlukan sama sekali (register tujuan umum hanya menempati 64 byte, sehingga perbedaannya signifikan). Optimalisasi yang jelas akan melakukan operasi ini hanya jika proses menggunakan register ini pada prinsipnya.
Seperti yang telah saya sebutkan, ketika coprocessor VFP dimatikan, upaya untuk mengeksekusi instruksi yang sesuai akan menghasilkan pengecualian "Undefined Instruction". Dalam penangan pengecualian ini, Anda perlu memeriksa apa yang disebabkan oleh pengecualian, dan jika itu adalah masalah menggunakan coprocessor VPF, maka proses ditandai sebagai menggunakan coprocessor VFP.
Sebagai hasilnya, save / restore konteks yang sudah ditulis ditambah dengan makro
#define ARM_FPU_CONTEXT_SAVE_INC(tmp, stack) \ vmrs tmp, FPEXC ; \ stmia stack!, {tmp}; \ ands tmp, tmp, #1<<30; \ beq fpu_out_save_inc; \ vstmia stack!, {d0-d15}; \ fpu_out_save_inc: #define ARM_FPU_CONTEXT_LOAD_INC(tmp, stack) \ ldmia stack!, {tmp}; \ vmsr FPEXC, tmp; \ ands tmp, tmp, #1<<30; \ beq fpu_out_load_inc; \ vldmia stack!, {d0-d15}; \ fpu_out_load_inc:
Untuk memeriksa kebenaran dari operasi switching konteks dalam kondisi floating-point, kami menulis tes di mana kami mengalikan dalam satu utas dalam satu lingkaran dan membaginya dalam yang lain, kemudian membandingkan hasilnya.
EMBOX_TEST_SUITE("FPU context consistency test. Must be compiled with -02"); #define TICK_COUNT 10 static float res_out[2][TICK_COUNT]; static void *fpu_context_thr1_hnd(void *arg) { float res = 1.0f; int i; for (i = 0; i < TICK_COUNT; ) { res_out[0][i] = res; if (i == 0 || res_out[1][i - 1] > 0) { i++; } if (res > 0.000001f) { res /= 1.01f; } sleep(0); } return NULL; } static void *fpu_context_thr2_hnd(void *arg) { float res = 1.0f; int i = 0; for (i = 0; i < TICK_COUNT; ) { res_out[1][i] = res; if (res_out[0][i] != 0) { i++; } if (res < 1000000.f) { res *= 1.01f; } sleep(0); } return NULL; } TEST_CASE("Test FPU context consistency") { pthread_t threads[2]; pthread_t tid = 0; int status; status = pthread_create(&threads[0], NULL, fpu_context_thr1_hnd, &tid); if (status != 0) { test_assert(0); } status = pthread_create(&threads[1], NULL, fpu_context_thr2_hnd, &tid); if (status != 0) { test_assert(0); } pthread_join(threads[0], (void**)&status); pthread_join(threads[1], (void**)&status); test_assert(res_out[0][0] != 0 && res_out[1][0] != 0); for (int i = 1; i < TICK_COUNT; i++) { test_assert(res_out[0][i] < res_out[0][i - 1]); test_assert(res_out[1][i] > res_out[1][i - 1]); } }
Tes berhasil lulus ketika optimasi dimatikan, jadi kami menunjukkan dalam deskripsi tes bahwa itu harus dikompilasi dengan optimasi, EMBOX_TEST_SUITE ("Tes konsistensi konteks FPU. Harus dikompilasi dengan -02"); Meskipun kita tahu bahwa tes tidak harus bergantung pada ini.
Prosesor NEON dan SIMD
Inilah saatnya untuk menceritakan mengapa saya menunda cerita tentang imx6. Faktanya adalah bahwa itu didasarkan pada inti Cortex-A9 dan mengandung lebih banyak NEON coprocessor (https://developer.arm.com/technologies/neon). NEON tidak hanya VFPv3, tetapi juga coprocessor SIMD. VFP dan NEON menggunakan register yang sama. VFP menggunakan register 32-bit dan 64-bit untuk operasi, dan NEON menggunakan register 64-bit dan 128-bit, yang terakhir hanya ditunjuk Q0-Q16. Selain nilai integer dan angka floating-point, NEON juga dapat bekerja dengan cincin polinomial modulo 2 tingkat 16 atau 8.
Mode vfp untuk NEON hampir tidak berbeda dari coprocessor vfp9-s yang dibongkar. Tentu saja, lebih baik untuk menentukan opsi vfpv3 atau vfpv3-d32 untuk -mfpu untuk optimasi yang lebih baik, karena memiliki 32 register 64-bit. Dan untuk mengaktifkan coprocessor, Anda harus memberikan akses ke coprocessor c10 dan c11. ini dilakukan dengan menggunakan perintah
asm volatile ("mrc p15, 0, %0, c1, c0, 2" : "=r" (val) :); val |= 0xf << 20; asm volatile ("mcr p15, 0, %0, c1, c0, 2" : : "r" (val));
tetapi tidak ada perbedaan mendasar lainnya.
Hal lain jika Anda menentukan -mfpu = neon, dalam hal ini kompiler dapat menggunakan instruksi SIMD.
Menggunakan SIMD dalam C
Untuk << mendaftar >> nilai secara manual dengan mendaftar, Anda dapat memasukkan "arm_neon.h" dan menggunakan tipe data yang sesuai:
float32x4_t untuk empat float 32-bit dalam satu register, uint8x8_t untuk delapan bilangan bulat 8-bit dan seterusnya. Untuk mengakses nilai tunggal, kami menyebutnya sebagai array, tambahan, perkalian, penugasan, dll. adapun variabel biasa, misalnya:
uint32x4_t a = {1, 2, 3, 4}, b = {5, 6, 7, 8}; uint32x4_t c = a * b; printf(“Result=[%d, %d, %d, %d]\n”, c[0], c[1], c[2], c[3]);
Tentu saja, menggunakan vektorisasi otomatis lebih mudah. Untuk vektorisasi otomatis, tambahkan flag -ftree-vectorize ke GCC.
void simd_test() { int a[LEN], b[LEN], c[LEN]; for (int i = 0; i < LEN; i++) { a[i] = i; b[i] = LEN - i; } for (int i = 0; i < LEN; i++) { c[i] = a[i] + b[i]; } for (int i = 0; i < LEN; i++) { printf("c[i] = %d\n", c[i]); } }
Loop tambahan menghasilkan kode berikut:
600059a0: f4610adf vld1.64 , [r1 :64] 600059a4: e2833010 add r3, r3, #16 600059a8: e28d0a03 add r0, sp, #12288 ; 0x3000 600059ac: e2811010 add r1, r1, #16 600059b0: f4622adf vld1.64 , [r2 :64] 600059b4: e2822010 add r2, r2, #16 600059b8: f26008e2 vadd.i32 q8, q8, q9 600059bc: ed430b04 vstr d16, [r3, #-16] 600059c0: ed431b02 vstr d17, [r3, #-8] 600059c4: e1530000 cmp r3, r0 600059c8: 1afffff4 bne 600059a0 <foo+0x58> 600059cc: e28d5dbf add r5, sp, #12224 ; 0x2fc0 600059d0: e2444004 sub r4, r4, #4 600059d4: e285503c add r5, r5, #60 ; 0x3c
Setelah melakukan tes untuk kode paralel, kami menemukan bahwa penambahan sederhana dalam satu lingkaran, asalkan variabel independen, memberikan akselerasi sebanyak 7 kali. Selain itu, kami memutuskan untuk melihat seberapa besar paralelisasi mempengaruhi tugas nyata, mengambil MESA3d dengan emulasi perangkat lunaknya dan mengukur jumlah fps dengan bendera yang berbeda, kami mendapat keuntungan 2 frame per detik (15 vs 13), yaitu akselerasi sekitar 15-20% .
Saya akan
memberikan contoh percepatan lainnya menggunakan perintah NEON , bukan milik kami, tetapi dari ARM.
Memori penyalinan hingga 50 persen lebih cepat dari biasanya. Contoh nyata ada di assembler.
Siklus penyalinan normal:
WordCopy LDR r3, [r1], #4 STR r3, [r0], #4 SUBS r2, r2, #4 BGE WordCopy
loop dengan perintah dan register neon:
NEONCopyPLD PLD [r1, #0xC0] VLDM r1!,{d0-d7} VSTM r0!,{d0-d7} SUBS r2,r2,#0x40 BGE NEONCopyPLD
Jelas bahwa penyalinan dengan 64 byte lebih cepat dari 4 byte, dan penyalinan seperti itu akan memberikan peningkatan 10%, tetapi 40% sisanya tampaknya memberikan pekerjaan coprocessor.
Cortex-m
Bekerja dengan FPU di Cortex-M tidak jauh berbeda dari yang dijelaskan di atas. Sebagai contoh, ini adalah bagaimana makro di atas terlihat untuk menyimpan konteks fpu-shny
#define ARM_FPU_CONTEXT_SAVE_INC(tmp, stack) \ ldr tmp, =CPACR; \ ldr tmp, [tmp]; \ tst tmp, #0xF00000; \ beq fpu_out_save_inc; \ vstmia stack!, {s0-s31}; fpu_out_save_inc:
Juga, perintah vstmia hanya menggunakan register s0-s31 dan register kontrol diakses secara berbeda. Oleh karena itu, saya tidak akan menjelaskan terlalu banyak, saya hanya akan menjelaskan perbedaan. Jadi, kami membuat dukungan untuk STM32F7discovery dengan
cortex-m7 untuk itu, masing-masing, kita perlu mengatur flag -mfpu = fpv5-sp-d16. Harap dicatat bahwa dalam versi seluler, Anda perlu melihat lebih dekat pada versi coprocessor, karena korteks-m yang sama mungkin memiliki opsi berbeda. Jadi, jika opsi Anda bukan dengan presisi ganda, tetapi dengan tunggal, maka mungkin tidak ada register D0-D16, seperti yang kita miliki di
stm32f4discovery , itulah mengapa varian dengan register S0-S31 digunakan. Untuk pengontrol ini kami menggunakan -mfpu = fpv4-sp-d16.
Perbedaan utama adalah akses ke register kontrol pengendali, mereka terletak langsung di ruang alamat inti utama, dan untuk berbagai jenis mereka berbeda
korteks-m4 untuk
korteks-m7 .
Kesimpulan
Pada ini saya akan mengakhiri cerita pendek saya tentang floating point untuk ARM. Saya perhatikan bahwa mikrokontroler modern sangat kuat dan cocok tidak hanya untuk kontrol, tetapi juga untuk memproses sinyal atau berbagai jenis informasi multimedia. Agar dapat menggunakan semua kekuatan ini secara efektif, Anda perlu memahami cara kerjanya. Saya harap artikel ini membantu memecahkan masalah ini sedikit lebih baik.