Forum: Compiler & IDEs AVR-spezifische Optimierung wirkt nicht in Member-Funktion


von ffonsel (Gast)


Lesenswert?

Hallo,

Ich verwende avr-g++ 4.3.5
ich habe eine Klasse, die einen Port abstrahieren soll.
Die Klasse hat ein Attribut
1
volatile uint8_t* const port_address
und eine Funktion
1
inline bool Port::get_pin(uint8_t pin_number) const
2
{
3
    bool result = *port_address_ & (1 << pin_number);
4
5
    return result;
6
}

Die Funktion ist in einem Header-File

Ich würde erwarten, dass hier mit -Os optimiert wird zu
1
sbis [port_address], [pin_number]

stattdessen bekomme ich (für pin_number = 4)
1
in r24,55-32
2
lsr r24
3
lsr r24
4
lsr r24
5
sbrc r24,0

Es scheint also, dass der Compiler korrekt inferiert, aber nicht die 
AVR-spezifische Optimierung anwendet.
Kompiliere ich die Funktion außerhalb einer Klasse, also z.B. mit einer 
globalen port-Variable und rufe diese Funktion dann mit pin_number als 
Parameter auf, funktioniert alles wie erwartet.

Wie kann ich nun den Compiler dazu bringen, die Optimierung anzuwenden?

tschüss

von Rolf Magnus (Gast)


Lesenswert?

Wie rufst du die Funktion denn auf?

von ffonsel (Gast)


Lesenswert?

Ich habe mit einer normalen Variable, mit einer const-Variable und mit 
einer Zahl/Immediate (das dürfte ja das gleiche wie eine konstante 
Variable als Parameter sein.)

Ich möchte die Funktion möglichst so halten, dass ich eine Konstante 
übergeben kann, also z.B.
1
 MyPort.get_pin(5)
 als auch dynamisch, also zum Beispiel in einer for-Schleife aufrufen. 
Der Compiler hat zu entscheiden, was passiert. Der statische Fall sollte 
aber natürlich nicht langsamer sein, als
1
 if ( PORT.x & (1 << pin_number))
. Klar das ist eigentlich nichts anders. Aber in einem anderen Kontext 
übersetzt der Compiler das in ein sbis mit nachfolgendem conditional 
branch, hier aber komischerweise nicht. Ich meine zwei shifts nach 
links, ein logisches AND und ein test auf null sind genau das gleiche, 
aber ein sbis + branch sind natürlich weniger Instruktionen. Ich frage 
mich also, warum optimiert der Compiler hier nicht bis zum Ende?

Ich habe auch mal zum Test den Parameter für get_bit als const 
deklariert; das sollte ja auf jeden Fall abhelfen, aber falsch gedacht.

von holger (Gast)


Lesenswert?

Versuchs doch mal so:

get_pin(1 << 4)


bool result = *port_address_ & pin_number;

von (prx) A. K. (prx)


Lesenswert?

Rolfs Frage gilt immer noch. Wenn der Compiler die Funktion so aufrufen 
muss, dass sie zwingend einen Wert 0/1 liefert, dann ist es Essig mit 
der von dir gewünschten Optimierung. Deshalb ebenso wichtig, wie die 
Funktion aufgerufen wird, wie deren Inhalt.

Sinnvollerweise baust du ein minimiertes(!) vollständiges Testbeispiel, 
dass idealerweise ohne zusätzliche Files compilierbar ist.

von (prx) A. K. (prx)


Lesenswert?

Minimiertes Gegenbeispiel, quick-and-dirty:
1
struct Port {
2
  Port (unsigned addr) : port((unsigned char *)addr) {}
3
  bool test(int bit) { return (*port & (1 << bit)) != 0; }
4
  volatile unsigned char *port;
5
};
6
7
extern void g();
8
9
void f()
10
{
11
  if (Port(55).test(4))
12
    g();
13
}
erzeugt mit avr-gcc 4.3.4
1
  sbic 55-32,4
2
  rcall _Z1gv

NB: Dass deine Funktion nicht 0/1 zurück gibt, sondern irgendwas 
ungleich 0, das könnte sich irgendwann rächen.

von ffonsel (Gast)


Lesenswert?

Ich verstehe nicht, was damit gemeint ist

A. K. schrieb:
> Wenn der Compiler die Funktion so aufrufen
> muss, dass sie zwingend einen Wert 0/1 liefert

Meinst du damit, dass der Compiler nichts optimiert, sondern quasi den 
Rückgabewert in r24 lässt (siehe assembly unten) und dann der Prolog und 
alles durch das Inlining wegfällt?

Hier mein Code (ich habe den andern Krempel einfach mal weggelassen):

http://pastebin.com/ucNJfnw4

Diese Beispiel kompiliert zu:
1
main:
2
        in r24,55-32
3
        lsr r24
4
        lsr r24
5
        lsr r24
6
        sbrc r24,0
7
        rjmp .L2
8
        cbi 55-32,2
9
.L3:
10
.L5:
11
        rjmp .L5
12
.L2:
13
        sbi 55-32,2
14
        rjmp .L3

Dieses Beispiel hingegen:
1
#include <avr/io.h>
2
3
volatile uint8_t* const port = &PORTB;
4
5
inline bool func(uint8_t x)
6
{
7
    return *port & (1 << x);
8
}
9
10
int main(void)
11
{
12
    uint8_t x = 4;
13
    if (func(x))
14
    {
15
        *port = 0xFF;
16
    }
17
18
    while (true);
19
    return 0;
20
}

Kompiliert zu:
1
main:
2
        sbis 56-32,4
3
        rjmp .L2
4
        ldi r24,lo8(-1)
5
        out 56-32,r24
6
.L2:
7
.L4:
8
        rjmp .L4

Ich verstehe den Unterschied nicht. In beiden Fällen ist das 
PORT-register durch einen const pointer auf volatile uint8_t gegeben und 
der pin wird als (const) parameter übergeben.
Wo ist nun die Grenze für die Optimierung? Die Inline Funktionen sind 
übrigens in den *_impl.h Header-Dateien definiert, also sollte es keine 
"Übersetzungseinheitsgrenzen" geben

holger schrieb:
> Versuchs doch mal so:
>
> get_pin(1 << 4)
>
>
> bool result = *port_address_ & pin_number;

Das liefert mir tatsächlich ein sbic.
Wodran liegt das? In dem Beispiel das ich in diesem Beitrag gegeben 
habe, wird ja auch nur die Pinnummer übergeben und es wird zu einem 
sbis.

häh?

von (prx) A. K. (prx)


Lesenswert?

Könnte sein, dass der interessante Teil genau der ist:
    // hier wird geschaut, ob der Button aktiviert ist,
    // oder sich der Zustand des Buttons geändert hat...
Weil der Port volatile ist muss der Zugriff darauf davor erfolgen. Da 
bleibt dem Compiler meist nichts übrig, als sich den Stand in einem 
Register zu merken. Das Flag-Register hält nicht lang vor.

von ffonsel (Gast)


Lesenswert?

A. K. schrieb im Beitrag #2228283:
> Wie bitteschön soll der Compiler beiinline void Button::Poll()
> ein sbic/sbis produzieren, wo darin ausdrücklich der Portzustand mit
> einer Variablen verglichen wird?

active_high_ ist als const bool deklariert, damit sollte es den 
Unterschied zwischen sbic und sbis machen, sonst nichts.

Aber wie gesagt, wenn ich
1
inline void Button::Poll()
2
{
3
    bool new_state = (port_->get_pin((1 << 3)) == active_high_);
4
    has_changed_ = (new_state != active_);
5
    active_ = new_state;
6
    return;
7
}

einsetze, (parameter geändert) und get_pin anpasse:
1
inline bool Port::get_pin(uint8_t pin_number) const
2
{
3
    bool result = *port_address_ & (pin_number);
4
5
    return result;
6
}

,dann bekomme ich mein sbic.

Wo liegt nun der unterschied, ob ich (1 << 3) übergebe, oder 1 und dann 
in der inline-Funktion (1 << pin_number) mache?

Nur die Pin-Nummer zu übergeben ist natürlich mein Ziel, da es logischer 
und bequemer ist.

von ffonsel (Gast)


Lesenswert?

A. K. schrieb:
> Könnte sein, dass der interessante Teil genau der ist:
>     // hier wird geschaut, ob der Button aktiviert ist,
>     // oder sich der Zustand des Buttons geändert hat...
> Weil der Port volatile ist muss der Zugriff darauf davor erfolgen. Da
> bleibt dem Compiler meist nichts übrig, als sich den Stand in einem
> Register zu merken. Das Flag-Register hält nicht lang vor.

Dieser Teil sieht so aus:
1
    bool active = ButtonB2.is_active();
2
    bool has_changed = ButtonB2.has_changed();
3
    bool activated = ButtonB2.activated();
4
    bool deactivated = ButtonB2.deactivated();
5
6
    bool x = (active && has_changed && activated);
7
8
    PortB.change_pin(2, x);

einfach irgendein Schwachsinn, damit nichts wegoptimiert wird.

von (prx) A. K. (prx)


Lesenswert?

Da hast du doch die Antwort. Die diversen Port-Tests lassen sich anders 
als sonstige Dinge nicht umordnen und dorthin verzögern wo wirklich 
benötigt, weil volatile. Daher finden die in der gegebenen Reihenfolge 
an der Stelle statt, wo sie im Quelltext stehen, und landen somit 
zwangsläufig als Wert in Registern.

Bei if(func(...)) entsteht das Problem nicht, weil die Verwendung des 
Ergebnisses direkt auf den Test folgt.

PS: Bei den const propagations hast du recht, daher hatte ich den Kram 
bereits gelöscht.

von ffonsel (Gast)


Lesenswert?

A. K. schrieb:
> Da hast du doch die Antwort. Die diversen Port-Tests lassen sich anders
> als sonstige Dinge nicht verzögern, weil volatile. Daher finden die in
> der gegebenen Reihenfolge statt und landen somit zwangsläufig als Wert
> in Registern.

Also, übergebe ich (1<<3) bekomme ich das sbic, übergebe nur die Drei 
(als inhalt einer konstanten Variable, erhalte ich folgendes assembly:
1
        in r24,55-32
2
        lsr r24
3
        lsr r24
4
        lsr r24   
5
        sbrc r24,0      
6
        rjmp .L2
7
        cbi 55-32,2
8
.L3:
9
.L5:
10
        rjmp .L5        ;bla bla und so weiter
11
.L2:
12
        sbi 55-32,2
13
        rjmp .L3

Drei mal left shift und dann wird gebrancht wenn der Inhalt vom Bit ganz 
rechts gleich 0 ist. Das kann doch ohne Probleme zu einem sbis r24, 3 
optimiert werden?

> PS: Bei den const propagations hast du recht, daher hatte ich den Kram
> bereits gelöscht.
Habe, ich dann auch gesehen, ist ja kein Ding.

von (prx) A. K. (prx)


Lesenswert?

> Drei mal left shift und dann wird gebrancht wenn der Inhalt vom Bit ganz
> rechts gleich 0 ist. Das kann doch ohne Probleme zu einem sbis r24, 3
> optimiert werden?

Kann. Aber ich habe bisher keinen Quellcode gesehen, der einen Pin 
ändert. NB: lsr schiebt rechts.

Wenn du dauernd unvollständige Codefragmente vorsetzt, dann artet das in 
Glaskugelspiele aus. Hier fehlt jetzt vermutlich change_pin.

Bring mal Code, der compilierbar ist (linkfähig muss er nicht sein), 
möglichst alles in einem File, keinerlei #includes (auch da ich grad 
keine Standardincludes parat habe). Und lass alles weg was unnötig.

von ffonsel (Gast)


Lesenswert?

A. K. schrieb:
> NB: lsr schiebt rechts.

Richtig, ich kann das nicht auseinanderhalten.

A. K. schrieb:
> Aber ich habe bisher keinen Quellcode gesehen, der einen Pin
> ändert.

Das soll ja auch extern geschehen.

A. K. schrieb:
> Bring mal Code, der compilierbar ist (linkfähig muss er nicht sein),
> möglichst alles in einem File, keinerlei #includes. Und lass alles weg
> was unnötig.

Hier:

http://pastebin.com/rvxj8piZ

das Ergebnis ist bei mir (avr-g++ -S -Os -mmcu=atmega8 test.cc):
1
  .file  "test.cc"
2
__SREG__ = 0x3f
3
__SP_H__ = 0x3e
4
__SP_L__ = 0x3d
5
__CCP__  = 0x34
6
__tmp_reg__ = 0
7
__zero_reg__ = 1
8
  .text
9
.global  main
10
  .type  main, @function
11
main:
12
/* prologue: function */
13
/* frame size = 0 */
14
  in r24,55-32
15
  lsr r24
16
  lsr r24
17
  lsr r24
18
  sbrs r24,0
19
.L3:
20
.L5:
21
  rjmp .L5
22
.L2:
23
  sbi 55-32,2
24
  rjmp .L3
25
  .size  main, .-main

von (prx) A. K. (prx)


Lesenswert?

Bestätigt. Maske geht, Bit nicht.

Tja, da kann ich dich nur dafür beglückwünschen, dass es dir gelungen 
ist, die Optimierung vom GCC auszuhebeln. Nobody is perfect, auch der 
nicht.

von holger (Gast)


Lesenswert?

>Wo liegt nun der unterschied, ob ich (1 << 3) übergebe, oder 1 und dann
>in der inline-Funktion (1 << pin_number) mache?

Du hast einen variablen Shift, und das ist immer Mist.

von (prx) A. K. (prx)


Lesenswert?

Der ist nicht variabel. Der erzeugte Code zeigt das ja auch.

von holger (Gast)


Lesenswert?

>Der ist nicht variabel. Der erzeugte Code zeigt das ja auch.

Ja, für die 3. Gibt es auch ein Beispiel für 4 oder 5
zusätzlich im Code?

von (prx) A. K. (prx)


Lesenswert?

Mit nicht-konstanter Bitnummer ist das trivialerweise nicht optimierbar, 
aber darum geht es hier nicht. Wärs bei if(PINA & (1<<n)) auch nicht.

von ffonsel (Gast)


Lesenswert?

holger schrieb:
> Ja, für die 3. Gibt es auch ein Beispiel für 4 oder 5
> zusätzlich im Code?

bis 3 wird lsr angewendet, dadrüber gibt es zuerst einen swap gefolgt 
von lsr.
swap vertauscht die nibble.

holger schrieb:
> Du hast einen variablen Shift, und das ist immer Mist.

Der Shift soll ja im Quelltext möglichst Variabel sein, so dass man die 
Möglichkeit hat Pins variabel zu setzen (also beispielsweise, abhängig 
von einer bestimmten Eingabe), oder eben statisch. Je nachdem was man 
möchte hat man damit das gleiche Interface, aber jeweils das richtige 
(statisch oder variabel) Resultat. Es gibt natürlich noch ca. 20 andere 
Funktionen um mehrer Pins auf einmal zu setzen, oder dem Port-Register 
einen anderen Zustand zuzuweisen, usw.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Möglicherweise einer von

http://gcc.gnu.org/PR42210 (fixed in 4.7.0)
http://gcc.gnu.org/PR33049 (Patch verfügbar, nicht upstream)

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Ja, ist PR33049.

Wer besseren Code sehen will, muss sich also bis zu avr-gcc 4.7 
gedulden. Leider sind solche Patches nicht gern gesehen, bisher ist es 
noch nicht in GCC integriert. Gegebenenfalls wäre die erzeugte 
Instruktionsfolge
1
  bst r24, 3
2
  clr r24
3
  bld r24, 0

Oder das Pflasterchen [1] selbst einbauen; es funktioniert auch mit 
älteren Versionen von avr-gcc.

[1] http://gcc.gnu.org/bugzilla/attachment.cgi?id=24277&action=diff

von ffonsel (Gast)


Lesenswert?

Vielen Dank!

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Da du avr-gcc offenbar selbst generierst:

1. Ist der Code mit dem Patch wie erwartet?

2. configure im Quellverzeichnis oder einem Unterverzeichnis
   davon wird nicht unterstützt. Rechne mit allen Sorten von
   seltsamen Effekten:
   http://gcc.gnu.org/install/configure.html

3. Ein Bugreport ohne Quellen ist nicht hilfreich, da niemand
   das Problem nachvollziehen kann — allein aus dem Bugreport
   noch nichtmal du selbst:
   http://gcc.gnu.org/bugs/#need

von ffonsel (Gast)


Lesenswert?

Ich hatte nochmal etwas freie Zeit.

Johann L. schrieb:
> 1. Ist der Code mit dem Patch wie erwartet?

Nein, genauso wie vorher. Allerdings habe ich gcc-4.6.0 kompiliert. Da 
scheinen im avr.md file ca. 200 Zeilen zu fehlen. Ich werde 4.7 bei 
Gelegenheit nochmal testen.

Johann L. schrieb:
> 3. Ein Bugreport ohne Quellen ist nicht hilfreich, da niemand
>    das Problem nachvollziehen kann — allein aus dem Bugreport
>    noch nichtmal du selbst:
>    http://gcc.gnu.org/bugs/#need

Danke, kenne ich schon.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

ffonsel schrieb:
> Ich hatte nochmal etwas freie Zeit.
>
> Johann L. schrieb:
>> 1. Ist der Code mit dem Patch wie erwartet?
>
> Nein, genauso wie vorher. Allerdings habe ich gcc-4.6.0 kompiliert.

Klar da ist die Äanderung ja auch nicht drinne. In 4.6 müsstest du die 
paar Zeilen von Hand hinzufügen.

von ffonsel (Gast)


Lesenswert?

Johann L. schrieb:
> Klar da ist die Äanderung ja auch nicht drinne. In 4.6 müsstest du die
> paar Zeilen von Hand hinzufügen.


Ich habe schon nicht vergessen zu patchen ;)

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

ffonsel schrieb:
> Johann L. schrieb:
>> Klar da ist die Äanderung ja auch nicht drinne. In 4.6 müsstest du die
>> paar Zeilen von Hand hinzufügen.
>
> Ich habe schon nicht vergessen zu patchen ;)

Auch neu generiert und installiert? *wegduck*

hmmm. Dann würd ich mir das nochmal anschauen falls das ii-File am 
Bugreport hinge.

von ffonsel (Gast)


Lesenswert?

Ich habe gerade den HEAD von heute kompiliert, mit und ohne Patch keine 
Änderung.

Ich fühle mich nicht danach, den bug zu filen, aber falls das jemand 
machen möchte sind hier alle nötigen Information

test.ii (intermediate)  http://pastebin.com/ZPV521si
test.s (ausgabe)   http://pastebin.com/DDFW0FCU

compiler-output (verbose) http://pastebin.com/XHNPB6WX

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

ffonsel schrieb:

> Ich fühle mich nicht danach, den bug zu filen...

Da:

http://gcc.gnu.org/PR49446

von ffonsel (Gast)


Lesenswert?

Oh, guter Detektiv, ich habe es nochmal rangehangen.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Bedenke, daß ein vernünftiger Fehlerreport mit allen notwendigen 
Informationen im Report notwendig ist, so daß überhaupt eine Arbeit an 
dem Problem möglich ist.

GCC-Entwickler haben idR keine Lust und Zeit, jemand die notwendigen 
Infos aus der Nase zu ziehen.

von ffonsel (Gast)


Lesenswert?

Johann L. schrieb:
> Bedenke, daß ein vernünftiger Fehlerreport mit allen notwendigen
> Informationen im Report notwendig ist, so daß überhaupt eine Arbeit an
> dem Problem möglich ist.
>
> GCC-Entwickler haben idR keine Lust und Zeit, jemand die notwendigen
> Infos aus der Nase zu ziehen.

Eigentlich hatte ich beim ersten Eintrag gedacht ich hätte alles 
rangehangen, aber irgendwo hat der File upload nicht geklappt, war nicht 
meine Absicht. Aber dann bei mir ist die frustgrenze dann meistens auch 
schnell erreicht, weil ich gar nicht im GCC irgendwo rumwühlen will und 
zwanzig Varianten ausprobieren, sondern ich will einfach nur "fertig 
werden".. anyway jetzt ist es da.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

http://gcc.gnu.org/PR33049 ist gefixt in 4.7.0.

Für einen Backport brauchst du allerdings nicht nur das Patch am PR (SVN 
175269), sondern auch die Prädikate "const1_operand" und 
"const_0_to_7_operand" aus ./gcc/config/avr/predicated.md. Die gibt's in 
älteren Versionen noch nicht:

http://gcc.gnu.org/viewcvs/trunk/gcc/config/avr/predicates.md?content-type=text%2Fplain&view=co

Viel Spaß.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.