Ich programmiere derzeit auf dem teensy 36 Board, das hat ne ARM Cortex
M4 CPU und bei den Fliesspunktbefehlen habe ich folgendes festgestellt:
Addition (Befehl VADD) und Multiplikation(Befehl VMUL) brauchen jweils 1
Zyklus. Die Multiply-Add Instruction (VMLA) braucht 3 Zyklen. Damit ist
der VMLA Befehl langsamer als wenn ich getrennt MUL und ADD mache.
Welchen Sinn ergibt das? Welchen Vorteil hat die VMLA Instruktion?
Im folgenden Beispiel
1
for(intk=0;k<nn;k++){
2
fx+=fy*fz;
3
fy+=1.2;
4
fz+=1.3;
5
}
benutzt der GCC compiler die VMLA Instruktion für fx += fy * fz ;
Das Ergebnis ist genauer, wenn erst abschliessend gerundet wird:
https://de.wikipedia.org/wiki/Fused_multiply-add
Es wird ein Zwischenregister eingespart.
PS: Lies auch die Fussnoten: "Floating-point arithmetic data processing
instructions, such as add, subtract, multiply, divide, square-root, all
forms of multiply with accumulate, as well as conversions of all types
take one cycle longer if their result is consumed by the following
instruction."
FMUL
FADD
..use result..
hat 2 Straftakte drin und kommt damit auf 4 Takte, während
VMLA
..use result..
nur einen Straftakt hat und damit gleichauf liegt.
FPUs mit Pipelining könnten mit FMA effizienter umgehen als mit
getrennten Operationen, und damit schneller sein. Das betrifft den M4F
nicht, aber der Befehlssatz ist möglicherweise darauf ausgerichtet.
Das klingt schon mal logisch. Besten Dank.
Ich habe die Reihenfolge der Statements in der Schleife jetzt mal
geändert (siehe unten). Ich dachte ich könnte so einen Strafzyklus
notwendig machen, weil der vfma Befehl jetzt das Resultat des vadd
Befehls vor ihm braucht. Aber der Compiler hat geschickterweise die subs
Instruktion vorgezogen.
1
int32_tstart=micros();
2
for(intk=0;k<nn;k++){
3
fy+=1.2;
4
fz+=1.3;
5
fx+=fy*fz;
6
}
7
int32_tstop=micros();
8
/*
9
50e: ee37 7a06 vadd.f32 s14, s14, s12 1 Zyklus
10
512: ee77 7aa5 vadd.f32 s15, s15, s11 1 Zyklus
11
516: 3b01 subs r3, #1 1 Zyklus
12
518: eee7 6a27 vfma.f32 s13, s14, s15 3 Zyklen
13
51c: d1f7 bne.n 50e <setup+0x9e> 2 Zyklen
14
*/
Jetzt verstehe ich folgendes nicht: Meine Laufzeitmessung sagt, dass ein
Schleifendurchlauf 7 Zyklen braucht. Ich komme aber auf 8 Zyklen, wo
mache ich einen Fehler ?
Martin O. schrieb:> Ich komme aber auf 8 Zyklen, wo mache ich einen Fehler ?
Wilde Vermutung: bne.n hat unterschiedliche Ausführungszeit für branch
taken und branch not taken
void schrieb:> Wilde Vermutung: bne.n hat unterschiedliche Ausführungszeit für branch> taken und branch not taken
Die hat er mit 2 Takten schon eingerechnet. Aber ohne nun beim M4
konkret nachgesehen zu haben könnte es sein, dass das Gesamtsystem ihm
noch einen Takt für nichtsequentiellen Zugriff oder schlecht aligntes
Sprungziel zusätzlich drauf haut.
A. K. schrieb:> Die hat er mit 2 Takten schon eingerechnet. Aber ohne nun beim M4> konkret nachgesehen zu haben
Ja, und wenn er für branch taken nur einen Takt braucht. So herum war es
gemeint. War halt eine Idee zum nachschauen. Das nehme ich ihm aber
nicht ab.
Sorry für OT, aber warum schaust du dir das so genau an? Ich meine, es
ist interessant aber irgendwie auch brotlos auf den größeren
Controllern. Mich würde es einfach interessieren.
Martin O. schrieb:> Ich komme aber auf 8 Zyklen, wo> mache ich einen Fehler ?
Bei der Adresse. 0x50E ist nicht auf 4 Bytes ausgerichtet - das gibt
hier den Staftakt, denn dort steht ein 4 Bytes langer Befehl.
Mach mal ein __NOP(); vor die Schleife..
A. K. schrieb:> So hatte ich das auch erst gelesen. Er beschwert sich aber über einen> Takt zu wenig.
vielleicht braucht das VMLA doch weniger als 3 Takte und die genannten
Effekte (alignment,...) traten bei der allerersten Messung auf?
Martin O. schrieb:> Welchen Sinn ergibt das? Welchen Vorteil hat die VMLA Instruktion?
Normalerweise macht man damit Filter und optimiert selbige.
Das heißt - da man ohnehin beim Filtern 2 Datenströme bearbeiten muß (I
und Q) - kann man das Ganze auch verschachteln und die Schleife zum Teil
aufrollen, womit man den Taktverbrauch für das Schleifenzählen und
Springen reduziert.
Also mal ganz grobschlächtig skizziert so:
for ..
{ sumI = sumI + pkoeffI++ * psamplI++;
sumQ = sumQ + pkoeffQ++ * psamplQ++;
sumI = sumI + pkoeffI++ * psamplI++;
sumQ = sumQ + pkoeffQ++ * psamplQ++;
...usw. immer I und Q abwechselnd
}
Wenn man z.B. in der Schleife 8 solche IQ Paare drin hat, dann ist der
Aufwand in Takten für das Schleifenzählen nur noch 1/8 dessen, was man
mit nur einem Paar braucht.
W.S.
In anderen Beispielen benötigte die CPU immer 2 Takte für den Sprung.
Daher zähle ich den Sprung hier auch mit 2 Zyklen.
Das ganze mache ich aus purer Neugirde und um herauszufinden welche
Konstrukte welchen Aufwand haben.
Eine eigene Frage?
Dieser Thread ist über 7 Jahre alt.
Stell sie in einem eigenen Thread – dort lesen mehr Leute mit, und sie wird öfter und schneller beantwortet.