
Hallo Habr! In diesem Artikel möchte ich über Gleitkommaarbeit für Prozessoren mit ARM-Architektur sprechen. Ich denke, dieser Artikel wird in erster Linie für diejenigen nützlich sein, die ihr Betriebssystem auf die ARM-Architektur portieren und gleichzeitig Unterstützung für Hardware-Gleitkomma benötigen (was wir für
Embox getan haben , das zuvor eine Software-Implementierung von Gleitkomma-Operationen verwendet hat).
Also fangen wir an.
Compiler-Flags
Um Gleitkomma zu unterstützen, müssen Sie die richtigen Flags an den Compiler übergeben. Ein schnelles
Googeln von uns führt zu der Idee, dass zwei Optionen besonders wichtig sind: -mfloat-abi und -mfpu. Die Option -mfloat-abi legt den
ABI für Gleitkommaoperationen fest und kann einen von drei Werten haben: 'soft', 'softfp' und 'hard'. Die Option 'soft' weist den Compiler, wie der Name schon sagt, an, die integrierten Funktionsaufrufe zum Programmieren des Gleitkommas zu verwenden (diese Option wurde zuvor verwendet). Die verbleibenden zwei 'softfp' und 'hard' werden etwas später berücksichtigt, nachdem die Option -mfpu in Betracht gezogen wurde.
-Mfpu-Flag und VFP-Version
Mit der Option -mfpu, wie in der
Online-Dokumentation zu gcc beschrieben , können Sie den Hardwaretyp angeben und die folgenden Optionen auswählen:
'auto', 'vfpv2', 'vfpv3', 'vfpv3-fp16', 'vfpv3-d16', 'vfpv3-d16-fp16', 'vfpv3xd', 'vfpv3xd-fp16', 'neon-vfpv3', 'neon -fp16 ',' vfpv4 ',' vfpv4-d16 ',' fpv4-sp-d16 ',' neon-vfpv4 ',' fpv5-d16 ',' fpv5-sp-d16 ',' fp-armv8 ',' neon -fp-armv8 'und' crypto-neon-fp-armv8 '. Und 'neon' ist dasselbe wie 'neon-vfpv3' und 'vfp' ist 'vfpv2'.
Mein Compiler (arm-none-eabi-gcc (15: 5.4.1 + svn241155-1) 5.4.1 20160919) erstellt eine etwas andere Liste, aber dies ändert nichts am Wesen der Sache. In jedem Fall müssen wir verstehen, wie sich dieses oder jenes Flag auf den Compiler auswirkt und natürlich, welches Flag wann verwendet werden sollte.
Ich habe angefangen, die Plattform zu verstehen, die auf dem imx6-Prozessor basiert, aber wir werden sie für eine kurze Zeit verschieben, da der Neon-Coprozessor Funktionen hat, die ich später besprechen werde, und wir werden mit einem einfacheren Fall beginnen - von der
Integrator / CP- Plattform aus.
Ich habe das Board selbst nicht, daher wurde das Debuggen auf dem QEMU-Emulator durchgeführt. In qemu basiert die Interator / cp-Plattform auf dem
ARM926EJ-S- Prozessor, der wiederum den
VFP9-S- Coprozessor unterstützt. Dieser Coprozessor entspricht der Vector Floating-Point Architecture Version 2 (VFPv2). Dementsprechend müssen Sie -mfpu = vfpv2 setzen, aber diese Option war nicht in der Liste der Optionen meines Compilers enthalten. Im
Internet habe ich eine Kompilierungsoption mit den Flags -mcpu = arm926ej-s -mfpu = vfpv3-d16 getroffen, installiert und alles für mich kompiliert. Als ich anfing, erhielt ich eine
undefinierte Anweisungsausnahme , die vorhersehbar war, da der Coprozessor ausgeschaltet war.
Damit der Coprozessor funktioniert, müssen Sie das EN-Bit [30] im
FPEXC- Register setzen. Dies erfolgt mit dem Befehl VMSR.
asm volatile ("VMSR FPEXC, %0" : : "r" (1 << 30);
Tatsächlich wird der VMSR-Befehl vom Coprozessor verarbeitet und löst eine Ausnahme aus, wenn der Coprozessor nicht eingeschaltet ist, der Zugriff auf dieses Register dies
jedoch nicht verursacht . Richtig, im Gegensatz zu den anderen ist der Zugriff auf dieses Register nur im
privilegierten Modus möglich .
Nachdem der Coprozessor arbeiten durfte, begannen unsere Tests für mathematische Funktionen zu bestehen. Als ich jedoch die Optimierung (-O2) aktivierte, wurde die zuvor erwähnte undefinierte Anweisungsausnahme ausgelöst. Und es entstand auf der
vmov- Anweisung, die zuvor im Code aufgerufen wurde, aber erfolgreich ausgeführt wurde (ohne Ausnahme). Schließlich fand ich am Ende der Seite den Satz "Die Anweisungen zum Kopieren von Sofortkonstanten sind in VFPv3 verfügbar" (dh Operationen mit Konstanten werden ab VFPv3 unterstützt). Und ich habe mich entschlossen zu überprüfen, welche Version in meinem Emulator veröffentlicht ist. Die Version wird im
FPSID- Register aufgezeichnet. Aus der Dokumentation folgt, dass der Wert des Registers 0x41011090 sein muss. Dies entspricht 1 im Architekturbereich [19..16], dh VFPv2. Nachdem ich beim Start einen Ausdruck gemacht hatte, bekam ich diesen
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
Nachdem ich sorgfältig gelesen habe, dass 'vfp' der Alias 'vfpv2' ist, habe ich das richtige Flag gesetzt, es hat funktioniert. Zurück zu der
Seite, auf der ich die Kombination der Flags -mcpu = arm926ej-s -mfpu = vfpv3-d16 gesehen habe, stelle ich fest, dass ich nicht vorsichtig genug war, da -mfloat-abi = soft in der Liste der Flags erscheint. Das heißt, in diesem Fall gibt es keine Hardwareunterstützung. Genauer gesagt ist -mfpu nur wichtig, wenn ein anderer Wert als 'soft' auf -mfloat-abi gesetzt ist.
Assembler
Es ist Zeit, über Assembler zu sprechen. Schließlich musste ich Laufzeitunterstützung leisten, und zum Beispiel weiß der Compiler natürlich nichts über Kontextwechsel.
Register
Beginnen wir mit der Beschreibung der Register. Mit VFP können Sie Operationen mit 32-Bit- (s0..s31) und 64-Bit- (d0..d15) Gleitkommazahlen ausführen. Die Entsprechung zwischen diesen Registern ist in der folgenden Abbildung dargestellt.

Q0-Q15 sind 128-Bit-Register aus älteren Versionen für die Arbeit mit SIMD, dazu später mehr.
Befehlssystem
Natürlich sollte die Arbeit mit VFP-Registern meistens dem Compiler übergeben werden, aber zumindest müssen Sie den Kontextwechsel manuell schreiben. Wenn Sie bereits ein ungefähres Verständnis der Syntax von Assembler-Anweisungen für die Arbeit mit Allzweckregistern haben, sollte der Umgang mit neuen Anweisungen nicht schwierig sein. Meistens wird das Präfix „v“ einfach hinzugefügt.
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}
Usw. Eine vollständige Liste der Befehle finden Sie auf
der ARM-Website .
Vergessen Sie natürlich nicht die VFP-Version, damit es keine Situationen wie die oben beschriebene gibt.
Flag -mfloat-abi 'softfp' und 'hard'
Zurück zu -mfloat-abi. Wenn Sie die
Dokumentation lesen, werden wir sehen:
'softfp' ermöglicht die Generierung von Code mithilfe von Hardware-Gleitkommaanweisungen, verwendet jedoch weiterhin die Soft-Float-Aufrufkonventionen. 'hard' ermöglicht die Erzeugung von Gleitkommaanweisungen und verwendet FPU-spezifische Aufrufkonventionen.
Das heißt, wir sprechen über das Übergeben von Argumenten an eine Funktion. Zumindest war mir nicht klar, was der Unterschied zwischen "Soft-Float" - und "FPU-spezifischen" Aufrufkonventionen ist. Unter der Annahme, dass der Hardcase Gleitkommaregister und der Softfp-Case Integer-Register verwendet, habe ich im
Debian-Wiki eine Bestätigung gefunden. Und das ist zwar für NEON-Coprozessoren, aber es spielt keine Rolle. Ein weiterer interessanter Punkt ist, dass der Compiler mit der Option softfp die Hardwareunterstützung verwenden kann, aber nicht muss:
„Der Compiler kann kluge Entscheidungen darüber treffen, wann und ob er emulierte oder echte FPU-Anweisungen generiert, abhängig vom gewählten FPU-Typ (-mfpu =).“
Zur besseren Übersicht habe ich mich für ein Experiment entschieden und war sehr überrascht, da der Unterschied bei deaktivierter -O0-Optimierung sehr gering war und nicht für Orte galt, an denen der Gleitkomma tatsächlich verwendet wurde. Als ich vermutete, dass der Compiler einfach alles auf den Stapel schiebt, anstatt Register zu verwenden, schaltete ich die -O2-Optimierung ein und war erneut überrascht, da der Compiler mit der Optimierung anfing, Hardware-Gleitkommaregister sowohl für die Hard- als auch für die Sotffp-Option zu verwenden, und der Unterschied ist wie folgt und im Fall von -O0 war es sehr unbedeutend. Infolgedessen habe ich dies für mich selbst dadurch erklärt, dass der Compiler das
Problem löst, das mit der Tatsache verbunden ist, dass die Leistung erheblich sinkt, wenn Sie Daten zwischen Gleitkommaregistern und Ganzzahlen kopieren. Und der Compiler beginnt bei der Optimierung, alle ihm zur Verfügung stehenden Ressourcen zu nutzen.
Auf die Frage, welches Flag 'softfp' oder 'hard' verwenden soll, antwortete ich für mich selbst: Wo immer Teile mit dem Flag 'softfp' kompiliert sind, sollten Sie 'hard' verwenden. Wenn es welche gibt, müssen Sie 'softfp' verwenden.
Kontextwechsel
Da Embox präemptives Multitasking unterstützt, war natürlich eine Implementierung der Kontextumschaltung erforderlich, um zur Laufzeit korrekt zu funktionieren. Hierzu müssen die Coprozessorregister gespeichert werden. Es gibt ein paar Nuancen. Erstens: Es stellte sich heraus, dass
die Stapeloperationsbefehle für Gleitkommawerte (vstm / vldm) nicht alle Modi unterstützen . Zweitens: Diese Operationen unterstützen keine Arbeit mit mehr als 16 64-Bit-Registern. Wenn Sie mehrere Register gleichzeitig laden / speichern müssen, müssen Sie zwei Anweisungen verwenden.
Ich werde noch eine kleine Optimierung geben. Tatsächlich ist das Speichern und Wiederherstellen von 256 Bytes von VFP-Registern jedes Mal überhaupt nicht erforderlich (Allzweckregister belegen nur 64 Bytes, daher ist der Unterschied signifikant). Eine offensichtliche Optimierung führt diese Operationen nur aus, wenn der Prozess diese Register im Prinzip verwendet.
Wie bereits erwähnt, führt ein Versuch, den entsprechenden Befehl auszuführen, beim Ausschalten des VFP-Coprozessors zu einer Ausnahme "Undefinierter Befehl". Im Handler dieser Ausnahme müssen Sie überprüfen, durch was die Ausnahme verursacht wird. Wenn es sich um die Verwendung eines VPF-Coprozessors handelt, wird der Prozess als Verwendung des VFP-Coprozessors markiert.
Infolgedessen wurde das bereits geschriebene Speichern / Wiederherstellen des Kontexts durch Makros ergänzt
#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:
Um die Richtigkeit der Kontextumschaltoperation unter Gleitkommabedingungen zu überprüfen, haben wir einen Test geschrieben, bei dem wir in einem Thread in einer Schleife multiplizieren und in einem anderen dividieren und dann die Ergebnisse vergleichen.
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]); } }
Der Test wurde erfolgreich bestanden, als die Optimierung deaktiviert wurde. Daher haben wir in der Testbeschreibung angegeben, dass er mit der Optimierung EMBOX_TEST_SUITE kompiliert werden soll ("FPU-Kontextkonsistenztest. Muss mit -02 kompiliert werden"). obwohl wir wissen, dass Tests nicht darauf beruhen sollten.
Co-Prozessor NEON und SIMD
Es ist Zeit zu erzählen, warum ich die Geschichte über imx6 verschoben habe. Tatsache ist, dass es auf dem Cortex-A9-Kern basiert und den fortschrittlicheren NEON-Coprozessor enthält (https://developer.arm.com/technologies/neon). NEON ist nicht nur VFPv3, sondern auch ein SIMD-Coprozessor. VFP und NEON verwenden dieselben Register. VFP verwendet 32-Bit- und 64-Bit-Register für den Betrieb, und NEON verwendet 64-Bit- und 128-Bit-Register, wobei letztere nur als Q0-Q16 bezeichnet wurden. Neben ganzzahligen Werten und Gleitkommazahlen kann NEON auch mit einem Polynomring von Modulo 2 16. oder 8. Grades arbeiten.
Der vfp-Modus für NEON unterscheidet sich fast nicht vom zerlegten vfp9-s-Coprozessor. Natürlich ist es besser, die Optionen vfpv3 oder vfpv3-d32 für -mfpu anzugeben, um eine bessere Optimierung zu erzielen, da 32 64-Bit-Register vorhanden sind. Um den Coprozessor zu aktivieren, müssen Sie Zugriff auf die Coprozessoren c10 und c11 gewähren. Dies geschieht mit Befehlen
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));
Es gibt jedoch keine weiteren grundlegenden Unterschiede.
Wenn Sie -mfpu = neon angeben, kann der Compiler in diesem Fall SIMD-Anweisungen verwenden.
Verwenden von SIMD in C.
Um Werte << registrieren >> manuell per Register zu registrieren, können Sie "arm_neon.h" einschließen und die entsprechenden Datentypen verwenden:
float32x4_t für vier 32-Bit-Floats in einem Register, uint8x8_t für acht 8-Bit-Ganzzahlen usw. Um auf einen einzelnen Wert zuzugreifen, bezeichnen wir ihn als Array, Addition, Multiplikation, Zuweisung usw. wie für gewöhnliche Variablen, zum Beispiel:
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]);
Natürlich ist die Verwendung der automatischen Vektorisierung einfacher. Fügen Sie für die automatische Vektorisierung das Flag -ftree-vectorize zu GCC hinzu.
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]); } }
Die Additionsschleife generiert den folgenden Code:
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
Nachdem wir Tests für parallelisierten Code durchgeführt hatten, stellten wir fest, dass eine einfache Addition in einer Schleife, sofern die Variablen unabhängig sind, eine bis zu siebenfache Beschleunigung ergibt. Außerdem haben wir uns entschlossen zu sehen, wie stark sich die Parallelisierung auf reale Aufgaben auswirkt, MESA3d mit seiner Software-Emulation verwendet und die Anzahl der fps mit verschiedenen Flags gemessen. Wir haben eine Verstärkung von 2 Bildern pro Sekunde (15 gegenüber 13) erhalten, dh die Beschleunigung beträgt etwa 15 bis 20% .
Ich werde
ein weiteres
Beispiel für die Beschleunigung mit NEON-Befehlen geben , nicht mit unseren, sondern mit ARM.
Das Kopieren von Speicher ist bis zu 50 Prozent schneller als normal. Wahre Beispiele gibt es in Assembler.
Normaler Kopierzyklus:
WordCopy LDR r3, [r1], #4 STR r3, [r0], #4 SUBS r2, r2, #4 BGE WordCopy
Schleife mit Neonbefehlen und Registern:
NEONCopyPLD PLD [r1, #0xC0] VLDM r1!,{d0-d7} VSTM r0!,{d0-d7} SUBS r2,r2,#0x40 BGE NEONCopyPLD
Es ist klar, dass das Kopieren um 64 Bytes schneller als 4 Bytes ist und ein solches Kopieren eine Erhöhung von 10% ergibt, aber die restlichen 40% scheinen durch die Arbeit des Coprozessors bereitgestellt zu werden.
Cortex-m
Die Arbeit mit FPUs in Cortex-M unterscheidet sich nicht wesentlich von der oben beschriebenen. So sieht das obige Makro beispielsweise aus, um den fpu-shny-Kontext zu speichern
#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:
Außerdem verwendet der Befehl vstmia nur die Register s0-s31, und auf die Steuerregister wird unterschiedlich zugegriffen. Deshalb werde ich nicht zu sehr ins Detail gehen, ich werde nur diff erklären. Daher haben wir STM32F7discovery mit
cortex-m7 unterstützt. Wir müssen das Flag -mfpu = fpv5-sp-d16 setzen. Bitte beachten Sie, dass Sie in mobilen Versionen die Coprozessor-Version genauer betrachten müssen, da derselbe Cortex-m möglicherweise unterschiedliche Optionen hat. Wenn Ihre Option also nicht mit doppelter Genauigkeit, sondern mit einfacher Genauigkeit ist, gibt es möglicherweise keine D0-D16-Register, wie wir es in
stm32f4discovery haben , weshalb die Variante mit den Registern S0-S31 verwendet wird. Für diesen Controller verwenden wir -mfpu = fpv4-sp-d16.
Der Hauptunterschied besteht im Zugriff auf die Steuerregister der Steuerung, sie befinden sich direkt im Adressraum des Hauptkerns und für verschiedene Typen sind sie für
cortex-m7 unterschiedlich
cortex-m4 .
Fazit
Damit werde ich meine Kurzgeschichte über Gleitkomma für ARM beenden. Ich stelle fest, dass moderne Mikrocontroller sehr leistungsfähig sind und sich nicht nur zur Steuerung, sondern auch zur Verarbeitung von Signalen oder verschiedenen Arten von Multimedia-Informationen eignen. Um all diese Kräfte effektiv nutzen zu können, müssen Sie verstehen, wie es funktioniert. Ich hoffe, dieser Artikel hat dazu beigetragen, dies ein wenig besser herauszufinden.