Hallo,
ich habe neulich eine Beobachtung gemacht, die vielleicht den einen oder
anderen interessieren könnte.
also, auf Cortex-M sind 32bit-Operationen schneller als 8bit, weil die
Maskierung entfällt. Das gilt auch beim Laden aus dem Speicher. Etwa
lokale Zähler und Schleifenvariablen sind daher als 32bit-Integer
schneller als in 8bit, auch wenn man nur den 8bit-Wertebereich braucht.
Daher dachte ich nun, daß bei einer Lookup-Tabelle ebenfalls 32bit
sinnvoll sind, auch wenn als Datentyp der Array-Elemente ein (u)int8_t
ausreichen würde.
Pustekuchen. Benchmarks haben gezeigt (C99, ARM-GCC 5.4.1 mit -O2), daß
die Tabellen in 8bit schneller sind, jedenfalls wenn man sie abhängig
von dynamischen Variablen indexiert anspricht, mit "table[foo*(bar+1)]"
oder so und nicht via "*table++".
Der Grund ist, daß beim Berechnen des Offsets der Index im Falle von
8bit-Tabelleninhalten direkt schon in Bytes ist, während er bei 32bit
erst noch geshiftet werden muß, was nicht umsonst ist.
Das Nette ist, daß man mit 8bit Speicher spart und zugleich ein bißchen
Performance gewinnt.
Cortex M kann Laden/Speicher mit Angabe der Speicheradresse über ein
"Basis-Register" + "Index-Register", wobei letzteres mit (1/)2/4/8
skaliert werden kann, d.h. es können Arrays aus 8-/16-/32-/64-Bit Werten
direkt über eine Laufvariable (i) angesprochen werden.
Dauert immer gleich lang.
Falls der Compiler das weis.
Bytes legt man aber schon aus Platzgründen "gepackt" an.
BTW, Intel Maschinen können das auch, Mainframes (gefühlt heute nicht
mehr im Einsatz, aber Gefühle täuschen manchmal ;-) können das, ...
AVR's leider nicht ;-(
Carl D. schrieb:> Falls der Compiler das weis.
Dann scheint es, daß GCC das zumindest in 5.4.1 nicht weiß.
Oder aber das Einrichten der Skalierung kostet selber erstmal Zeit.
Einen eigenen Assembler-Befehl habe ich so auf Anhieb im instruction set
dafür nicht gesehen - gibt's den?
> Bytes legt man aber schon aus Platzgründen "gepackt" an.
Jein; mitunter ist der Platzbedarf egal, weil nur Geschwindigkeit zählt.
Die Lookup-Tabellen sind übrigens genau deswegen, obwohl statisch,
trotzdem ins RAM gelinkt.
Nop schrieb:> Carl D. schrieb:>>> Falls der Compiler das weis.>> Dann scheint es, daß GCC das zumindest in 5.4.1 nicht weiß.>> Oder aber das Einrichten der Skalierung kostet selber erstmal Zeit.> Einen eigenen Assembler-Befehl habe ich so auf Anhieb im instruction set> dafür nicht gesehen - gibt's den?
LDR und STR und das ARM Programmierhandbuch für Cortex M(3).
"Base Scaled Index" ist eine Adressierungsart. (man darf auch noch eine
Konstante bis 255 dazu tun).
Wobei natürlich table[foo*(Bar+1)] (2-dimensionaler wahlfreier Zugriff)
damit nicht machbar ist, nur das
> Der Grund ist, daß beim Berechnen des Offsets der Index im Falle von> 8bit-Tabelleninhalten direkt schon in Bytes ist, während er bei 32bit> erst noch geshiftet werden muß, was nicht umsonst ist.
das Skalieren von (foo*(bar+1)) auf die Größe der Arrayelemente von
2/4/8 Bytes, das kann die Maschine selbst und umsonst.
Hi Nop,
interessantes Thema, um das Ganze greifbarer zu machen, fehlen jedoch
ein paar Infos, z.B.:
- Hardware (M0..7)
- Systemsetup (Speichermodell, Waitstates, ...)
- Beispielcode ggf. Disassembly
- quantitative Ergebnisse, auch bei anderen Optimierungsstufen, Zeiten
und und Speicherverbrauch
- Vergleich mit anderen Compilern insbesondere Keil und IAR
Grüße,
marcus
Carl D. schrieb:> LDR und STR und das ARM Programmierhandbuch für Cortex M(3).
Ah ich habs für den M4 gefunden, Kapitel 3.4.3 "LDR and STR, register
offset":
op{type}{cond} Rt, [Rn, Rm {, LSL #n}]
"The offset is specified by the register Rm and can be shifted left by
up to 3 bits using LSL".
> Wobei natürlich table[foo*(Bar+1)] (2-dimensionaler wahlfreier Zugriff)> damit nicht machbar ist
Ist klar - was ich damit nur ausschließen wollte, ist ein Zugriff wie
"table[3]", wo man den Shift auch schon zur Compilezeit machen könnte.
> das Skalieren von (foo*(bar+1)) auf die Größe der Arrayelemente von> 2/4/8 Bytes, das kann die Maschine selbst und umsonst.
Skurril, wenn ich was messe, wo ich nichts messen sollte. Muß ich mal am
WE gründlicher nachforschen.
Nop schrieb:> Die Lookup-Tabellen sind übrigens genau deswegen, obwohl statisch,> trotzdem ins RAM gelinkt.
Das ist gar nicht unbedingt besser, mit Pech sogar langsamer (aber dafür
deterministisch), denn manche STM32 haben beim Flash ja einen Cache und
der ist ja auch über einen extra Bus angebunden.
Nop schrieb:> Skurril, wenn ich was messe, wo ich nichts messen sollte. Muß ich mal am> WE gründlicher nachforschen.
Assembly Listing ist hier wie üblich sehr hilfreich...
Nop schrieb:> also, auf Cortex-M sind 32bit-Operationen schneller als 8bit, weil die> Maskierung entfällt. Das gilt auch beim Laden aus dem Speicher. Etwa> lokale Zähler und Schleifenvariablen sind daher als 32bit-Integer> schneller als in 8bit, auch wenn man nur den 8bit-Wertebereich braucht.
Ja, diese Beobachtung habe ich für STM32 auch bereits vor ein paar
Jahren gemacht. Seitdem benutze ich u.a. für Schleifenvariablen, für die
ein uint8_t vom Wertebereich ausreicht, immer den uint_fast8_t Typ.
Ich könnte da zwar aus Bequemlichkeit auch einfach uint32_t (oder auch
unsigned int) hinschreiben, aber es gibt 2 Gründe, warum ich lieber
uint_fast8_t benutze:
1. Portabilität: Wenn der Code sowohl auf STM32 als auch auf AVR
laufen soll, empfiehlt sich ein uint_fast8_t, um den AVR
nicht durch Verwendung eines uint32_t ins Schwitzen zu bringen.
Konkrete Anwendung ist hier IRMP, welche auf vielen
verschiedenen µCs zum Einsatz kommt. So läuft der Code auf
allen Prozessoren mit optimaler Geschwindigkeit - egal ob
8- oder 32-Bitter.
2. Codeverständnis: Mit der Verwendung von uint_fast8_t deute ich
an, dass der Wertebereich der Variablen von 0 bis 255 geht, auch
wenn dann konkret auf STM32 letztendlich eine 32-Bit-Variable
verwendet wird.
Marcus H. schrieb:> - Hardware (M0..7)
Cortex-M4, genauer gesagt STM32F405.
> - Systemsetup (Speichermodell, Waitstates, ...)
Kein externer Speicher, Waitstates nach Datenblatt - 5 bei 168MHz.
Dcache/Icache enabled. Also die übliche Konfiguration.
> - Beispielcode ggf. Disassembly
Habe ich derzeit nicht, weil das nur Teil der Applikation ist und kein
eigentlicher Benchmark. Allerdings konnte ich bei einem längeren
Durchlauf eine Reduktion von 605s auf 600s messen, also der Effekt ist
merklich. Meßschwankungen bei den Einzelläufen sind übrigens nicht
vorhanden bzw. unter einer Sekunde, also der Unterschied ist kein
Rauschen.
Muß ich mal ein Minimalbeispiel zusammenstellen, vielleicht ein simpel
runtergehacktes "Game of Life" oder so, mal mit einem Array aus uint8_t,
mal mit uint32_t.
> - quantitative Ergebnisse, auch bei anderen Optimierungsstufen, Zeiten
Ließe sich machen. Wobei meine Erfahrung ist, daß man -O3 gar nicht erst
testen braucht, weil es sowieso langsamer als -O2 ist.
> und Speicherverbrauch
Ist für mich nicht relevant, mich interessiert nur die Geschwindigkeit.
> - Vergleich mit anderen Compilern insbesondere Keil und IAR
Fällt aus, weil ich die nicht habe - bei Keil ist die Gratisversion zu
begrenzt, und bei IAR weiß ich nichtmal, ob es überhaupt eine gibt. Nur
für einen Test setze ich aber keine Toolchain von Compilern auf, die ich
sonst ohnehin nicht einsetzen kann.
Andererseits, mit dem (noch zu erstellenden) Minimalbeispiel könnten das
ja auch andere machen, die die Toolchains ohnehin schon benutzen.
Dr. Sommer schrieb:> denn manche STM32 haben beim Flash ja einen Cache
Der Dache, ja, aber der wirkt nicht bei scattered access, und selbst
wenn, wäre das jedesmal ein Cache Miss, weil bis zum nächsten Lookup
noch einiges passiert.
@Nop
Danke für Dein Feedback. Es tut gut, wenn die Bemühung um objektive
Berichterstattung honoriert wird.
Ich hatte vor Jahren mal einen Artikel für eine Zeitschrift geschrieben.
Es ging um CoreMark 1.0 auf STM32F1 / STM32F4.
Wir sind zwar mittlerweile zig Compilerversionen weiter, aber vielleicht
vermittelt der Text immer noch ein bisserl Gefühl für die verschiedenen
Konfigurationen.
Ich verwende aktuell meist Atollic/GCC. Dadurch kann ich dem Kunden für
Wartungszwecke ein komplettes, kostenloses Entwicklungssystem in die
Hand geben.
Allerdings würde ich, wenn's bei großen Stückzahlen auf der MCU eng
wird, Testläufe mit IAR bzw. Keil machen. Und ja, die bieten
Teststellungen.
http://www.harerod.de/links_ger.html ->
CoreMark_STM32 - Testbericht über CoreMark 1.0 Läufe auf STM32 mit
verschiedenen Compilereinstellungen.
So, hier mal Ergebnisse. Interessant ist, daß signed und unsigned
unterschiedlich schnell gehen, sobald man mit O2 optimiert.
Da ich ursprünglich nur signed hatte, ist mir auch nur die
Beschleunigung beim Übergang auf 8 bit aufgefallen; bei unsigned wäre es
aber umgedreht gewesen.
Wer das noch mit anderen Compilern oder so probieren will: Quelltext
liegt bei, kann man in jedes Framework recht einfach reinsetzen. Eine
Demo-main() für PC liegt auch bei. Die erzeugten Assembler-Listings für
alle 4 Varianten (signed/unsigned, O2/O0) ebenfalls.
GCC: arm-none-eabi 5.4.1
Optionen: -Wall -std=c99 -mcpu=cortex-m4 -mtune=cortex-m4 -g -mthumb
STM32F405, dCache/iCache enabled, ART enabled, 168 MHz, 5 flash
waitstates
NUM_GENS 100000ul
-O02
unsigned:
8 bit: 28.04 s
32 bit: 27.43 s
signed:
8 bit: 26.82 s
32 bit: 28.04 s
-O00
unsigned:
8 bit: 97.50 s
32 bit: 100.55 s
signed:
8 bit: 97.50 s
32 bit: 100.55 s
Und für die, die nur eben den Code angucken wollen, ohne das
runterzuladen:
Nico W. schrieb:> Könntest du das ganze noch mit -Os machen?
Os und O03 funktionieren leider nicht, weil GCC dann anfängt
herumzuspinnen und irgendwelche Library-Funktionen einfügen möchte, die
ich gar nicht aufrufe. Das führt dann zu unresolved symbols und
Linkerfehlern.
Nico W. schrieb:> Gut, dann kau ich das einmal neu durch. Das liegt sicher nicht am GCC.
Vermutlich doch, weil ich ohne C-Standard-Bibliothek linke, GCC aber bei
Os und O3 selber einfach unaufgefordert Funktionen davon reinnimmt. Das
fällt natürlich auf die Nase. Linkst Du die Standardbibliothek mit rein?
Deine Ergebnisse sind relativ ähnlich, nur bei O0/unsigned hat 6.2.1
wohl aufgeholt, dafür aber bei O2/unsigned ganz stark nachgelassen. Das
sieht nach einer Regression aus, denn da ist 8bit ja sagenhafte 32%
schneller.
Da, wie oben im Thread dargelegt, die Adressierung eigentlich gratis
sein sollte, ist das schon bemerkenswert.
Nop schrieb:> Linkst Du die Standardbibliothek mit rein?
Wieder was gelernt. Ich hab hier zumindest kein -nostdinc im Makefile,
falls es das ist was du meinst.
Ansonsten darfst du nicht vergessen, das mein Prozessor nur mit 100MHz
läuft. Deiner aber mit 168MHz.
Als Basis hab ich jetzt einfach meinen Port der Teacup-Firmware
genommen. Falls du genau sehen willst, wie das Makefile aussieht:
https://github.com/Traumflug/Teacup_Firmware/blob/arm-stm32f411-port/Makefile-ARM
Nico W. schrieb:> Wieder was gelernt. Ich hab hier zumindest kein -nostdinc im Makefile,> falls es das ist was du meinst.
Ich habe "-nostartfiles -nodefaultlibs -ffreestanding" mit im
Linkeraufruf, das dürfte der verantwortliche Unterschied sein.
> Ansonsten darfst du nicht vergessen, das mein Prozessor nur mit 100MHz> läuft. Deiner aber mit 168MHz.
Das ist klar, aber mit den 32% meinte ich bei Dir den Unterschied
innerhalb Deiner Meßergebnisse: 8bit/21.7s, 32bit/28.7s. Das ist
wesentlich mehr Unterschied, als ich mit GCC 5.4.1 bei O2/unsigned
zwischen 8bit/32bit messe.
Nop schrieb:> Ich habe "-nostartfiles -nodefaultlibs -ffreestanding" mit im
Irks: IIRC deaktiviert -ffreestanding etliche interne Builtins und
Optimierungen, z.B. wird bei strlen("Hallo") das strlen() aufgerufen und
nicht durch eine Konstante ersetzt. Nimm das mal raus...
Jim M. schrieb:> Irks: IIRC deaktiviert -ffreestanding etliche interne Builtins und> Optimierungen,
Laut GCC-Doku ist ffreestanding dafür gedacht: "A freestanding
environment is one in which the standard library may not exist, and
program startup may not necessarily be at main." Genau deswegen habe ich
das drin. Das Problem ist auf Os/O3 eher, daß GCC -ffreestanding
gepflegt ignoriert und trotzdem versucht, mir Funktionen aus der
Runtime-Lib reinzudrücken.
> z.B. wird bei strlen("Hallo") das strlen() aufgerufen und> nicht durch eine Konstante ersetzt.
Bei den wenigen Gelegenheiten, wo ich die Länge eines Strings brauche,
ist der sowieso von unbekannter Länge.