Ich mach grad meine ersten zögerlichen Gehversuche mit inline assembler,
und hätte meine erste Funktion fertig. Ich hab mir fmuls ausgesucht,
weil ich das in weiterer Folge auch brauchen werde, wohl wissend dass es
das schon fertig als builtin gibt. Mir gehts aber ums Lernen und
Verstehen.
Also: ist das soweit korrekt?
Meine Fragen dazu:
a) das Ergebnis von fmul (wie auch von vielen weiteren Operationen)
steht oft in r0 (oder r0/r1). Das muss ich immer in ein Register
weiterschieben, oder? Ich kann dem GCC nicht sagen "Ergebnis wäre in r0
und r1, wenn du magst kannst du dort damit weiterrechnen, sonst schiebs
dir wohin es dir beliebt"?
b) das "=&r" passt so?
c) müssen/sollen r0 und r1 in der clobber-list angegeben werden?
Danke!
Michael Reinelt schrieb:> a) das Ergebnis von fmul (wie auch von vielen weiteren Operationen)> steht oft in r0 (oder r0/r1). Das muss ich immer in ein Register> weiterschieben, oder?
Ja, denn r0 und r1 sind für den GCC (leider, die Entscheidung war
vor der Einführung der Multiplikationsbefehle so getroffen worden)
Register mit Sonderfunktionen (generic scratch register, zero register).
> b) das "=&r" passt so?
Ja.
> c) müssen/sollen r0 und r1 in der clobber-list angegeben werden?
Ja, wobei es sogar sein kann, dass du dich selbst drum kümmern musst,
die Konstante 0 wieder nach r1 zu schreiben. Musst du mal im
generierten Assemblercode schauen, ob die Deklaration als clobber
genügt, dass das der Compiler von sich aus macht.
Jörg Wunsch schrieb:> Michael Reinelt schrieb:>>> a) das Ergebnis von fmul (wie auch von vielen weiteren Operationen)>> steht oft in r0 (oder r0/r1). Das muss ich immer in ein Register>> weiterschieben, oder?>> Ja, denn r0 und r1 sind für den GCC (leider, die Entscheidung war> vor der Einführung der Multiplikationsbefehle so getroffen worden)> Register mit Sonderfunktionen (generic scratch register, zero register).
Danke. ist nciht so tragisch, wenn ich schon unbedingt da ein paar
nanosekunden einsparen will, mach ich halt das "Weiterrechnen" auch im
inline-asm.
>> b) das "=&r" passt so?>> Ja.
Puh!
Irgendwie hab ich manchmal geselen, dass man "l" (für "lvalue) nehmen
soll... da blick ich noch nicht so richtig durch...
>> c) müssen/sollen r0 und r1 in der clobber-list angegeben werden?>> Ja, wobei es sogar sein kann, dass du dich selbst drum kümmern musst,> die Konstante 0 wieder nach r1 zu schreiben. Musst du mal im> generierten Assemblercode schauen, ob die Deklaration als clobber> genügt, dass das der Compiler von sich aus macht.
Ich muss es auf jeden fall wieder leeren / auf null setzen. In
irgendeinem der Cookbooks steht, dass es eben nix bringt r1 in die
Clobber-Liste mit aufzunehmen, man muss es auf jeden Fall mit clr
löschen. Die Frage ist eher, ob man üblicherweise r0 und r1 da
hinschreibt oder nicht.
Aber wenn das soweit mal passt, dann werd ich mich mal an eine fmul_8x16
rantrauen. Wobei die Kombination inline-asm und fractional multiply
momentan recht schwerer tobak für mich ist :-)
Michael Reinelt schrieb:> Die Frage ist eher, ob man üblicherweise r0 und r1 da hinschreibt oder> nicht.
Hat zumindest dann Kommentarwert, wenn du das in fünf Jahren mal
wieder liest. ;-)
Beim GCC inline Assembler kriegt ich immer rote Pusteln im Gesicht und
Schaum vorm Mund.
Einfacher ist es, ein Object komplett in Assembler zu schreiben (*.S
Datei). Die Lesbarkeit steigt enorm.
Peter Dannegger schrieb:> Beim GCC inline Assembler kriegt ich immer rote Pusteln im Gesicht und> Schaum vorm Mund.
Nur, weil du nicht verstanden hast, wie genial das Ding ist. ;-)
Die Ernüchterung ist mir gekommen, als ich mir mal angesehen habe,
wie primitiv inline assembly beim IAR gemacht ist. Da kannst du so
ziemlich alles erraten, was der Compiler wohl ringsum anstellen wird,
und es könnte natürlich schon in der nächsten Version des Compilers
auch ganz anders kommen. Eigentlich kannst du dich dort bestenfalls
auf globale Variablen verlassen, sonst nichts.
Der GCC dagegen gestattet es, dass der Programmierer selbst bei so
einem tiefen Eingriff wie dem Inline-Assembler dem Compiler noch so
komplett wie möglich die Integration in seine Optimierungsstragie
überlassen kann. Du beschreibst also nicht: „Ich will mit Register 6
jetzt den Wert verarbeiten, dann den von Register 19 dazu.“, sondern
du beschreibst: „Ich brauche ein beliebiges allgemeines Register, und
dann noch eins aus der oberen Registerhälfte.“ Damit ist es möglich,
dass der Programmierer nicht dem Compiler dumm im Weg rumsteht mit
der Wahl der Register für seinen Assemblercode.
Michael, an deiner Stelle würde ich allerdings den Argumenten Namen
geben, statt sie nur durchzunummerieren. Das ist ein neueres Feature
des GCC, verbessert aber meiner Meinung nach die Lesbarkeit. Also
etwa so:
Den Algorithmus hab ich aus der Atmel-Doku abgeschrieben, keine Angst
:-) von alleine wär ich nie drauf gekommen, dass da plötzlich ein sbc
vorkommen muss....
ABER: hier bin ich jetzt sehr verunsichert, welche "Kenner" ich in die
Parameterlisten schreiben muss. es sind ja diesmal 2- bzw.
4-byte-Werte...
Jörg? Hilfe!
@Peter: Hier sieht man sehr schön die von Jörg angesprochene Genialität:
ich brauch ein Register mit Inhalt 0. Das Standard-r1 kann ich nicht
nehmen, weil da mein Ergebnis der Multiplikation drinnensteckt. ich muss
mich jetzt nicht entscheiden, sondern kann es dem Compiler überlassen,
irgendein Register mit 0 zu belegen und im Assembler-Code zu verwenden.
In einem geb ich dir aber recht: Schön ist anders :-)
Michael Reinelt schrieb:> Ich mach grad meine ersten zögerlichen Gehversuche mit inline assembler,> und hätte meine erste Funktion fertig. Ich hab mir fmuls ausgesucht,1
Wobei 2) etwas ineffizienter ist als 1), aber sicherstellt, dass -1 * -1
nicht überläuft sonder knapp 1 liefert.
O.g. Funktionen implementiert man bevorzugt als "static inline".
Jörg Wunsch schrieb:> p.s.: Ich denke, das „volatile“ ist hier unnütz.
Ja. Ebenso das early-clobber: "=&r" --> "=r", falls du noch weniger:
> dem Compiler dumm im Weg rumsteht[en willst]
Hallo Johann,
ich hatte gehofft dass du hier reinschaust :-)
Johann L. schrieb:> return __builtin_avr_fmuls (a, b);
Danke, kannte ich, aber ich wollte nicht nur fmuls implementieren,
sondern etwas über inline asm lernen.
> *2*[c]#include <stdfix.h>
Hmmm... hab ich nicht.
1
dpkg-query -l | grep avr
2
ii avr-libc 1:1.8.0-4.1
3
ii gcc-avr 1:4.8-2
die nächsthöhere Version (1.8.0+Atmel-3.4.4-1) hab ich bewusst nicht
installiert wegen dem lästigen Bug (ähhh was war das gleich? Irgendwas
mit ISR misspelled)
> Ja. Ebenso das early-clobber: "=&r" --> "=r"
Warum? "Register should be used for output only" warum ist das
kontraproduktiv?
Und ich würde mich sehr freuen, wenn mir noch jemand bei meinem letzten
Beispiel "fmul_16x16" auf die Sprünge helfen könnte, was ich da bei den
input und output operands hinschreiben soll, wenn mehr als ein Register
betroffen ist (sprich: 16 oder 32 bits angesprochen werden)
Danke!
Michael Reinelt schrieb:>> Ja. Ebenso das early-clobber: "=&r" --> "=r"> Warum? "Register should be used for output only" warum ist das> kontraproduktiv?
Das & ("early-clobber") sagt dem Compiler, daß dieser output-Operand
beschrieben ("erschlagen") wird, bevor die inputs "aufgebraucht" sind -
d.h. daß er für diesen Output keins der Input-Register verwenden darf.
Läßt man das "&" weg (Assembler-Code genau anschauen, ob o.g. Bedingung
auch wirklich falsch ist), kommt man mit einem Register weniger aus.
> die nächsthöhere Version (1.8.0+Atmel-3.4.4-1) hab ich bewusst nicht
Wenn in "4.8" auch avr-gcc 4.8 drinne ist, dann ist auch Fixed-Point
Support drinne:
http://gcc.gnu.org/gcc-4.8/changes.html#avr
Allerdings gibt es keine dedizierte Option für ISO/IEC DTR 18037 aka.
"Embedded-C". GNU-C muss aktive sein.
>> Ja. Ebenso das early-clobber: "=&r" --> "=r"> Warum? "Register should be used for output only" warum ist das> kontraproduktiv?
Zum einen gibt es bei deinem asm keinen Grund, warum sich Ein- und
Ausgabe nicht überlappen dürfen. Zum anderen ist der Registerallokator
mit "&" konservativer, d.h. man kann beobachten, dass mit "&" u.U.
unnötige MOVs erzeugt werden, weil die Register-Allokation suboptimal
arbeitet. Aufgefallen ist mit das mit Reload und mit IRA, ob LRA diese
Schwäche immer noch hat, weiß ich net.
Versuch einfach mal Code, in dem ein Wert Eingabe und Ausgabe eines asm
ist und nach R24 allokiert werden sollte, etwa weil er Parameter /
Returnwert einer Funktion ist.
> Und ich würde mich sehr freuen, wenn mir noch jemand bei meinem letzten> Beispiel "fmul_16x16" auf die Sprünge helfen könnte, was ich da bei den> input und output operands hinschreiben soll, wenn mehr als ein Register> betroffen ist (sprich: 16 oder 32 bits angesprochen werden)
Genau wie sonst auch. GCC kennt die Typen der Operanden und allokiert
entsprechend viele Register. Kann allerdings Probleme geben, wenn es in
der gewünschten Registerklasse eng wird, siehe
http://gcc.gnu.org/PR56479
Johann L. schrieb:> Wenn in "4.8" auch avr-gcc 4.8 drinne ist, dann ist auch Fixed-Point> Support drinne:
Fixed-Point Support dürfte ich haben, zumindest compiliert er den Typ
_Fract problemlos.
Allerdings finde ich dann im Compiler-Ergebnis für short _Fract * short
_Fract noch
1
call __mulhq3
Ich denke da fehlt dann noch ein patch von dir, den ich irgendwie
gefunden habe, mit dem dann direkt fmuls instructions erzeugt werden
sollten... (zumindest hab ich was im Google dazu gefunden... scheduled
for 4.9 oder so..)
Abgesehen davon dass ich eigentlich eine fmuls-instruktion erwartet
hätte, sind die operationen auf r18 (zuerst ein lsr, dann ein clr, dann
ein ror?) für mich nicht wirklich nachvollziehbar.
nicht mehr ganz so viele unnötige Sachen, trotzdem etwas umständlich:
Zuerst wird r26 (a) geladen, dann nach r27 geschoben und r26 auf null
gesetzt (selbes Spiel mit b)
Ich vermute der call __mulhq3 erledigt nicht nur das fmuls sondern auch
die Korrektur nach überlauf?
Michael Reinelt schrieb:> Oje, vielleicht sollte man beim testen die richtigen Datentypen> verwenden...
Yupp. Die Schnittmenge zwischen "int8_t" und "(short) fract" ist { -1,
0 }. Nicht sonderlich ergiebig ;-)
> volatile [auto];>> nicht mehr ganz so viele unnötige Sachen, trotzdem etwas umständlich:
Na wenn's dir um unnötige Sachen oder die Quintessenz geht, dann ist
volatile eine schlechte Wahl:
1
#include<stdint.h>
2
#include<stdfix.h>
3
4
fract
5
fmul_8x8_16(shortfracta,shortfractb)
6
{
7
return(fract)a*b;
8
}
liefert mit -Os -S -dp:
1
fmul_8x8_16:
2
clr r18 ; 7 fractqqhq2 [length = 2]
3
mov r19,r24
4
mov r27,r22 ; 8 fractqqhq2 [length = 2]
5
clr r26
6
call __mulhq3 ; 17 *mulhq3.call [length = 2]
7
ret ; 26 return [length = 1]
Die Werte weden erst nach fract expandiert (fractqqhq2, von QQmode nach
HQmode), und auf diesen wird dann die Multiplikation erledigt (HQ = HQ *
HQ).
Bedeutung der GCC machine modes findet man z.B. in Tabelle "Machine
Modes" des avr-gcc ABI [1] oder — wie immer — in der GCC-Quelle [2][3].
Je nach Kontext, z.B. bei Inlining und -mrelax, verschwinden auch die
beiden MOVs und das RET.
Register 18/19 und 26/27 werden angefasst, weil __mulhq3 eine
Nicht-ABI-Funktion ist. Dies entnimmt man dem mit -dP erzeugten
Assembler
1
; ([...]
2
; (parallel [(set (reg:HQ 24)
3
; (mult:HQ (reg:HQ 18)
4
; (reg:HQ 26)))
5
; (clobber (reg:HI 22))])
6
; [...])
7
call __mulhq3 ; 17 *mulhq3.call [length = 2]
der GCC-Quelle [4] oder der libgcc [5].
Zum Arbeiten mit impliziten Fixed-Point verwendet man die ?bits?
Funktionen um von einer Welt in die andere zu wechseln. Mit den Headern
wie oben:
Johann L. schrieb:> Yupp. Die Schnittmenge zwischen "int8_t" und "(short) fract" ist { -1,> 0 }. Nicht sonderlich ergiebig ;-)
:-)
Johann L. schrieb:> Na wenn's dir um unnötige Sachen oder die Quintessenz geht, dann ist> volatile eine schlechte Wahl:
Gutes Argument. ich hab das volatile nicht hingeschrieben, ich schwöre!
:-)
Johann L. schrieb:> Zum Arbeiten mit impliziten Fixed-Point verwendet man die ?bits?> Funktionen um von einer Welt in die andere zu wechseln. Mit den Headern> wie oben:> [...]> return bitsr ((fract) fa * fb);
sorry, da bin ich jetzt ausgestiegen: Was genau macht bitsr()? Hier
versagt auch mein Google...
Ansonsten: Vielen dank für deine ausführlichen Erklärungen. Ich denke in
diesen _Fract Datenttypen steckt enorm viel Potential, speziell für
Signalverarbeitung (und allen Bereichen wo man mit [-1..1] arbeitet).
natürlich kann man das "zu Fuß" nachbauen, aber alleine ein 0.42 * 0.8
ist mit "händischer fixed-point arithmetik" lästig weil man dauernd
rechtsscheiben muss bzw. die LSBs wegwerfen. Da wird der Code mit _Fract
mit Sicherheit weit einfacher, besser lesbar, und effizienter
Michael Reinelt schrieb:> Johann L. schrieb:>> Zum Arbeiten mit impliziten Fixed-Point verwendet man die ?bits?>> Funktionen um von einer Welt in die andere zu wechseln. Mit den Headern>> wie oben:>> [...]>> return bitsr ((fract) fa * fb);>> sorry, da bin ich jetzt ausgestiegen: Was genau macht bitsr()? Hier> versagt auch mein Google...
Die Spezifikation steht nicht in Google sondern in IEC TR 18037.
Letzterer legt nicht nur neue Built-in Macros fest, sondern auch einen
Zoo neuer Typen und Funktionen. Die ?bits? Funktionen wandeln
Ganzzahl-Typen in Fixed-Typen um und umgekehrt, ohne dabei die
Darstellung zu ändern.
Um zwischen Integer und float zu wechseln kann man z.B. direkt zuweisen
— was den Wert (soweit möglich) nicht ändert — oder die Bitdarstellung
ungeändert übernehmen. Bei float/integer gibt es dafür keine
Standard-Funktion. Der standardkonforme (und i.d.R effizienteste) Weg
wäre da ein memcpy. Für Fixed gibt es da dedizierte Funktionen für.
Beispiel für accum. accum hat Suffix "k", analog wie "f" zu float
gehört. Damit macht bitsk ein Bitbang von accum zu int_k_t, d.h. zu
einem integralen Typ mit der gleichen Anzahl Bits und gleicher
Signedness, ohne jedoch ein einziges Bit der (Binär)darstellung zu
ändern. Analog kbits: Integer (int_k_t) rein, k (accum) raus:
1
int_k_t/* "long int" bzw. "long long int" mit -mint8 */
2
get_bitsk(void)
3
{
4
returnbitsk(3e-3k)+bitsk(-0x3p-3k);
5
}
Die erste Konstante ist 3E-3 als accum, d.h. 3·10^{-3} = 0.003. Die
zweite Konstante ist -0x3·2^{-3} = -3/8 = -0.375.
Bitbang nach int_k_t (long im üblichen Kontext) ergibt für die
Konstanten den jeweils 2^15-fachen Wert weil accum Signatur s16.15 hat
und long s31. Wert 1 wird also zu 98 (ca. 98.304) und Wert 2 zu -12288.
Deren Summe wird dann zurückgegeben. Das Ergebnis ist also -12190L; als
accum wären das ca. -0.37201. Der Fehler von 0.00001 (ca. 0.3 LSBs)
ergibt sich dadurch, dass 3/1000 nicht exakt als accum darstellbar ist.
Um einen Überblick zu bekommen, was es so an zusätzlichem Werkzeug gibt,
kann man stdfix.h und dessen Includes lesen oder einen Blick in TR 18037
riskieren, dort dann insbesondere in Abschnitt 7.18a.
> Ich denke in diesen _Fract Datenttypen steckt enorm viel Potential,> speziell für Signalverarbeitung (und allen Bereichen wo man> mit [-1..1] arbeitet).
Mit [-1,1). Für größere Bereiche dann eben einen der accums, oder
unsigned fract / accum wenn nicht-negative Werte ausreichen.