Hallo Leute,
da ich in einem Projekt von mir viele Berechnungen mit Zahlen
unterschiedlichen Formates habe, habe ich mir mal ein paar
Inline-Assembler Funktionen geschrieben, um diese ein wenig zu
optimieren.
Da ich nur bescheidene Assembler Kenntnisse habe, wäre es schön, wenn
der eine oder andere mal einen Blick darauf werden könnte, ob alles so
in Ordnung ist und ob man noch etwas optimieren könnte.
Evtl. kann man die Funktionen noch als inline deklarieren.
Ein Modul mit ein paar Testfunktionen ist auch beigefügt.
Ansonsten stehen die Funktionen für alle zu Benutzung frei.
Gruß
Dirk
"=&r" ist nur erforderlich, wenn das Ergebnis nicht im gleichen Register
stehen darf wie die Operanden. Wenn das beispielsweise der Befehl nicht
zulässt, oder wenn die Operanden stückweise verarbeitet werden.
Mindestens bei den 8x8=>16 Multiplikationen ist "=r" ausreichend.
Ich würde dringend empfehlen, die kurzen Funktionen ins .h File zu
stellen und mit "static inline" zu markieren. Dann kann der Compiler sie
ohne Aufruf direkt verwenden, was die Sache grad bei den
8x8-Multiplikationen erheblich beschleunigt.
R2 gehört zu den Registern, die gesichert werden müssen, daher wird der
Compiler bei dessen Verwendung PUSH/POP einschieben. Mir scheint der in
Funktionen nicht zu sichernde Registerbereich sinnvoller, also
beispielsweise R31.
Wenn du "optimieren" sagst, muss man schon wissen, was zu optimieren
ist...
-- Laufzeit?
-- Codedichte?
-- Entwicklungszeit?
-- Portabilität?
-- ...
So wie A. schon schrieb, ist R2 ungünstig als Clobber-Register. Besser
definierst du eine lokale Variable, und lässt den Compiler die
Allokierung übernehmen:
1 | int32_t mul_s16xu8_s32(int16_t nNumber1, uint8_t ucNumber2)
| 2 | {
| 3 | int32_t lResult;
| 4 | uint8_t zero = 0;
| 5 |
| 6 | asm (
| 7 | "clr %C[lResult]" "\n\t"
| 8 | "clr %D[lResult]" "\n\t"
| 9 | "mul %A[nNumber1], %[ucNumber2]" "\n\t" // al * b
| 10 | "movw %A[lResult], r0" "\n\t"
| 11 | "mulsu %B[nNumber1], %[ucNumber2]" "\n\t" // (signed)ah * b
| 12 | "sbc %D[lResult], %[zero]" "\n\t"
| 13 | "add %B[lResult], r0" "\n\t"
| 14 | "adc %C[lResult], r1" "\n\t"
| 15 | "adc %D[lResult], %[zero]" "\n\t"
| 16 | "clr __zero_reg__" "\n\t"
| 17 | : [lResult] "=&r" (lResult)
| 18 | : [nNumber1] "a" (nNumber1), [ucNumber2] "a" (ucNumber2), [zero] "r" (zero)
| 19 | );
| 20 |
| 21 | return lResult;
| 22 | }
|
Wie du siehst, ist das asm auch nicht mehr volatile: Wenn das Ergebnis
nicht gebraucht wird, darf der Compiler den kompletten asm-Schnippel in
die Tonne werfen, denn wir haben alle Nebenwirkungen beschrieben!
8x8=16 kann gcc und macht kein schlecheren Code als deine Asm-Stücke. Er
ist sogar besser, weil er's inline macht und nicht als Call.
Bei mul_u16xu16_u32, mul_u32xu16_u32 und mul_u32xu8_u32 brauchst du kein
zero-Register. Du kannst ebenso R1 aka _zero_reg_ nehmen (natürlich an
den verwendeten Stellen nach vorherigen clr). Ok, braucht hier und da
nen Tick und 2 Byte Code mehr...
Leider ist es mit dem momentanen AVR-Backend nicht möglich, die
Schnippel als taransparente Calls umzusetzen :-( Mit besseren
Constraint-Definitionen in avr-gcc wäre das problemlos möglich und
könnte sehr gewinnbringend eingesetzt werden.
Johann
Johann L. schrieb:
> So wie A. schon schrieb, ist R2 ungünstig als Clobber-Register. Besser
> definierst du eine lokale Variable, und lässt den Compiler die
> Allokierung übernehmen:
Nur kostet dann der =0 Löschbefehl. Wird direkt ein passenderes Register
als R2 verwendet, dann kostet es zumindest in separater Funktion
überhaupt nichts.
A. K. schrieb:
> Johann L. schrieb:
>
>> So wie A. schon schrieb, ist R2 ungünstig als Clobber-Register. Besser
>> definierst du eine lokale Variable, und lässt den Compiler die
>> Allokierung übernehmen:
>
> Nur kostet dann der =0 Löschbefehl. Wird direkt ein passenderes Register
> als R2 verwendet, dann kostet es zumindest in separater Funktion
> überhaupt nichts.
Ich versteh jetzt nicht was du damit meinst. Die 0 muss ja irgendwie ins
Register kommen. Von nix kommt eben nix. Und wenn gcc weiß, daß die 0
rein soll, und er sie schon irgendwo hat, hat er so die Chance, das
Wissen zu nutzen. Hier mal ein konstruiertes Beispiel:
1 | char x;
| 2 |
| 3 | void foo (void)
| 4 | {
| 5 |
| 6 | if (((uint8_t) (x+1)) == 0)
| 7 | {
| 8 | uint8_t zero = 0;
| 9 |
| 10 | asm (" ; x=%[x], zero=%[zero]" : [x] "+r" (x) : [zero] "r" (zero));
| 11 | }
| 12 | }
|
Das wird zu 1 | foo:
| 2 | lds r25,x ; x, x ; 10 *movqi/4 [length = 2]
| 3 | mov r24,r25 ; tmp42, x ; 35 *movqi/1 [length = 1]
| 4 | subi r24,lo8(-(1)) ; tmp42, ; 11 addqi3/2 [length = 1]
| 5 | brne .L7 ; , ; 13 branch [length = 1]
| 6 | /* #APP */
| 7 | ; x=r25, zero=r24 ; x, tmp42
| 8 | /* #NOAPP */
| 9 | sts x,r25 ; x, x ; 23 *movqi/3 [length = 2]
| 10 | .L7:
| 11 | ret ; 37 return [length = 1]
|
D.h. weil gcc weiß, das in R24 schon eine 0 drinne ist, verwendet er
sie.
Johann
Johann L. schrieb:
> Ich versteh jetzt nicht was du damit meinst.
War Blödsinn. Sorry, ich hatte das nicht komplett gelesen, sondern die
Variable als direkten Ersatz für R2 verstanden.
...und der Vollständigkeit halber gehören dann auch 16=8*16 dazu:
1 | static inline uint16_t
| 2 | mul_u16xu8_u16 (uint16_t unNumber1, uint8_t ucNumber2)
| 3 | {
| 4 | uint16_t unResult;
| 5 |
| 6 | asm (
| 7 | "mul %A[unNumber1], %[ucNumber2]" "\n\t" // al * b
| 8 | "movw %A[unResult], r0" "\n\t"
| 9 | "mul %B[unNumber1], %[ucNumber2]" "\n\t" // ah * b
| 10 | "add %B[unResult], r0" "\n\t"
| 11 | "clr __zero_reg__"
| 12 | : [unResult] "=&r" (unResult)
| 13 | : [unNumber1] "r" (unNumber1), [ucNumber2] "r" (ucNumber2)
| 14 | );
| 15 |
| 16 | return unResult;
| 17 | }
| 18 |
| 19 | // etc.
|
Hallo Leute,
vielen Dank erst mal für die Antworten und die Tipps.
Ich habe ein paar von den Vorschlägen eingebaut und hier wieder
angehängt. Die Funktionen sind alle als static inline im Header-File
deklariert. Das hat evtl. den kleinen Nachteil, dass sie vom Compiler
immer "ge-inlined" ;-) werden, also etwas mehr Platz brauchen.
> 8x8=16 kann gcc und macht kein schlecheren Code als deine Asm-Stücke. Er
> ist sogar besser, weil er's inline macht und nicht als Call.
Hm, das wusste ich gar nicht. Ist das bei allen Optimierungsstufen so?
Ich dachte, errechnet dann 8bit x 8bit -> 8bit und konvertiert danach
erst in 16bit?!
Die 16x8 -> 16 Geschichte gehe ich evtl. in den nächsten Tagen mal an
und reiche die dann nach.
Gruß
Dirk
Dirk Schmidt schrieb:
> Ich habe ein paar von den Vorschlägen eingebaut und hier wieder
> angehängt. Die Funktionen sind alle als static inline im Header-File
> deklariert. Das hat evtl. den kleinen Nachteil, dass sie vom Compiler
> immer "ge-inlined" ;-) werden, also etwas mehr Platz brauchen.
Falls man Funktionen oft braucht, kann man sie ja in normale Funktionen
einpacken.
>> 8x8=16 kann gcc und macht kein schlecheren Code als deine Asm-Stücke. Er
>> ist sogar besser, weil er's inline macht und nicht als Call.
> Hm, das wusste ich gar nicht. Ist das bei allen Optimierungsstufen so?
Die Pattern "mulqihi3" und "umulqihi3" für 16=8x8 sind zumindest im
avr-Backend beschrieben. Sie sind daher für alle O-Stufen gültig.
http://gcc.gnu.org/viewvc/trunk/gcc/config/avr/avr.md?revision=152958&view=markup
> Ich dachte, errechnet dann 8bit x 8bit -> 8bit und konvertiert danach
> erst in 16bit?!
Das wäre der Fall mit -mint8 wo ein int nur 8 Bit hat.
> Die 16x8 -> 16 Geschichte gehe ich evtl. in den nächsten Tagen mal an
> und reiche die dann nach.
Schade daß man das alles von Hand machen muss... Im AVR-Backend würde
sich sowas ganz gut machen. Muss nur jemand einbauen, der da mitspielen
darf wie Jörg.
Hallo,
hier noch die fehlenden 16x8->16 Funktionen.
>Schade daß man das alles von Hand machen muss... Im AVR-Backend würde
>sich sowas ganz gut machen. Muss nur jemand einbauen, der da mitspielen
>darf wie Jörg.
Wenn man ein paar dieser Optimierungen einbauen würde, könnte man
bestimmt noch etwas an Geschwindigkeit und Platz herausholen.
Ich hab das bei mir gemerkt. Die Berechnungen sind schneller und der
Code kleiner (mit -O3) geworden.
Vielleicht hat der eine oder andere ja auch noch ähnliche Makros für
Division in der Schublade liegen?
Wobei ich jetzt gar nicht weiss, wie z.B. eine 32 : 8 Division intern
vom GCC gerechnet wird!? Als 32 : 8 oder 32 : 32?
Gruß
Dirk
Nochmal: Bei den einfachen Versionen ist "=&r" nicht sinnvoll, "=r"
reicht aus und erlaubt es dem Compiler, die gleichen Register zu
verwenden.
Hinweis:
Ab avr-gcc 4.7 kann der Compiler selbst das Produkt erweiter, d.h. bei
Code wie 1 | long mul (shart a, short b)
| 2 | {
| 3 | return (long) a * b;
| 4 | }
|
wird nicht vor der Multiplikation auf 32 Bits erweitert, sondern während
der Operation.
Unterstützt werden:
16 = 16 × 8
16 = 8 × 8
32 = 32 × 16
32 = 32 × 8
32 = 16 × 16
32 = 16 × 8
32 = 8 × 8
für signed und unsigned, allerdings nur bei vorhandener MUL-Instruktion.
Diese erweiternde Multiplikation kann auch für Divisionen mit konstantem
Divisior Verwendung finden: 1 | unsigned int div16_10 (unsigned int x)
| 2 | {
| 3 | return x / 10;
| 4 | }
| 5 |
| 6 | unsigned char div8_10 (unsigned char x)
| 7 | {
| 8 | return x / 10;
| 9 | }
|
wird zB mit -O2 für ATmega8 übersetzt zu 1 | div16_10:
| 2 | movw r18,r24
| 3 | ldi r26,lo8(-13107)
| 4 | ldi r27,hi8(-13107)
| 5 | rcall __umulhisi3 ; 32 = 16 * 16
| 6 | lsr r25
| 7 | ror r24
| 8 | lsr r25
| 9 | ror r24
| 10 | lsr r25
| 11 | ror r24
| 12 | ret
| 13 |
| 14 | div8_10:
| 15 | ldi r25,lo8(-51)
| 16 | mul r24,r25
| 17 | mov r24,r1
| 18 | clr __zero_reg__
| 19 | lsr r24
| 20 | lsr r24
| 21 | lsr r24
| 22 | ret
|
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
|