Hallo, ich habe folgendes Problem:
Wenn ich zu einem uint8_t einen festen Wert addiere übersetzt mein
Compiler(arm-none-eabi-gcc) das zu einer Integerrechnung. Für mich ist
es aber wichtig, dass es ein uint8_t bleibt. Sonst drohen Zugriffe auf
undefinierte Speicherabschnitte.
Beispiel:
int32_t Array[256];
uint8_t ZaehlVariable;
int32_t Variable;
Variable = Array[ZaehlVariable+20];
In diesem Beispiel wird die 20 als Integer erkannt und die Rechnung als
Integerrechnung durchgeführt und damit wird auch das Ergebnis ein
Integer.
Für float-Wert kenne ich die Möglichkeit 3.14f zu schreiben (für
unsigned int 20u, für unsigned long 20ul) damit der Compiler das
richtige Format an nimmt und zur Richtigen Instruktion übersetzt. Gibt
es das auch für char (z.B. 20uc)?
Einen Cast Variable = Array[(uint8_t)(ZaehlVariable+20)]; möchte ich
nicht machen, denn das Übersetzt der Compiler zu einer Integerrechnung
und einem Cast, was ein zusätzlicher Rechenschritt ist.
Hierfür gibt es doch sicher eine einfache, elegante Lösung.
Vielen Dank schon mal im voraus.
Sebastian schrieb:> Einen Cast Variable = Array[(uint8_t)(ZaehlVariable+20)]; möchte ich> nicht machen, denn das Übersetzt der Compiler zu einer Integerrechnung> und einem Cast, was ein zusätzlicher Rechenschritt ist.
1
Variable=Array[(ZaehlVariable+20)&0xFF];
Ob das nun besser als ein Cast ist, musst du selbst entscheiden.
Sebastian schrieb:> Einen Cast Variable = Array[(uint8_t)(ZaehlVariable+20)]; möchte ich> nicht machen, denn das Übersetzt der Compiler zu einer Integerrechnung> und einem Cast, was ein zusätzlicher Rechenschritt ist.
Ein cast ist in dem Fall kein zusätzlicher Rechenschritt. Allerdings ist
nunmal in C ein Arrayindex per Definition ein Integer, daran kannst du
auch mit casten nichts ändern.
Es geht aber so:
Sebastian schrieb:> Einen Cast Variable = Array[(uint8_t)(ZaehlVariable+20)]; möchte ich> nicht machen, denn das Übersetzt der Compiler zu einer Integerrechnung> und einem Cast, was ein zusätzlicher Rechenschritt ist.
Hast du das im Assemblerlisting nachgesehen? Oder rätst du nur?
Es ist sowieso klar, dass ein 32 Bit Prozessor jede Rechnung mit 32
Bit ausführt. Du kannst nur bestimmen, welchen Teil des Ergebnisses du
hinterher verwenden willst...
Sebastian schrieb:> Einen Cast Variable = Array[(uint8_t)(ZaehlVariable+20)]; möchte ich> nicht machen, denn das Übersetzt der Compiler zu einer Integerrechnung> und einem Cast, was ein zusätzlicher Rechenschritt ist
das optimiert er doch weg.
Lothar M. schrieb:> Es ist sowieso klar, dass ein 32 Bit Prozessor jede Rechnung mit 32> Bit ausführt.
Eben. Bei Rechnungen in kleineren Einheiten kostet das beim STM32 eher
mehr Zeit. Entsprechende Tests habe ich schon gemacht. Seitdem habe ich
bei Source-Portierungen von AVR nach STM32 sehr viel Gefallen an den
uint_fastXt Typen gefunden. So bleibt der Source portabel und der STM32
kann trotzdem mit Vollgas arbeiten - solange man Überläufe nicht
implizit ausnutzt, wie der TO das will. Ich halte das für unsauber.
Besser wäre dann eine explizite Maskierung mit 0xFF.
Vielen Dank für die schnellen Antworten.
die Möglichkeit das Ergebnis mit einer &-Operation zu begrenzen
Variable = Array[(ZaehlVariable+20)&0xFF];
ist im Endeffekt das gleiche wie der Cast
Variable = Array[(uint8_t) (ZaehlVariable+20)];.
Allerdings ist in beiden Fällen ein weiterer Rechenschritt notwendig.
Ich 320 dieser Rechnungen in einem engen Rahmen zu machen und der
zusätzliche Rechenschritt erhöht meine Durchlaufzeit um mehr als 10%.
Ich hatte gehofft ich kann das umgehen in dem ich dem Compiler die Art
seiner Rechnung von vornerein vorgeben kann.
Sebastian schrieb:> Allerdings ist in beiden Fällen ein weiterer Rechenschritt notwendig.
nein nicht wirklich. Schau dir den ASM-Code an, dann sieht du was
wirklich passiert.
Wenn man dir Sagt das du die letzte stelle von 1234 nennen sollte,
rechnest du auch nicht.
Bei Array[256] ist aber max(uint8_t)+20 auch außerhalb. Das funktioniert
also auch nicht "implizit", sonder ist schlicht buggy Code. Und zwar auf
jeder Plattform.
Lothar M. schrieb:> Hast du das im Assemblerlisting nachgesehen? Oder rätst du nur?
Ich habe die Durchlaufzeiten gemessen und es ist eindeutig, dass die
Zeiten mit dem Cast länger werden.
Sebastian schrieb:> Ich habe die Durchlaufzeiten gemessen und es ist eindeutig, dass die> Zeiten mit dem Cast länger werden.
Gegenüber welchem Code?
Der ohne Cast ist falsch, den kanst du also nicht als Referenz nehmen.
Du kannst nur den Code mit Cast mit dem mit & vergleichen.
Oder, wenn es so zeitkritisch ist, Assembler nehmen.
Hast du die Optimierung an? Welche?
Carl D. schrieb:> Array[(ZaehlVariable+20)%256];Das das per "&0xFF" geht, bekommt der> Compiler selber hin. Es bleibt> aber richtig, wenn die Array-Größe mal nicht 256 sein sollte.
Wenn schon, denn schon:
Sebastian schrieb:> uint8_t ZaehlVariable;Sebastian schrieb:> Ich habe die Durchlaufzeiten gemessen und es ist eindeutig, dass die> Zeiten mit dem Cast länger werden.
Dann versuche, uint8_t generell und insbesondere bei Zählvariablen zu
vermeiden! Nimm uint_fast8t. Damit laufen zum Beispiel Schleifen über
die Zählvariablen um einiges schneller.
Allerdings musst Du dann alle bisher implizit einkalkulierten Überläufe
selber mit & 0xFF in Ordnung bringen, da auf dem STM32 ein uint_fast8t
denselben Wertebereich wie ein uint32_t hat.
@Dirk B:
Du hast recht ich habe es mit dem falschen Code, ohne Cast verglichen.
Hier rechnet er über das Array hinaus, aber schnell.
Das mit dem uint_fast8_t wird ich mir ansehen, das habe ich noch nicht
benutzt. Wenn das nicht klappt werde ich wohl den Cast oder das &0xFF
nehmen und wenn es zum Schluss zeitlich nicht reicht werde ich es mal
mit Assembler versuchen.
Sebastian schrieb:> Das mit dem uint_fast8_t wird ich mir ansehen, das habe ich noch nicht> benutzt.
uint_fast8_t benutzt immer den Typ, mit dem der konkrete µC am
schnellsten rechnen kann. Auf dem STM32 ist es identisch mit uint32_t,
auf einem ATmega oder ATtiny ist es identisch mit uint8_t.
Das heisst konkret für den STM32: Er rechnet in 32-Bit sauschnell.
Allerdings ist ein Wert größer als 255 durchaus möglich. Dem musst du
selbst vorbeugen.
Du fragst Dich sicher: "Warum soll ich denn dann nicht direkt uint_32t
nehmen".
Für mich gibt es da 2 Gründe:
1. Mnemotechnisch: Du sagst dem Leser des Codes: Das ist eine
Variable, deren Wertebereich bis 255 geht. Allerdings muss ich
mich selber drum kümmern, dass dieser Wertebereich auch
eingehalten wird.
2. Portabilität: Von 8-Bit bis 32-Bit µC sauschnell
Peter II schrieb:>> Allerdings ist in beiden Fällen ein weiterer Rechenschritt notwendig.>> nein nicht wirklich. Schau dir den ASM-Code an, dann sieht du was> wirklich passiert.
Hier ist ein ARM am Werk, kein AVR oder x86. Und ein ARM kann nicht mit
8 Bits rechnen. Der rechnet immer mit 32 Bits und muss ggf.
anschliessend maskieren. Was immer der TE also macht, die effiziente
reine 8-Bit Rechnung, die er sich vorstellt, wird er nicht kriegen.
Nicht in C, aufgrund der Sprachdefinition, aber auch nicht in Assembler.
Es ist daher schon so wie Frank schreibt. Wenn man versucht, jedes
einzelne Zwischenergebnis auf 8 Bits runterzubrechen, egal ob per
Variablendeklaration oder Cast, schmeisst man dem Compiler bloss Knüppel
zwischen die Beine. Denn das kostet.
Frank M. schrieb:> 2. Portabilität: Von 8-Bit bis 32-Bit µC sauschnell
Richtig. Für Grössenangaben, also z.B. für einen Index eines Arrays,
würde ich jedoch size_t empfehlen. Da der Operator sizeof auf jedes
Objekt angewandt werden kann und sein Ergebnis als size_t liefert, kann
kein Objekt im Speicher, auch kein Array, mehr als SIZE_MAX Bytes
belegen.
Portabel bedeutet auch, daß man hinschreibt, was man meint. Ein Array
als "Ringpuffer" zu betreiben, indem man auf das Überlaufverhalten einer
Variable hofft, taugt nichts. Wenn man Dinge wie "2^n" als Arraygröße
ausnutzen will, dann schreibt man das in den Kommentar, den Code aber
so, daß er mit jeder Größe funktioniert. Und wenn ein Compiler damit
nicht umgehen kann, sprich 2^n nicht erkennt, dann sollte man sich einen
besseren suchen.
be s. schrieb:> Für Grössenangaben, also z.B. für einen Index eines Arrays,> würde ich jedoch size_t empfehlen.
Das halte ich für nicht optimal. size_t ist auf einem AVR 16 Bit groß.
Das heisst, ich sollte nach Deiner Vorstellung eine Indexvariable vom
Typ uint_16t nehmen. Das wäre auf einem AVR aber längst nicht die
optimale Wahl, wenn mein Array eine Größe von weniger als 256 Elementen
hat.
Frank M. schrieb:> Das halte ich für nicht optimal. size_t ist auf einem AVR 16 Bit groß.> Das heisst, ich sollte nach Deiner Vorstellung eine Indexvariable vom> Typ uint_16t nehmen. Das wäre auf einem AVR aber längst nicht die> optimale Wahl, wenn mein Array eine Größe von weniger als 256 Elementen> hat.
Da hast du natürlich recht. Andererseits programmiere ich lieber
portabel, als dass ich voreilige Optimierungen vornehme, die
wahrscheinlich gar nicht nötig sind. Bisher hatte ich noch nie das
Problem, dass aufgrund eines 16Bit statt 8Bit Indizes das Programm zu
langsam war oder nicht mehr in den Programmspeicher passte. Sollte es
aber jemals nachweislich daran scheitern, würde ich diese
Handoptimierung nicht scheuen (zumal sie wahrscheinlich nur an ein, zwei
Stellen nötig wäre).
be s. schrieb:> Bisher hatte ich noch nie das> Problem, dass aufgrund eines 16Bit statt 8Bit Indizes das Programm zu> langsam war oder nicht mehr in den Programmspeicher passte.
Möglicherweise bist du von AVRs verwöhnt. Schon mal mit grösseren
Datentypen auf einer 8-Bit Akkumulator-Architektur in C gearbeitet?
Selbst wenn das in Platz und Zeit reicht kriegt man mitunter
Hirnkrämpfe, wenn man den erzeugten Code anschaut. Besonders bei solchen
Zierden wie den 12/14-Bit PICs.
be s. schrieb:> Andererseits programmiere ich lieber portabel
Das mache ich auch, versuche aber trotzdem optimalen Code zu erreichen.
Und das geht! Eine Verwendung einer 16-Bit-Variablen als Index ist für
mich in 99% aller Fälle ein No-Go. Die käme für mich tatsächlich nur in
Frage, wenn das Array mehr als 255 Elemente hat. Aber wann ist das schon
so?
Ich kann beim besten Willen keine Portabilität beim Verwenden eines
size_t für Indexvariablen von Arrays erkennen. Magst Du mir diese
erläutern?
(Das Argument, dass size_t wegen SIZE_MAX Bytes immer jedes theoretisch
noch so große Array abdeckt, ist für mich keines. Denn es gibt in den
meisten Fällen gar keine "theoretisch noch so große Arrays". In den
allermeisten Fällen sind sie wesentlich kleiner.)
Vielleicht verwechselst Du Portabilität mit Codesicherheit?
Frank M. schrieb:> Das halte ich für nicht optimal. size_t ist auf einem AVR 16 Bit groß.> Das heisst, ich sollte nach Deiner Vorstellung eine Indexvariable vom> Typ uint_16t nehmen. Das wäre auf einem AVR aber längst nicht die> optimale Wahl, wenn mein Array eine Größe von weniger als 256 Elementen> hat.
Bin mir nicht sicher, ob auch deine 8bit nicht als 16bit übergeben
werden. Intern ist es eine Addition mit Adressen. Die wird immer mit int
gerechnet.
Peter II schrieb:> Bin mir nicht sicher, ob auch deine 8bit nicht als 16bit übergeben> werden. Intern ist es eine Addition mit Adressen. Die wird immer mit int> gerechnet.
Du hast das im falschen Kontext gelesen. Wir sind da schon weiter, bitte
genauer lesen.
Meine Formulierung war nicht auf das Problem des TOs bezogen, sondern
auf Stuckis Aussage, man solle generell size_t als Typ für
Indexvariablen nehmen.
Das halte ich - speziell auf AVRs - für suboptimal.
Frank M. schrieb:> Ich kann beim besten Willen keine Portabilität beim Verwenden eines> size_t für Indexvariablen von Arrays erkennen.
Der "Vorteil" ist eher Gewöhnung aus der PC-Programmierung. Die
STL-Container geben halt alle ihre Größenangaben in size_t zurück, und
spätestens bei der Compilierung für 64-Bit haut einem der Compiler
begründete Warnungen um die Ohren, wenn man die mit Integern vermischt.
Dann landet man zwangsweis dabei, für solche Größen den dafür gedachten
size_t und seine Verwandten zu verwenden.
Oliver
Peter II schrieb:> Bin mir nicht sicher, ob auch deine 8bit nicht als 16bit übergeben> werden. Intern ist es eine Addition mit Adressen. Die wird immer mit int> gerechnet.
Nicht die Adressrechnung selbst ist davon wesentlich betroffen, denn die
läuft immer in size_t. Der Kram drumherum jedoch kann es sein, also die
Handhabung vom Index ausserhalb der eigentlichen Adressierung. Je nach
Umgebung ist mal dies mal jenes besser. Mit uint_fast8_t liegt man stets
auf der effizienten Seite, mit size_t nicht überall.
A. K. schrieb:> Möglicherweise bist du von AVRs verwöhnt. Schon mal mit grösseren> Datentypen auf einer 8-Bit Akkumulator-Architektur in C gearbeitet?> Selbst wenn das in Platz und Zeit reicht kriegt man mitunter> Hirnkrämpfe, wenn man den erzeugten Code anschaut. Besonders bei solchen> Zierden wie den 12/14-Bit PICs.
Ich hab mit den alten 16er PICs angefangen, nicht ohne Grund habe ich
die alten PICs (10/12/16/18) hinter mir gelassen. Da gibt es noch ganz
andere Fallstricke wie z.B. ein 8-stufiger Hardwarestack (alte 16er,
vergiss den Interrupt nicht...). Die neueren 18er sind eher wieder gut
zu gebrauchen, die 24er und 32er habe ich nie verwendet, diese fallen
aber so oder so in eine andere Kategorie.
Frank M. schrieb:> Eine Verwendung einer 16-Bit-Variablen als Index ist für> mich in 99% aller Fälle ein No-Go. Die käme für mich tatsächlich nur in> Frage, wenn das Array mehr als 255 Elemente hat. Aber wann ist das schon> so?
Wenn ich eine Bibliothek schreibe, die mit übergebenen Arrays hantiert,
kann ich zum Voraus unmöglich wissen, wie gross das Array ist. Die
gesamte Bibliothek umschreiben, nur weil ich diese später auf einem
grösseren uC oder auf dem PC mit Arraygrössen > 256 nutzen will? Nein
Danke.
Für ein lokales Array bin ich mit dir einverstanden, da kann man sowas
machen, ich tus nicht.
Oliver S. schrieb:> Der "Vorteil" ist eher Gewöhnung aus der PC-Programmierung.
Ja, den "Vorteil" sehe ich natürlich ein. Für einen PC oder Server
programmiere ich auch ganz anders. Da bringt es überhaupt nichts, mit
uint_fast8 zu kleckern. Ich käme da auch nie in den Sinn, so etwas zu
nutzen. Allerdings habe ich auch schon Programme geschrieben, die auf
einem AVR, Linux und auch Windows laufen. Dann wird gekleckert, sonst
geklotzt. Wobei man "Klotzen" jetzt nicht mit Speicherverschwendung
verwechseln sollte.
Es kommt immer drauf an, ob man auf bestimmte Zielplattformen Rücksicht
nehmen muss oder nicht. Handelt es sich beispielsweise um ein
STM32-Programm, welches niemals auf einen 8-Bit-µC portiert werden
wird: Scheiss auf uint_fast8t. Nimm direkt int oder unsigned int oder
auch speziell bei Indexvariablen size_t.
A. K. schrieb:> Mit uint_fast8_t liegt man stets> auf der effizienten Seite, mit size_t nicht überall.
Korrektur: Stimmt leider auch nicht. Da mindestens in Linux auf einer
32/64-Bit Kiste mit 8-Bit Operationen wie x86(-64) der Typ uint_fast8_t
mit 8-Bits definiert ist, ist man da mit size_t besser dran als damit.
Das Ei des Kolumbus gibts dafür also nicht.
Frank M. schrieb:> Handelt es sich beispielsweise um ein> STM32-Programm, welches niemals auf einen 8-Bit-µC portiert werden> wird: Scheiss auf uint_fast8t. Nimm direkt uint32_t.
Das sehe ich anders, ist sicher aber auch Geschmackssache. Warum sollte
ich für eine Membervariable einer Klasse etwas anderes als uint_least8_t
verwenden, wenn ich nur die Werte 0, 1, 2 und 3 speichern muss?
Ja ich weiss, es gibt Plattformen, bei denen solche Speicherzugriffe
extremst ineffizient sind. Andererseits könnte die Klasse auch X-Mal im
Speicher liegen, dann machts schon einen Unterschied, ob 1M * 1Byte oder
1M * 4Byte (je nach Plattform natürlich). Aber es ist so wie mit allem,
allen kann mans nicht rechtmachen.
be s. schrieb:> Wenn ich eine Bibliothek schreibe, die mit übergebenen Arrays hantiert,> kann ich zum Voraus unmöglich wissen, wie gross das Array ist.
Das ist korrekt. Außer es gibt schon technische Beschränkungen, die eine
obere Grenze vorgeben.
Aber wie A.K. schon zitierte: "640 kB ought to be enough for anybody."
Da hat er Recht - und damit Du auch.
be s. schrieb:> Das sehe ich anders, ist sicher aber auch Geschmackssache. Warum sollte> ich für eine Membervariable einer Klasse etwas anderes als uint_least8_t> verwenden, wenn ich nur die Werte 0, 1, 2 und 3 speichern muss?
Aufgrund des möglicherweise zusätzlichen Aufwands für den Umgang mit
8-Bit Daten in lokalen Variablen? Es hängt also von der Verwendung ab.
Im Speicher minimieren, im Register an den Maschinenworten orientieren.
A. K. schrieb:> Da mindestens in Linux> auf einer 32/64-Bit Kiste mit 8-Bit Operationen wie x86(-64) der Typ> uint_fast8_t mit 8-Bits definiert ist, ist man da mit size_t besser> dran als damit.
Habe es gerade mal geprüft. Tatsächlich ist dem so:
sizeof (uint_fast8_t)
ist unter Linux (32/64) tatsächlich 1.
Verstehen kann ich das nicht. Siehst Du dafür einen Grund?
> Das Ei des Kolumbus gibts dafür also nicht.
Korrekt.
Frank M. schrieb:> Siehst Du dafür einen Grund?
warum sollte auf einem x68 64bit schneller als 8bit sein?
mehrere 8bit Operationen können gleichzeitig gemacht werden.
Frank M. schrieb:> Verstehen kann ich das nicht. Siehst Du dafür einen Grund?
Bei uint_fast8_t stand offenbar nicht die Verwendung als Index im
Vordergrund, sondern der normale Umgang bei Rechnungen, Vergleichen etc.
Und da ist x86 bei Skalaren neutral.
Peter II schrieb:> warum sollte auf einem x68 64bit schneller als 8bit sein?
Bei der hier im Thread erfolgten Verwendung als Array-Index entsteht
zusätzlicher Aufwand durch die erforderliche Erweiterung.
A. K. schrieb:> Bei der hier im Thread erfolgten Verwendung als Array-Index entsteht> zusätzlicher Aufwand durch die erforderliche Erweiterung.
bei x86 kann man doch auch 64 und 8 bit addieren. Da muss doch vorher
nicht erweitert werden.
Peter II schrieb:> bei x86 kann man doch auch 64 und 8 bit addieren.
Wie?
Ausserdem wird diese Rechnung meist in der Adressierung selbst
durchgeführt, und die kennt keine 8-Bit Register.
Bei x86-64 sind das trotz 32-Bit int/unsigned 64-Bit Register. Hier
lässt GCC deshalb 2 Indizes parallel laufen, einer mit 32 fürs Zählen
und einer mit 64 Bits fürs Adressieren, weil i negativ sein kann:
A. K. schrieb:> Und da ist x86 bei Skalaren neutral.
Habe ich gerade mal durch einen kleinen selbstgehackten Benchmark auf
x86-64 (Linux) verifiziert. Du hast recht. Beim x86 ist das tatsächlich
so. Kenne ich von anderen 32- oder 64-Bit-Plattformen (diverse
RISC-CPUs) anders.
Ist das der x86-typischen Altlast geschuldet?
Frank M. schrieb:> Kenne ich von anderen 32- oder 64-Bit-Plattformen (diverse> RISC-CPUs) anders.
RISCs haben vom Prinzip her nur Wortoperationen in Registern. Bei 64 Bit
Architekturen aber u.U. auch noch 32 Bit Operationen, um beim (L)LP64
Modell (32 Bit int/unsigned) keinen zusätzlichen Aufwand zu bekommen.
Aber weder 8 noch 16 Bits. ARM hatte Anfangs noch nicht einmal Lade- und
Speicheroperationen für 16 Bit Typen.
> Ist das der x86-typischen Altlast geschuldet?
Ja. 8086 hatte gleichermassen 8- wie 16-Bit Operationen. Die haben sich
erhalten, so dass x86-64 gleichermassen mit 8, 16, 32 und 64 Bits
rechnen kann. Bei der Division kann sich ein kleinerer Type auch sehr
lohnen und in der Codierung gibts einen kleinen Vorteil bei 8 und 32
Bits.
Es kann allerdings subtile Laufzeitunterschiede durch die
Implementierung geben. Etwa wenn ein Prozessor bei Teilwortoperationen
(8 und 16 Bits) den Rest des Registers mit durch die ALU schiebt und
sich dabei ggf. falsche Abhängigkeiten einhandelt.
A. K. schrieb:> lässt GCC deshalb 2 Indizes parallel laufen, einer mit 32 fürs Zählen> und einer mit 64 Bits fürs Adressieren, weil i negativ sein kann:
Das war Unsinn. Komplizierter als mit 64-Bit Variablen ist der Code aber
trotzdem. Zumal er (4.7.2) bei unsigned als Indextyp die implizite
zero-extension von 32 Bit Operationen nicht optimal nutzt.