www.mikrocontroller.net

Forum: Projekte & Code GCC Inline Assembler Multiplikationen


Autor: Dirk Schmidt (disc)
Datum:
Angehängte Dateien:

Bewertung
0 lesenswert
nicht lesenswert
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

Autor: A. K. (prx)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
"=&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.

Autor: Johann L. (gjlayde) Benutzerseite
Datum:

Bewertung
0 lesenswert
nicht lesenswert
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:
int32_t mul_s16xu8_s32(int16_t nNumber1, uint8_t ucNumber2)
{
   int32_t lResult;
   uint8_t zero = 0;

   asm (
         "clr    %C[lResult]" "\n\t"
         "clr    %D[lResult]" "\n\t"
         "mul    %A[nNumber1], %[ucNumber2]" "\n\t" // al * b
         "movw   %A[lResult], r0" "\n\t"
         "mulsu  %B[nNumber1], %[ucNumber2]" "\n\t" // (signed)ah * b
         "sbc    %D[lResult], %[zero]" "\n\t"
         "add    %B[lResult], r0" "\n\t"
         "adc    %C[lResult], r1" "\n\t"
         "adc    %D[lResult], %[zero]" "\n\t"
         "clr    __zero_reg__" "\n\t"
         : [lResult] "=&r" (lResult)
         : [nNumber1] "a" (nNumber1), [ucNumber2] "a" (ucNumber2), [zero] "r" (zero)
       );

   return lResult;
}

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

Autor: A. K. (prx)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
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.

Autor: Johann L. (gjlayde) Benutzerseite
Datum:

Bewertung
0 lesenswert
nicht lesenswert
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:
char x;

void foo (void)
{
    
    if (((uint8_t) (x+1)) == 0)
    {
        uint8_t zero = 0;
        
        asm (" ; x=%[x], zero=%[zero]" : [x] "+r" (x) : [zero] "r" (zero));
    }
}

Das wird zu
foo:
  lds r25,x   ;  x, x   ;  10  *movqi/4  [length = 2]
  mov r24,r25   ;  tmp42, x   ;  35  *movqi/1  [length = 1]
  subi r24,lo8(-(1))   ;  tmp42,   ;  11  addqi3/2  [length = 1]
  brne .L7   ; ,   ;  13  branch  [length = 1]
/* #APP */
   ; x=r25, zero=r24   ;  x, tmp42
/* #NOAPP */
  sts x,r25   ;  x, x   ;  23  *movqi/3  [length = 2]
.L7:
  ret   ;  37  return  [length = 1]

D.h. weil gcc weiß, das in R24 schon eine 0 drinne ist, verwendet er 
sie.

Johann

Autor: A. K. (prx)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
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.

Autor: Johann L. (gjlayde) Benutzerseite
Datum:

Bewertung
0 lesenswert
nicht lesenswert
...und der Vollständigkeit halber gehören dann auch 16=8*16 dazu:
static inline uint16_t 
mul_u16xu8_u16 (uint16_t unNumber1, uint8_t ucNumber2)
{   
    uint16_t unResult;

    asm (
        "mul    %A[unNumber1], %[ucNumber2]" "\n\t" // al * b
        "movw   %A[unResult], r0"            "\n\t"
        "mul    %B[unNumber1], %[ucNumber2]" "\n\t" // ah * b
        "add    %B[unResult], r0"            "\n\t"
        "clr    __zero_reg__"
            : [unResult] "=&r" (unResult)
            : [unNumber1] "r" (unNumber1), [ucNumber2] "r" (ucNumber2)
    );

    return unResult;
}

// etc.

Autor: Dirk Schmidt (disc)
Datum:
Angehängte Dateien:

Bewertung
0 lesenswert
nicht lesenswert
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

Autor: Johann L. (gjlayde) Benutzerseite
Datum:

Bewertung
0 lesenswert
nicht lesenswert
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...

> 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.

Autor: Dirk Schmidt (disc)
Datum:
Angehängte Dateien:

Bewertung
0 lesenswert
nicht lesenswert
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

Autor: A. K. (prx)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Nochmal: Bei den einfachen Versionen ist "=&r" nicht sinnvoll, "=r" 
reicht aus und erlaubt es dem Compiler, die gleichen Register zu 
verwenden.

Autor: Johann L. (gjlayde) Benutzerseite
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Hinweis:

Ab avr-gcc 4.7 kann der Compiler selbst das Produkt erweiter, d.h. bei 
Code wie
long mul (shart a, short b)
{
    return (long) a * b;
}
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:
unsigned int div16_10 (unsigned int x)
{
    return x / 10;
}

unsigned char div8_10 (unsigned char x)
{
    return x / 10;
}
wird zB mit -O2 für ATmega8 übersetzt zu
div16_10:
  movw r18,r24
  ldi r26,lo8(-13107)
  ldi r27,hi8(-13107)
  rcall __umulhisi3 ; 32 = 16 * 16
  lsr r25
  ror r24
  lsr r25
  ror r24
  lsr r25
  ror r24
  ret

div8_10:
  ldi r25,lo8(-51)
  mul r24,r25
  mov r24,r1
  clr __zero_reg__
  lsr r24
  lsr r24
  lsr r24
  ret

Antwort schreiben

Die Angabe einer E-Mail-Adresse ist freiwillig. Wenn Sie automatisch per E-Mail über Antworten auf Ihren Beitrag informiert werden möchten, melden Sie sich bitte an.

Wichtige Regeln - erst lesen, dann posten!

  • Groß- und Kleinschreibung verwenden
  • Längeren Sourcecode nicht im Text einfügen, sondern als Dateianhang

Formatierung (mehr Informationen...)

  • [c]C-Code[/c]
  • [avrasm]AVR-Assembler-Code[/avrasm]
  • [code]Code in anderen Sprachen, ASCII-Zeichnungen[/code]
  • [math]Formel in LaTeX-Syntax[/math]
  • [[Titel]] - Link zu Artikel
  • Verweis auf anderen Beitrag einfügen: Rechtsklick auf Beitragstitel,
    "Adresse kopieren", und in den Text einfügen




Bild automatisch verkleinern, falls nötig
Bitte das JPG-Format nur für Fotos und Scans verwenden!
Zeichnungen und Screenshots im PNG- oder
GIF-Format hochladen. Siehe Bildformate.
Hinweis: der ursprüngliche Beitrag ist mehr als 6 Monate alt.
Bitte hier nur auf die ursprüngliche Frage antworten,
für neue Fragen einen neuen Beitrag erstellen.

Mit dem Abschicken bestätigst du, die Nutzungsbedingungen anzuerkennen.