Hex-Werte aus Liste auslesen und seriell steuern

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Benutzeravatar
gatonero
User
Beiträge: 21
Registriert: Montag 4. April 2022, 21:12

Die Einhaltung der Begrenzung übernimmt ja schon die cursorPos- Methode. Das muss ich nicht noch einmal berechnen. Nachdem die Cursorposition auf jeden Fall korrekt ist, wird anschließend mit write() an der ermittelten Position die Nachricht ausgegeben.
Benutzeravatar
gatonero
User
Beiträge: 21
Registriert: Montag 4. April 2022, 21:12

Ich glaube, ich bin auf einer heißen Spur und versuche es mit return() ;-)
LukeNukem
User
Beiträge: 232
Registriert: Mittwoch 19. Mai 2021, 03:40

gatonero hat geschrieben: Montag 11. April 2022, 17:45 Puuh, der Fachmann staunt und der Laie wundert sich!
Ehrlich gesagt glaube ich nicht, daß ein Fachmann staunen würde... ;-)
gatonero hat geschrieben: Montag 11. April 2022, 17:45 [*]Auf dem PC habe ich das fehlerfrei zum Laufen gekriegt. Auf dem ESP32 tut sich noch nichts. Das versuche ich dort erstmal zum Laufen zu kriegen und dann das Ganze zu verstehen.
Viel Erfolg und Vergnügen! ;-)

Ein "Overengineering" vermag ich hier allerdings beim besten Willen nicht zu erkennen. Das "lc"-Objekt in der Demo belegt laut sys.getsizeof() 48 Byte Speicher, die Klassendeklarationen LcdCommand und LcdCmds 1064 Byte. Richtig ist, daß Blackjack eine Methode gezeigt hatte, die die Werte für die Positionen berechnet hat, aber wer ins Datenblatt gescnaut hat weiß, daß es neben dem 20x4er-LDD auch noch andere gibt, mindestens eines davon mit nur 16x2 Zeichen -- und da sind dann eben "Lücken" in den Werten, die man irgendwie anders abfangen müßte. Keine Ahnung, wie ein 16x2-LCD reagieren würde, wenn es eine ungültige Position erhält, aber daß ich das so gestaltet habe, ist nur eine simple Abstraktion von verschiedener, aber sehr ähnlicher Hardware. Wer will, kann die Positionen ja in der goto()-Methode mittels Blackjacks Vorschlag berechnen, das tut sich IMHO wenig.

Wie dem auch sei: es ist trotzdem sinnvoll, hier eine Klasse als "Datenhalter" für die Verbindung zu nutzen, und je nachdem, wie Micropython genutzt wird -- meines Wissens kann das auch den Code ganz oder teilweise kompilieren -- könnte der Overhead noch bedeutend geringer sein als bei CPython. Ich wundere mich ohnehin sehr regelmäßig, daß in einem Forum, welches einer objektorientierten Programmiersprache gewidmet ist, vom Einsatz objektorientierter Features dieser Sprache abgeraten wird.Keine Ahnung, ob hier viele Freunde prozeduraler oder funktionaler Programmierung unterwegs sind, aber irgendwie erscheinen mir Hinweise wie "da braucht man keine Klasse, sondern nimmt eine Funktion" ein bisschen merkwürdig.

Darüber hinaus geht es bei solchen Einwürfen am Ende natürlich um eine Art Optimierung. Die wichtigsten Leitsätze der Optimierung sind "premature optimization is the root of all evil" (Donald E. Knuth), "measure, don't guess" (Kirk Pepperdine) und "make it work, make it right, make it fast" (Kent Beck), wobei es in diesem Falle wohl besser "make it fit" heißen sollte. Im Endeffekt laufen sie alle auf dasselbe hinaus: Oprimierungen "ins Blaue hinein" an unfertigem Code sind verschwendete Arbeits- und Lebenszeit, ganz unabhängig von der Sprache, denn erstens verhält sich der solcherart künstlich optimierte Code in seiner realen Umgebung wahrscheinlich vollkommen anders, zweitens erzielen diese verfrühten Optimierungen in der Regel nicht den gewünschten Effekt, und drittens gibt es für nicht genutzte Ressourcen leider kein Geld zurück.
Benutzeravatar
__blackjack__
User
Beiträge: 13985
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@LukeNukem: Python ist halt nicht Java, wo man alles in Klassen stecken *muss*, auch wenn die ”Methode” gar nicht auf einen Zustand des Objekts zugreift, auf dem sie aufgerufen wurde. Ausserdem ist in Python alles ein Objekt was man an einen Namen binden kann, also auch Funktionen, Module, Klassen, und Methoden. Man kann also eine Funktion auch einfach als aufrufbares Objekt sehen. Und ein Modul als Namensraum für aufrufbare Objekte.

Prozedural ist Python auch mit Klassen, weil der Code in Methoden ja letztlich prozedural beschrieben wird. Mit eingebauten Funktionen wie `map()` und `filter()` und den `itertools`- und `functools`-Modulen und „comprehension“-Syntax gibt es auch viel aus der funktionalen Programmierung was man in Python verwenden kann.

Objektorientierung allgemein braucht auch gar keine Klassen, denn es heisst ja *objekt*orientiert und nicht *klassen*orientiert. Es gibt objektorientierte Programmiersprachen ohne Klassen. Self, Io, und lange Zeit JavaScript. In Scheme- und Lisp-Dialekten gibt es oft auch keine OOP-Unterstützung direkt in der Sprache sondern als Bibliothek die dann ”Klassen” bietet, oder eben auch nicht.

Bei Deinem Code wurde auch ”premature” optimiert: auf Flexibilität.
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
Benutzeravatar
gatonero
User
Beiträge: 21
Registriert: Montag 4. April 2022, 21:12

Ich verabschiede mich dann mal aus dem Thread. Das artet in eine Grundsatzdiskussion aus und hat nichts mehr mit dem Thema zu tun. :roll:
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

gatonero hat geschrieben: Montag 11. April 2022, 21:34 Ich verabschiede mich dann mal aus dem Thread. Das artet in eine Grundsatzdiskussion aus und hat nichts mehr mit dem Thema zu tun. :roll:
Das ist nachvollziehbar. Tu dir nur einen Gefallen & Denk nicht, weil Luke’s Code besonders kompliziert wäre, ist er auch besonders gut. Dein Microcontroller wird es dir danken.
LukeNukem
User
Beiträge: 232
Registriert: Mittwoch 19. Mai 2021, 03:40

__blackjack__ hat geschrieben: Montag 11. April 2022, 20:46 @LukeNukem: Python ist halt nicht Java, wo man alles in Klassen stecken *muss*, auch wenn die ”Methode” gar nicht auf einen Zustand des Objekts zugreift, auf dem sie aufgerufen wurde. Ausserdem ist in Python alles ein Objekt was man an einen Namen binden kann, also auch Funktionen, Module, Klassen, und Methoden.
Das ist ja lustig. Denn ich habe so ganz und gar nichts mit Java zu tun, kann es -- zum Leidwesen meiner damaligen Vorgesetzten bei Sun Microsystems -- nicht leiden und halte es in vielerlei Hinsicht für eine Fehlkonstruktion. In meinem ganzen Leben habe ich keine 500 Zeilen Java-Code geschrieben, dafür aber etwa dreißig Jahre Erfahrung mit Perl, C, und C++. Die Wahrscheinlichkeit, daß mein Denken von Java beeinflußt ist, liegt nahe null. ;-)

Aber ansonsten ist das, was Du schreibst, ja vollkommen richtig: alles Benennbare in Python ist ein Objekt, was mein Befremden bezüglich der in diesem Forum zelebrierten Ablehnung gegenüber Klassen nur noch verstärkt. Ich sehe das auch nicht so eng, daß Methoden nur dazu da sein sollten, den Zustand eines Objekts ihrer Klasse zu verändern, stattdessen orientiere mich eher an Alan Kay. Demzufolge dienen Klassen auch der Organisation und der Wiederverwendbarkeit von Code, mithin zur Gruppierung zusammengehöriger Funktionalität -- insofern ist LcdCommand ein Beispiel für Kapselung und Abstraktion, zudem womöglich eine Basis für Vererbung.

Im Übrigen kann man natürlich auch mal die Frage stellen, ob die Methoden "goto()", "cmd()" und "write()" nicht tatsächlich zumindest indirekt den Zustand des repräsentierten Objekts ändern, nämlich des LC-Displays. Aus dieser Perspektive ist die Klasse LcdCommand eine softwareseitige Repräsentation einer LCD-Hardware, deren Zustand durch die Methoden geändert wird. Warum sollte man das mit Funktionen abbilden? Es in eine Klasse einzupacken ist wesentlich eleganter.
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

micropython ist ein wesentlich simplerer und entsprechend kaum optimierender Interpreter. Die Annahme, dem beliebige komplexe Konstrukte vorwerfen zu können, weil er das schon clever wegoptimiert, ist also grundfalsch. Hätte man messen können, frei nach Pepperdine.

Das du an anderer Stelle mit Microoptimierungen gegen kaskadiertem Zugriff für Gewinne im mikrosekunden-Bereich argumentiert hast, aber auf einem schlechteren Interpreter, mit Hardware um mehrere Größenordnungen langsamer, und mit weniger Speicher, gleich einen ganzen Zoo von unnötigen Indirektionen und Datenstrukturen in den Ring wirfst, räumt auf jeden Fall mal den Ironie des Monats Preis ab. Diese Grundlagen der Optimierung scheinen doch recht flexibel in der Anwendung….

Eine Bereichsabfrage für gültige Koordinaten erledigt man üblicherweise auch durch genau 4 Vergleichsoperationen. Und nicht das hinterlegen aller möglichen Koordinatenpaare in einem Wörterbuch. Das ist auch kein Fall von verfrühter Optimierung. Sondern einfach schlecht, in jedem Fall.
LukeNukem
User
Beiträge: 232
Registriert: Mittwoch 19. Mai 2021, 03:40

__deets__ hat geschrieben: Montag 11. April 2022, 21:36 Das ist nachvollziehbar. Tu dir nur einen Gefallen & Denk nicht, weil Luke’s Code besonders kompliziert wäre, ist er auch besonders gut. Dein Microcontroller wird es dir danken.
Ehrlich gesagt sehe ich nicht, wo mein Code überhaupt kompliziert wäre -- geschweige denn, auch noch "besonders" kompliziert. Einfacher geht es doch kaum?! Es sei denn, man hielte objektorientierte Programmierung ganz grundsätzlich für kompliziert. Aber ich fürchte, dann müßte man sich die Frage stellen, ob eine strikt objektorientierte Programmiersprache wie Python wirklich das Richtige für einen ist.

"Dein Mikrocontroller wird es Dir danken"... meine Güte, das ist schon ein bisschen traurig. Könnten wir uns bitte darauf einigen, mit belegbaren Fakten zu argumentieren? Danke.
__deets__ hat geschrieben: Dienstag 12. April 2022, 09:07 micropython ist ein wesentlich simplerer und entsprechend kaum optimierender Interpreter. Die Annahme, dem beliebige komplexe Konstrukte vorwerfen zu können, weil er das schon clever wegoptimiert, ist also grundfalsch. Hätte man messen können, frei nach Pepperdine.
Soweit ich das sehen kann, ist MicroPython nicht nur ein Interpreter, sondern kann auch nativen Code erzeugen, entweder "plain JIT" für einzelne Funktionen und Methoden, oder "optimized JIT" ("Viper"-Modus) mithilfe Typeannotations, oder sogar komplette Firmware-Binaries im Executable and Linking Format (ELF). Aber ich kenne Micropython nur dem Namen nach und von einigen Diskussionen im Mikrocontroller.net-Forum, wo ich einige interessante Diskussionen zu diesem Thema verfolgt habe. Vielleicht wissen andere Teilnehmer dieses Forums ja mehr darüber?

Ich hatte eine Lösung präsentiert, die Du, Blackjack, und ein anderer Herr zwar kritisieren konntet, aber außer nebulösem Gemurmel wie dem dankbaren Mikrocontroller konnte bislang keiner von Euch zu dieser "Kritik" etwas präsentieren, das einer seriösen Begründung oder einer sachlichen Argumentation auch nur entfernt geähnelt hätte. Mach Deine Hausaufgaben doch bitte selbst -- es ist nämlich nicht meine Aufgabe, Deine Aussagen zu begründen, sondern Deine. Also präsentiere doch einfach Deine eigene Lösung, miß sie aus und stell sie meiner Lösung gegenüber, frei nach Pepperdine: "measure, don't guess". Das wäre eine seriöse Argumentation unter Erwachsenen, nicht dieses nebulöse Gemurmel. Wir diskutieren hier doch unter Erwachsenen, oder?
__deets__ hat geschrieben: Dienstag 12. April 2022, 09:07 Das du an anderer Stelle mit Microoptimierungen gegen kaskadiertem Zugriff für Gewinne im mikrosekunden-Bereich argumentiert hast, aber auf einem schlechteren Interpreter, mit Hardware um mehrere Größenordnungen langsamer, und mit weniger Speicher, gleich einen ganzen Zoo von unnötigen Indirektionen und Datenstrukturen in den Ring wirfst, räumt auf jeden Fall mal den Ironie des Monats Preis ab. Diese Grundlagen der Optimierung scheinen doch recht flexibel in der Anwendung….
Bitte korrigiere mich, wenn ich mich falsch erinnere, aber In dem anderen Fall ging um recht große Datenmengen, wo sich auch Mikrosekunden zu signifikanten Größen summieren können. Hier hingegen geht nur um die Ansteuerung eines LC-Displays, bei der obendrein auch noch auf jeden Befehl ein Delay folgt, damit das Display den Befehl ausführen kann. Zu bemerken, daß die Laufzeitperformance hier keine große Rolle spielt, wird erfahrene Entwickler wie Dich und Blackjack kaum herausfordern. Und wer eine hohe Performance oder eine minimierte Speicherbelegung benötigt, wird eher nicht auf Micropython setzen...

Ansonsten, Pardon, scheinst Du dasselbe Vorurteil zu bedienen, das mir auch im Mikrocontroller.net-Forum immer wieder begegnet: daß Objektorientierung übermäßig kompliziert sei und einen großen, unverhältnismäßigen und kaum vertretbaren Overhead produziere. Das ist zwar ein beliebtes Vorurteil, entspricht nach meinen Erfahrungen (und Messungen) aber nicht der Realität.

Du kannst Deine Behauptungen aber ganz leicht belegen, indem Du eine Lösung präsentierst, die eine meinem Code äquivalente Funktionalität hat (ja, bitte auch die Erweiterungsmöglichkeit auf ähnliche LCDs) , ebenso leicht und einfach verständlich ist, und wesentliche Vorteile im Hinblick auf Performance und Speicherbedarf aufweist. Viel Spaß und Erfolg!
__deets__ hat geschrieben: Dienstag 12. April 2022, 09:07 Eine Bereichsabfrage für gültige Koordinaten erledigt man üblicherweise auch durch genau 4 Vergleichsoperationen. Und nicht das hinterlegen aller möglichen Koordinatenpaare in einem Wörterbuch. Das ist auch kein Fall von verfrühter Optimierung. Sondern einfach schlecht, in jedem Fall.
Jaaaa, siehst Du... in meinem Code wäre die Bereichsabfrage ganz leicht und nur ein Lookup auf die Keys des Dictionary. Und bei mir würde das nach einer minimalen Anpassung sofort mit der 16x2-Variante des LCD funktionieren. Im Mikrocontroller-Bereich ist das übrigens eine oft genutzt Optimierungstechnik zur Einsparung von Taktzyklen, vorberechnete Funktionswerte im Speicher abzulegen, auch wenn es in diesem Fall hier natürlich primär der Anpaßbarkeit an ähnliche LCDs desselben Typs gedacht ist. Deswegen erhält der Konstruktor von LcdCommand auch das Objekt, auf das mit der write()-Methode geschrieben werden soll, denn Parallax biete solche LCDs nicht nur mit Serieller Schnittstelle, sondern auch mit I2C, und andere bieten ähnliche LCDs mit SPI an.
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Es ist wirklich faszinierend, zu welchen mentalen Verrenkungen du faehig bist, um deinen verbockten Ansatz zu rechtfertigen. Zu behaupten, der lookup eines Wertes in einem generischen dictionary mit beliebigen Schluessel- und Wertetypen waere effizienter, als eine simple Vergleichspruefung "if 0 <= x < width and 0 <= y < height" ist schon absurd. Das dann zu rechtfertigen mit der Nutzung von LUTs, die komplett anders arbeiten, zeigt einfach nur, dass jeder Strohalm rechtens ist. Ein solcher arithmetischer Ausdruck wird uebrigens von der viper Optimierung ggf. erfasst, weil die sich lediglich auf Grunddatentypen und Arrays erstreckt. Nicht auf beliebige, dynamisch erzeugte Datentypen und -strukturen. Oder, wie andere es nenne: ein Fakt. Statt die von dir hier prasentierte Spekulation, was denn wie sein koennte.

Mein Ansatz benoetigt zur Anpassung an andere Dimensionen die Anpassung von *zwei* Werten. Deiner die Erzeugung und Lagerung von hunderten von sinnlosen Eintraegen... deine Definition von minimal ist spannend, vorsichtig ausgedrueckt.

Strohmannargumente wie "wer's effizient braucht, nutzt nicht Python", sind ja nun auch zur Genuege bekannt. Es gibt nicht nur eine Dimension bei der Enscheidung, welches Sprache oder Tool zum Einsatz kommt. Aus der relativen Langsamkeit den Umkehrschluss zu rechtfertigen, wenn's nicht effzient sein muss, dann kann man halt rumsauen, ist ein Logikfehler.

Anders als du habe ich praktische Erfahrung mit micropython. Schon oefter hat es sich da als notwendig herausgestellt, auf liebgewonnen Abstraktionen und sogar Mantras wie "keine globalen Variablen" zu verzichten, da die unter den gegebenen Umstaenden leider nicht haltbar waren. Was etwas anders Formuliert "der Mikrocontroller wird es dir danken" meint.

Was hier passiert ist, habe ich schon oft genug in der Praxis erleben duerfen. Das eigentliche Problem (was ja schon geloest war..) ist einfach nicht spannend genug. Der Entwickler baut sich also ein Problem, das ihn reizt. Heute: Extraktion von Konstanten aus einem PDF. Eine Loesung, die vermeintlich "generisch" ist, aber nur bis zur naechsten Revision, in der jemand die aufgeblasene Darstellung trivial zu berechnender Werte durch einen Absatz mit der notwendigen Formel ersetzt.

Und hey, hat dir Spass gemacht, schoen fuer dich. Aber das dann auf Teufel komm raus als ueberlegen zu praesentieren...
LukeNukem
User
Beiträge: 232
Registriert: Mittwoch 19. Mai 2021, 03:40

__deets__ hat geschrieben: Dienstag 12. April 2022, 14:15 Es ist wirklich faszinierend, zu welchen mentalen Verrenkungen du faehig bist, um deinen verbockten Ansatz zu rechtfertigen.
Zeig doch einfach Deinen Code. Ach, Du hast gar keinen? Schade... aber warum bin ich nicht überrascht?

Das bockige Gezicke wird mir jetzt echt zu blöd, ich bin 'raus. Meld' Dich wenn Du Code hast. Bis dahin danke und viel Spaß beim letzten Wort.
Benutzeravatar
__blackjack__
User
Beiträge: 13985
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Der Punkt war doch, dass das Problem bereits gelöst war und man deswegen nicht noch mal Code schreiben muss. Da jetzt nach Code zu fragen ist ein bisschen unsinnig.
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

LukeNukem hat geschrieben: Dienstag 12. April 2022, 16:17 Zeig doch einfach Deinen Code. Ach, Du hast gar keinen? Schade... aber warum bin ich nicht überrascht?

Das bockige Gezicke wird mir jetzt echt zu blöd, ich bin 'raus. Meld' Dich wenn Du Code hast. Bis dahin danke und viel Spaß beim letzten Wort.
Ich dachte, ich unterhalte mich hier mit einem erfahrenen Entwickler, dem der Hinweis, das Koordinaten Bereichspruefung und das berechnen des notwendigen Steuercodes aus der Eingaben bereits einleuchten. Das du da nicht abstraktionsfaehig genug bist, ist natuerlich bedauerlich.

Darum mal der Verweis auf meine Antwort auf die erste Frage des Users hier zu dem Problem:

viewtopic.php?f=31&t=54443#p404173

Fuer dich nochmal zum extra leichten nachvollziehen mit Link auf die Zeile, auf die ich mich hier beziehe: https://github.com/iamthechad/parallax_ ... D.cpp#L183

Muss ich das noch in Python, Pseudocode, UML und ein Flussdiagramm umformulieren, damit es klar ist, warum diese eine Zeile Code sowohl schneller als auch speichersparender ist, auch in micropython? Ach, ich mach's einfach mal, man weiss ja nie:

Code: Alles auswählen

WRITE_BASE = 128
COL_SIZE =  20

class ParallaxLCD:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def pos(self, column, row):
        if 0 <= column < self.width and 0 <= row < self.height:
            return bytes([(WRITE_BASE + (row * COL_SIZE) + column)])
        assert False, "Invalid position"
Und du bleibst dabei, dies zwei Zeilen sind genauso gut wie deine ~2.8KB Datenstruktur (64 Bit Linux, einen Faktor von 1.5 angenomen fuer 32 Bit dann noch ~2KB)? :roll:
LukeNukem
User
Beiträge: 232
Registriert: Mittwoch 19. Mai 2021, 03:40

__blackjack__ hat geschrieben: Dienstag 12. April 2022, 16:36 Der Punkt war doch, dass das Problem bereits gelöst war und man deswegen nicht noch mal Code schreiben muss. Da jetzt nach Code zu fragen ist ein bisschen unsinnig.
Hier [1] wurde -- nach meinem Verständnis -- erklärt, ohne Konstanten brauche man keine Klasse. Dennoch haben alle zuvor präsentierten Beispiele jeweils eine Klasse benutzt. Warum der Autor die anderen Beispiele nicht kritisiert hat, weiß ich nicht -- vielleicht kommt es aber auch gar nicht darauf an, ob OOP verwendet wird, sondern viel mehr darauf, wer sie benutzt. ;-)

Natürlich kann man die Position für dieses spezifische LC-Display leicht berechnen, wobei mir Deine Lösung mit min() und max() wesentlich besser gefällt als deets' Idee -- zumal seine Idee auch bei ungültigen Positionen sogar einen AssertionError wirft und mir bislang nicht ganz eingängig ist, wie man auf einem Mikrocontroller adäquat darauf reagieren sollte. Mein Problem mit solchen "Rechenlösungen" ist aber, daß diese inhärent vom jeweiligen LC-Display abhängig sind und daher ausschließlich mit Displays funktionieren können, bei denen die Positionen tatsächlich auch aus (Zeile, Spalte) berechenbar sind. Die anderen Lösungen legen obendrein auch noch die Berechnungsformel fest, so daß diese allesamt nur diesen einen einzigen Displaytyp ansteuern können.

Mein Designziel war es jedoch, eine allgemeinere Lösung zu zeigen -- deswegen gibt es bei mir die Tabelle mit den Positionen, die bei allen Displays funktioniert, bei denen die Positionen direkt angesprungen werden können. Daß das ein wenig mehr Speicher verbraucht, ist natürlich korrekt, aber das ist eben der Preis für die größere Unabhängigkeit vom konkreten LCD-Typ. Wiederverwendbarkeit versus Ressourcenbedarf ist ja nun ein Thema, welches in der Softwareentwicklung nicht wirklich neu ist, und nicht immer, aber meistens, gewinnen Wiederverwendbarkeit und Portabilität. Wenn es immer nur um maximale Performance und minimalem Bedarf an Ressourcen ginge, dann würden wir vermutlich alle noch in Assembler entwickeln.

Weil ich's aber wissen wollte, habe ich Eure Berechnungslösungen mal implementiert, Deine in demo2.py und deets' in demo3.py, die drei Programme dann jeweils 100 Mal mit Zeitmessung sowie mit und ohne Micropython aufgerufen, als bekennender Faulpelz (siehe oben) allerdings über subprocess.run() und unter time(1) -- nicht dem Bash-Builtin, sondern dem echten Programm unter /usr/bin/time und mit dem Verbose-Modus. Dann habe ich die Ergebnisse der Läufe gesammelt und die minimalen, maximalen und durchschnittlichen Werte, die Summen und die Standardabweichung daraus berechnet. Die Resultate (und alle Skripte finden sich unter [2], wobei deren Übertragbarkeit auf ein Micropython in einer gänzlich anderen Hardwareumgebung natürlich mehr als fragwürdig ist. Die Messungen wurden auf einem Dell 5580 mit Intel i7-7820HQ, 2 x 16GB DDR4-2400 RAM (Hynix HMA82GS6AFR8N-UH) und einer Samsung 970 Pro 1 TB NvME, unter Kubuntu 20.04 LTS mit Python 3.8.10 bzw. MicroPython v1.12-1 durchgeführt.

Ich persönlich finde die Ergebnisse durchaus spannend -- siehe dazu beispielsweise die im Archiv vorhandene Datei "measurements_python.txt". Bei diesem Lauf war meine Tabellenlösung (demo.py) im Schnitt sogar die speichereffizienteste und immerhin sogar minimal schneller als deets' Berechnungslösung, während Deine Berechnungslösung zwar hinsichtlich der Laufzeit gewinnt, aber den größten Speicherbedarf hat. Der Fairneß halber muß allerdings unbedingt dazu gesagt werden, daß Reihenfolgen sich bei jedem Aufruf ändern und die Unterschiede somit offenbar kleiner sind als die Meßungenauigkeit.

Bei den Messungen mit x86_64-Micropython ("measurements_micropython.txt") sieht die Sache geringfügig anders aus -- da liegt Deine Lösung beim Speicherbedarf zwar zuverlässig, aber recht geringfügig vorne, bei der Laufzeit jedoch minimal hinten, während deets' Lösung speicherseitig etwa auf dem Niveau meiner Tabellenlösung ist, aber ein wenig mehr Laufzeit benötigt. Auch in diesem Fall würde ich allerdings sagen, daß das in dieser Umgebung zwar reproduzierbare Tendenzen sind, die Unterschiede aber wiederum eher im Bereich der Meßungenauigkeiten liegen...

Klar: die Messungen beinhalten auch jeweils die Startup-Zeiten der Interpreter und daher von begrenzter Aussagekraft. Aber im Kern können wir, glaube ich, wohl festhalten: meine universellere Lösung mit der Tabelle ist so lange gut, bis ihr Benutzer in Speicherprobleme läuft. Wenn das geschieht, wäre die Berechnung der Befehle aus den Koordinaten ein sinnvoller Ansatz für eine, wenngleich nicht besonders große Optimierung -- sofern das mit der betreffenden Hardware möglich ist, versteht sich. Daß der Mikrocontroller sich deswegen vor dem Entwickler auf die Knie wirft und ihm vor Dankbarkeit die Füße küßt, sehe ich jetzt allerdings auch nicht.

Abschließend möchte ich anmerken, daß es mich schon überrascht hat, wie viel weniger Speicher micropython verwendet hat (etwa Faktor 3 gegenüber CPython) und wie viel schneller Micropython war (etwa Faktor 2). Damit liegt Micropython hinsichtlich der Laufzeit etwa auf dem Niveau von Pypy, das mit ~ 60 MB allerdings etwa um den Faktor 20 mehr Arbeitsspeicher belegt...

Wer lauffähigen Code zum Messen hat, möge ihn gerne hier einstellen, ich unterziehe ihn dann denselben Tests wie Deinen, deets' und meinen Code, und nehme die Ergebnisse in meine Listen auf.


[1] viewtopic.php?f=1&t=54459#p404449
[2] https://drive.google.com/file/d/1DeQyyX ... sp=sharing
Sirius3
User
Beiträge: 18249
Registriert: Sonntag 21. Oktober 2012, 17:20

Ich habe explizit davon geschrieben, dass man eine kleine spezielle Klasse im Micro-Controller-Umfeld haben möchte.
Wie Du daraus liest, dass Klassen generell verboten seien, kann ich nicht nachvollziehen.
Ein Modul, das genau eine Klasse enthält, die nur aus Konstanten besteht, ist mindestens eine Hierarchieebene zu viel.
Es geht also nicht um das "wer", sondern um das wie. Auch wenn Du immer wieder darauf bestehst, Java nicht zu mögen, Du magst es Javaartigen Pythoncode zu schreiben.

Und ja, genau deshalb schreibt man ja eine Klasse, weil man damit die Positionsbestimmung abstrahieren kann, und je nach Display-Typ eine andere Methode dafür schreiben kann.
Es ist nicht nötig, jeden Fall vorzuberechnen und in ein Wörterbuch zu packen, das ist eher nicht OOP (von dem Du ja so begeister bist).
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

@LukeNukem Erstmal eine persoenliche Bemerkung: Respekt dafuer, hier wieder konstruktiv nachzulegen. Ich lasse mich manchmal ein bisschen zu sehr von der Leine, und das ist nicht wirklich konstruktiv. Es gibt eine Menge Dinge, bei denen ich inhaltlich nicht deiner Meinung bin. Das sollte aber nicht in triefendem Sarkasmus enden, und ich versuche mich da zu bessern. Jetzt zum Thema:

Auch ich habe einen Benchmark geschrieben, und den auf der dazu gedachten Plattform - ESP32 hier und heute - laufen lassen.

Und die Unterschiede sind signifikant. Der dict-basierte Ansatz braucht doppelt so viel Zeit wie die Berechnung. Wenn man lediglich die Berechnung vs. den Lookup vergleicht, also die umliegende Bereichspruefung weglaesst, und keine Methodenaufrufe hat, ist der Vorsprung etwa Faktor 2.5. Achtung: ich war schon beim schreiben nicht zufrieden mit meiner oben gewaehlten Berechnung des Bytestrings via Listenbildung & bytes(). Frueher(tm) haette ich chr() benutzt, aber das ist in Python 3 nicht mehr sinnvoll. Ich habe einen anderen Weg gefunden, siehe Code. Aber selbst mit dem obenstehenden Code, der ja deinen Berechnungen zugrunde liegt, komme ich auf 25% Vorteil. Beide Arten sind unten dargestellt, fuer eigene Versuche.

Diese Ergebnisse sind auch nach meiner Erfahrung kein Wunder. Grund fuer diese grossen Unterschiede werden die Menge der benoetigten Speicherzugriffe sein. Die sind einfach hoeher bei deinem Ansatz. Und das ist ein Problem. Auch wenn wir heutztutage mehrere 100MHz Takte im uC-Bereich haben, die Speicherschnittstellen hinken schon lange(!) der Entwicklung der CPU-Geschwindigkeit hinterher. Ich erinnere mich nicht mehr genau, aber der 6502 hat zwei Takte pro Zugriff gebraucht, heute sind es 8 oder 16 oder sowas. Dem wirken die grossen Architekturen mit aufwaendigen Cache-Layern entgegen, die sowohl teuer in der Entwicklung, Die-Flaeche, als auch Strom sind. Und die ein Micro einfach nicht hat. Deine Bemerkung zu LUTs ist darum bei diesen Architekturen - und da reden wir durchaus auch ueber "grosse" ARMs wie A9/A15 - nicht richtig. Oft ist es besser, ein paar Instruktionen mit Berechnungen zu verbringen, als eine LUT zu benutzen.

Und dann kommen wir mal zum Speicherverbrauch. Mein ESP32 ist mit seinen 512KB in dem Bereich eher gut bestueckt. Micropython hat davon etwa 110 KB frei. Und dein dict kostet davon ein bisschen mehr als zwei, also etwas mehr als 2 Prozent. Ist fuer eine so untergordnete Funktionalitaet schon happig finde ich. Ich habe schon oefter Techniken zur Speicherverbrauchsvermeidung nutzen muessen bei micropython Projekten. Da macht das dann einen Unterschied.

Das Argument der groesseren Flexibilitaet erschliesst sich mir nicht. Mein Code zeigt sprichwoertlich, dass lediglich die Dimensionen des LCDs angegeben werden muessen, und es geht fuer beide uns bekannten Formen. Dein Code muss dafuer deutlich mehr angefasst werden, und deutlich weniger offensichtlich. Und wenn das ganze als Bibliothek angeboten wird, wie geht man dann damit um? Zwei dicts, eines pro Modell? Oder duennt man aus, womit letztlich eine Formel wie meine zum Einsatz kommt, nur hinter den Kulissen? Ich sehe da nichts, dass einfacher/bequemer waere.

Das assert ist ein Hinweis gegen Fehlbedienung. In einer polierten Bibliothek wuerde das anders aussehen, aber du schmeisst einen KeyError - wo ist da der (substantielle) Unterschied?

Darum bleibe ich bei meiner Schlussfolgerung: der lookup-Ansatz ist weder ueberlegen, noch flexibler. Sein einziger Vorteil basierend auf deinem konkreten Vorgehen besteht in der Extraktion aus einer anderen Quelle. Unter sehr bestimmten Randbedingungen, zb einem irgendwie seltsam verteilten Abbildung, oder etablierten Prozessen in der Dokumentation und Produktion, mag das mal nett sein. Hier IMHO nicht.

Ergebnisse des Benchmarks:

Code: Alles auswählen

>>> import coord_lookup_benchmark
>>> coord_lookup_benchmark.luke_outer()
Function luke_benchmark Time = 10150.776ms
>>> coord_lookup_benchmark.deets_outer()
Function deets_benchmark Time = 5209.934ms
>>> coord_lookup_benchmark.luke_raw()
Function luke_raw Time = 4649.070ms
>>> coord_lookup_benchmark.deets_raw()
Function deets_raw Time = 1906.599ms
Mit der schlechten Bildung des Bytes:

Code: Alles auswählen

>>> import coord_lookup_benchmark
>>> coord_lookup_benchmark.luke_outer()
Function luke_benchmark Time = 10150.404ms
>>> coord_lookup_benchmark.deets_outer()
Function deets_benchmark Time = 7538.597ms
Code:

Code: Alles auswählen

import time

WRITE_BASE = 128
COL_SIZE =  20


LUKES_LOOKUP = {}


class ParallaxLCD:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def pos(self, column, row):
        if 0 <= column < self.width and 0 <= row < self.height:
            #return bytes([(WRITE_BASE + (row * COL_SIZE) + column)])
            return (WRITE_BASE + (row * COL_SIZE) + column).to_bytes(1, 'little')
        assert False, "Invalid position"


class LukesLCD:

    def pos(self, column, row):
        return LUKES_LOOKUP.get((row, column))


def timed_function(f):
    myname = str(f).split(' ')[1]
    def new_func(*args, **kwargs):
        t = time.ticks_us()
        result = f(*args, **kwargs)
        delta = time.ticks_diff(time.ticks_us(), t)
        print('Function {} Time = {:6.3f}ms'.format(myname, delta/1000))
        return result
    return new_func


@timed_function
def deets_benchmark(lcd):
    for i in range(100_000):
        lcd.pos(i % 20, i % 2)

        
def deets_outer():
    lcd = ParallaxLCD(20, 2)
    deets_benchmark(lcd)


@timed_function
def luke_benchmark(lcd):
    for i in range(100_000):
        lcd.pos(i % 20, i % 2)


def luke_outer():
    # I generate the dictionary. Makes no difference.
    lcd = ParallaxLCD(20, 2)
    for col in range(lcd.width):
        for row in range(lcd.height):
            # swapped
            LUKES_LOOKUP[(col, row)] = lcd.pos(col, row)

    lukes_lcd = LukesLCD()
    luke_benchmark(lukes_lcd)


@timed_function
def deets_raw():
    for i in range(100_000):
        (WRITE_BASE + ((i % 2) * COL_SIZE) + (i % 20)).to_bytes(1, 'little')


@timed_function
def luke_raw():
    for i in range(100_000):
        LUKES_LOOKUP[((i % 20, i % 2))]
LukeNukem
User
Beiträge: 232
Registriert: Mittwoch 19. Mai 2021, 03:40

__deets__ hat geschrieben: Donnerstag 14. April 2022, 08:11 @LukeNukem Erstmal eine persoenliche Bemerkung: Respekt dafuer, hier wieder konstruktiv nachzulegen. Ich lasse mich manchmal ein bisschen zu sehr von der Leine, und das ist nicht wirklich konstruktiv. Es gibt eine Menge Dinge, bei denen ich inhaltlich nicht deiner Meinung bin. Das sollte aber nicht in triefendem Sarkasmus enden, und ich versuche mich da zu bessern.
Lieben Dank für die Blumen, aber mir geht's ja genauso, insofern: alles gut. Vielleicht könnten wir uns ja zusammen bessern? ;-)

Nebenbei bemerkt, ist das bei mir vielleicht auch so eine... Persönlichkeitssache (böse Zungen behaupten, es sei eher eine Persönlichkeitsstörung). Wenn alle einer Meinung zu sein scheinen, frage ich mich meistens, ob sie wirklich Recht haben... Nenn' es eine grundsätzliche Skepsis gegenüber Schwarmintelligenz, vorgefertigten Meinungen oder, wie Terry Pratchett schrieb: "the IQ of a mob is the IQ of its most stupid member divided by the number of mobsters". Leider reißt das andere Menschen nicht immer zu großer Begeisterung hin, trotzdem war ich damit zu oft erfolgreich, um es bleibenzulassen. ;-)
__deets__ hat geschrieben: Donnerstag 14. April 2022, 08:11 Jetzt zum Thema:

Auch ich habe einen Benchmark geschrieben, und den auf der dazu gedachten Plattform - ESP32 hier und heute - laufen lassen.

Und die Unterschiede sind signifikant.
Vielen lieben Dank für Deine Zeit und Arbeit, Du siehst mich ebenso erstaunt wie überrascht. Deine Ergebnisse beweisen neben der Richtigkeit Deiner Ausführungen auch einmal mehr, wie wichtig es bei Benchmarks aller Art ist, die Hard- und Softwarearchitektur zu erwähnen... denn auf meiner x86-64-Hardware sehen die Ergebnisse Deines Code ganz anders aus, siehe unten?! 8-O
__deets__ hat geschrieben: Donnerstag 14. April 2022, 08:11 Und dann kommen wir mal zum Speicherverbrauch. Mein ESP32 ist mit seinen 512KB in dem Bereich eher gut bestueckt. Micropython hat davon etwa 110 KB frei. Und dein dict kostet davon ein bisschen mehr als zwei, also etwas mehr als 2 Prozent. Ist fuer eine so untergordnete Funktionalitaet schon happig finde ich.
Ja, das ist enorm, keine Frage... und so große Unterschiede hätte ich keinesfalls erwartet.
__deets__ hat geschrieben: Donnerstag 14. April 2022, 08:11 Das Argument der groesseren Flexibilitaet erschliesst sich mir nicht.
Irgendwo in meiner Bastelkiste liegen einige LCDs, bei denen die Positionswerte nicht nebeneinander liegen und sich daher nicht oder jedenfalls nicht mit einer einfachen Formel ohne weitere Fallunterscheidungen berechnen lassen. Wenn ich in einigen Wochen wieder daheim bin, versuche ich mal herauszufinden, was genau das für Typen sind, jedenfalls hatte ich die im Hinterkopf.

Wie dem auch sei, ist das ja letztlich doch nur ein Implementierungsdetail. Auch wenn man die Steuerwerte für die Positionen berechnet, hielte ich es für sinnvoll, das konfigurierbar zu machen -- ob mit einer Funktion in Python, die ja ohnehin ein erstklassiges Objekt ist, oder mit einem Funktionszeiger, einer std::function oder einem Lambdaausdruck in C++... irgendwas in der Art halt.

Wie dem auch sei -- ich habe zwar eine Handvoll ESP2866 und ESP32 in meiner Bastelkiste, aber bisher leider noch keine Zeit, keine Idee und keine Gelegenheit gehabt, damit zu spielen. Insofern bieten Deine Benchmarks schon einmal sehr wertvolle und nützliche Hinweise auf die Unterschiede dieser Plattformen zu x86_64. Apropos x86_64: ich habe mir mal erlaubt, Deine Benchmarks ein bisschen umzubauen, damit sie auf x86-Python und x86-Mikropython laufen, das Ergebnis meiner Umbauten und die Resultate meiner Benchmarks dazu findest Du weiter unten. Wärest Du vielleicht bitte so freundlich, einmal auf meine Änderungen zu schauen, um nochmals sicherzustellen, daß ich nicht versehentlich etwas verändert habe, das Deine Benchmarks verfälscht? Nach meinen Änderungen an Deinem Code sieht er so aus:

Code: Alles auswählen

#!/usr/bin/env python
import sys
print('running with: {:s}\n'.format(sys.implementation.name))
import time

WRITE_BASE = 128
COL_SIZE =  20


timer_func = None
compare_func = None
if sys.implementation.name == 'micropython':
    timer_func = time.ticks_us
    compare_func = lambda a, b: time.ticks_diff(a, b) / 1000
else:
    timer_func = time.perf_counter
    compare_func = lambda a, b: (a - b) * 1000

LUKES_LOOKUP = {}


class ParallaxLCD:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def pos(self, column, row):
        if 0 <= column < self.width and 0 <= row < self.height:
            #return bytes([(WRITE_BASE + (row * COL_SIZE) + column)])
            return (WRITE_BASE + (row * COL_SIZE) + column).to_bytes(1, 'little')
        assert False, "Invalid position"


class LukesLCD:
    
    def pos(self, column, row):
        return LUKES_LOOKUP.get((row, column))


def timed_function(f):
    myname = str(f).split(' ')[1]
    def new_func(*args, **kwargs):
        t = timer_func()
        result = f(*args, **kwargs)
        e = timer_func()
        delta = compare_func(e, t)
        print('Function {} Time:\t{:8.3f}'.format(myname, delta))
        return result
    return new_func


@timed_function
def deets_benchmark(lcd):
    for i in range(100_000):
        lcd.pos(i % 20, i % 2)

        
def deets_outer():
    lcd = ParallaxLCD(20, 2)
    deets_benchmark(lcd)


@timed_function
def luke_benchmark(lcd):
    for i in range(100_000):
        lcd.pos(i % 20, i % 2)


def luke_outer():
    # I generate the dictionary. Makes no difference.
    ## by luke: doubles overall runtime in mpy :-(
    lcd = ParallaxLCD(20, 2)
    '''
    for col in range(lcd.width):
        for row in range(lcd.height):
            # swapped
            LUKES_LOOKUP[(col, row)] = lcd.pos(col, row)
    '''
    from lib.lcd_parallax import LCD_POSITIONS as LUKES_LOOKUP
    lukes_lcd = LukesLCD()
    luke_benchmark(lukes_lcd)


@timed_function
def deets_raw():
    for i in range(100_000):
        (WRITE_BASE + ((i % 2) * COL_SIZE) + (i % 20)).to_bytes(1, 'little')


@timed_function
def luke_raw():
    for i in range(100_000):
        #LUKES_LOOKUP[((i % 20, i % 2))]
        LUKES_LOOKUP[((i % 2, i % 20))]


if __name__ == '__main__':
    luke_outer()
    deets_outer()
Die Resultate auf x86_64 (Angaben zur Hardware siehe weiter oben):

Code: Alles auswählen

~$ ./callit.py -c 100 ./deets.py ; echo; ./callit.py -c 100 -m ./deets.py 
name                                               |       min       |       mean      |       max       |       sum       |      stddev    
--------------------------------------------------------------------------------------------------------------------------------------------
count of runs                                      | 100
runtime_ms                                         |         62.1511 |         65.0002 |         69.0681 |       6500.0172 |          1.5234
running with                                       | cpython
Function luke_benchmark Time                       |         14.4230 |         15.7642 |         17.7250 |       1576.4150 |          0.8381
Function deets_benchmark Time                      |         29.0740 |         30.7242 |         33.0530 |       3072.4190 |          1.0322
Command being timed                                | "./deets.py"
User time (seconds)                                |          0.0400 |          0.0556 |          0.0600 |          5.5600 |          0.0052
System time (seconds)                              |          0.0000 |          0.0002 |          0.0100 |          0.0200 |          0.0014
Percent of CPU this job got                        |         98.0000 |         99.2000 |        100.0000 |       9920.0000 |          0.9847
Maximum resident set size (kbytes)                 |       9540.0000 |       9622.8000 |       9712.0000 |     962280.0000 |         51.2676
Minor (reclaiming a frame) page faults             |       1118.0000 |       1122.4200 |       1128.0000 |     112242.0000 |          2.2660
Voluntary context switches                         |          1.0000 |          1.0000 |          1.0000 |        100.0000 |          0.0000
Involuntary context switches                       |          0.0000 |          1.3900 |         10.0000 |        139.0000 |          2.0884
Page size (bytes)                                  |       4096.0000 |       4096.0000 |       4096.0000 |     409600.0000 |          0.0000

name                                               |       min       |       mean      |       max       |       sum       |      stddev    
--------------------------------------------------------------------------------------------------------------------------------------------
count of runs                                      | 100
runtime_ms                                         |         66.7286 |         69.6528 |         73.0929 |       6965.2773 |          1.3489
running with                                       | micropython
Function luke_benchmark Time                       |         22.7160 |         24.2632 |         26.8990 |       2426.3220 |          1.0676
Function deets_benchmark Time                      |         39.1270 |         41.0664 |         43.4950 |       4106.6360 |          1.1113
Command being timed                                | "/usr/bin/micropython ./deets.py"
User time (seconds)                                |          0.0500 |          0.0600 |          0.0700 |          6.0000 |          0.0014
Percent of CPU this job got                        |         97.0000 |         99.3100 |        100.0000 |       9931.0000 |          0.9713
Maximum resident set size (kbytes)                 |       4824.0000 |       4909.8400 |       4956.0000 |     490984.0000 |         31.1318
Minor (reclaiming a frame) page faults             |        647.0000 |        649.9700 |        652.0000 |      64997.0000 |          1.0867
Voluntary context switches                         |          1.0000 |          1.0000 |          1.0000 |        100.0000 |          0.0000
Involuntary context switches                       |          0.0000 |          1.2100 |          9.0000 |        121.0000 |          1.9863
Page size (bytes)                                  |       4096.0000 |       4096.0000 |       4096.0000 |     409600.0000 |          0.0000
Ach so: da mein "callit.py" ja das Programm unter "/usr/bin/time -v" ausführt und damit alles inklusive der Startup-Zeiten des Interpreters etc. mißt, habe ich Deinen dynamischen Aufbau von LUKES_LOOKUP einmal heraus genommen und durch einen Import meines Dictionary ersetzt, denn der dynamische Aufbau verdoppelt die Laufzeit des Programms unter Micropython. Ich vermute, daß Micropython zur Senkung des Speicherverbrauchs auf die Präallokation verzichtet, die CPython macht. Bitte beachte die Zeiten "Funktion <xxx>_benchmark Time", die Dein Code ausgibt (ich habe nur das " =" durch ":" ersetzt, da mein callit.py-Skript diesen Trenner erwartet.

Trotzdem finde ich es sehr spannend, daß mein Code auch bei Deinem Benchmark auf der x86_64-Plattform deutlich schneller ist als Deine Berechnung, um Faktoren von etwa 1,7 bis 2. Ich finde das insgesamt schon sehr überraschend, und dann auch noch insbesondere mit diesem großen Abstand... damit hätte ich, ganz ehrlich, nicht gerechnet. Ist das wirklich richtig so, oder habe ich bei der Anpassung Deines Codes was verfälscht? 8-O
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich habe einen im Grunde gleichen Umbau getaetigt, genau so via ImportError. Sieht also gut aus.

Ich habe das nicht so intensiv getestet, aber die Laufzeit war fuer den lookup besser, stark beeinflusst auch davon, ob bei mir eine Bereichspruefung erfolgt, oder nicht. In dem Fall auch in etwa Faktor zwei. Scheint also zu passen.

Und fairerweise muss man die Bereichspruefung machen, denn sonst rechnet das Ding still und leise Unfug aus.

Ohne das tiefer betrachtet zu haben, habe ich nur eine Vermutung: ohne JIT-Optimierung bedeutet mein Ansatz, dass letztlich mehr Zeilen code (Pruefung, dann Rechnung) und mehrere Lookups (self.*, WRITE_BASE etc) durchgefuehrt. Wenn ich die durch hartkodierung entferne, komme ich auf einen Faktor von 1.7. Auch nicht geil. Nehme ich dann noch den to_bytes-Aufruf weg, und ersetze ihn durch ein chr (eigentlich nicht gueltig, aber man koennte natuerlich ein "byte" in den builtins haben, wenn man wollte) , kommen wir auf 1.35.

Erst wenn ich die Bereichspruefung wegmache, und nur den reinen Ausdruck + chr berechne, wird es schneller.

Code: Alles auswählen

    def pos(self, column, row):
        #if 0 <= column < self.width and 0 <= row < self.height:
        #if 0 <= column < 20 and 0 <= row < 2:
            #return bytes([(WRITE_BASE + (row * COL_SIZE) + column)])
            #return (WRITE_BASE + (row * COL_SIZE) + column).to_bytes(1, 'little')
        return chr(128 + (row * 20) + column)#.to_bytes(1, 'little')
Ergebnis:

Code: Alles auswählen

Function luke_benchmark Time = 1749.358ms
Function deets_benchmark Time = 1618.544ms
Was damit in meinen Augen uebrigbleibt: das nachschlagen eines Wertes in einem Woerterbuch ist im Grunde *DIE* zentrale Semantik in Python. Der gesamte Interpreter ist ja eigentlich ein riesen Baum von Woerterbuechern. Klassen, Instanzen, Module, etc. Und dieser lookup ist darum das wahrscheinlich optimiersteste Stueck Code, das man findet. Und du hast davon zwei, einmal das dict selbst zu bestimmen, und dann den eigentlichen Nachschlag, sowie ein Tupel um den Key zu bauen. Bei mir sind das zwei Instanzvariablen, zwei Konstanten, eine Methode. Und dann unter der Haube ggf. noch eine Reihe mehr, um die eigentlichen Methoden der Operatoren zu finden. Das scheint sich zu laeppern.
LukeNukem
User
Beiträge: 232
Registriert: Mittwoch 19. Mai 2021, 03:40

__deets__ hat geschrieben: Samstag 16. April 2022, 14:05 Und fairerweise muss man die Bereichspruefung machen, denn sonst rechnet das Ding still und leise Unfug aus.
Eine valide Frage wäre ja auch, was geschieht, wenn das LCD ungültige Positionswerte erhält. Mach meinen (überschaubaren) Erfahrungen mit solchen Displays (mit C++ auf Atmel / Microchip AVR) gibt es da eine Reihe von Möglichkeiten: einige ignorieren das, andere führen einen Reset durch, wieder andere nehmen danach keine weiteren Befehle mehr an bis die Versorgungsspannung unterbrochen wurde, und wenn ich mich richtig erinnere, hatte ich sogar mal eines, das dann einfach wieder auf (0, 0) sprang. Dabei ist auch mein Lookup natürlich nicht ideal, denn er erkennt nicht, wenn man über das Ende einer Zeile hinaus schreibt... Es gibt also noch Möglichkeiten für weitere Verbesserungen, aber auch die würden natürlich wieder negative Auswirkungen auf Speicherbedarf und Laufzeit haben. Zumal sich ja angelegentlich dann auch immer die Frage stellen würde, wie unsere Software dann auf erkannte Bereichsüberschreitungen reagiert...

Andererseits... so ein LCD ist ja ein Anzeigegerät, das mit den Augen abgelesen wird, und wir wissen, daß Augen als solche jetzt nicht unbedingt zu unseren "schnellsten" Sinnesorganen zählen. Zudem erfolgt nach jedem Befehl, der an das LCD gesendet wird, ohnehin noch ein Delay, das ist also inhärent ohnehin langsam. Insofern halte ich -- insbesondere nach Deinen Hinweisen, wie viel Speicher Micropython auf dem bereits recht üppig ausgestatteten ESP32 benötigt -- eine Laufzeitoptimierung an dieser Stelle für weniger relevant als eine der Speicherbelegung. Warum sollte ich aus etwas, das ohnehin zwangsläufig langsam sein muß (Delay) und obendrein keine besonders hohe Geschwindigkeit braucht (langsame Augen), noch das letzte Quäntchen Geschwindigkeit herausquetschen? Insofern sind Deine und Blackjacks Berechnungslösungen für solche Zielplattformen zweifellos die besseren Varianten, jedenfalls, wenn sie denn möglich sind.
__deets__ hat geschrieben: Samstag 16. April 2022, 14:05 Was damit in meinen Augen uebrigbleibt: das nachschlagen eines Wertes in einem Woerterbuch ist im Grunde *DIE* zentrale Semantik in Python. Der gesamte Interpreter ist ja eigentlich ein riesen Baum von Woerterbuechern. Klassen, Instanzen, Module, etc. Und dieser lookup ist darum das wahrscheinlich optimiersteste Stueck Code, das man findet. Und du hast davon zwei, einmal das dict selbst zu bestimmen, und dann den eigentlichen Nachschlag, sowie ein Tupel um den Key zu bauen. Bei mir sind das zwei Instanzvariablen, zwei Konstanten, eine Methode. Und dann unter der Haube ggf. noch eine Reihe mehr, um die eigentlichen Methoden der Operatoren zu finden. Das scheint sich zu laeppern.
Ja, scheint so... und womöglich spielt es auch eine Rolle, wie die dicts erzeugt werden. Wenn ich Deinen Code mit der dynamischen Erzeugung des Lookup-Dictionary unter Micropython benutze, verdoppelt sich die Laufzeit im Vergleich zum Importieren meines vordefinierten Lookup-Dictionary. Das deutet für mich darauf hin, daß Micropython hier für das Einfügen eines neuen Items im Dictionary einen Roundtrip übers Betriebbssystem (Linux: malloc(3) als LibC-Frontend für brk(2) und sbrk(2)) macht. CPython (und zum Beispiel auch Perl) hingegen machen ja eine abschnittsweise Vorbelegung von Speicher, und interagieren dadurch viel weniger oft mit dem OS. Das sieht auf ESP32 und anderen Plattformen, auf denen Micropython direkt auf Bare Metal läuft, aber sicher noch einmal ganz anders aus...

Lieben Dank für die spannende Diskussion, aus der ich einige wertvolle Dinge mitnehmen konnte für eine Zukunft, in der ich mal Zeit, Lust und Gelegenheit habe, mich mit meinen ESP32 zu beschäftigen... ;-)
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Na die ganzen Performancegeschichten sind natuerlich nicht so sehr aus der Perspektive der Laufzeit fuer den User. Bei vielleicht ein paar dutzen Frames pro Sekunde ist das kaum relevant. Allgemein das Verhalten des Interpreters besser zu verstehen ist schon eher interessant, und eben die Softwaredesignfrage, ob man eine solche Funktion als diskrete Abbildung, oder via Formel durchfuehrt. Ich behaupte mal, wenn das Datenblatt nicht die Werte diskret abgebildet haette, waere das auch nicht dein Vorgehen gewesen. Bestenfalls wuerde man dann, wenn es denn wirklich ein Bottleneck darstellt, sowas wie memoization benutzen. Wenn der Speichertradeoff es erlaubt.

Und ich denke nicht, dass hier das malloc das Problem darstellt, sondern eben die schon besprochene schlechtere Caching-Architektur. Grund fuer diese Annahme ist der Raspberry Pi. Wir haben den intensiv gebenchmarkt fuer unser Produkt (Pi 3), und das sowohl in C++ (bzw geekbench), als auch mit Python (weil wir darin viel Code hatten).

Die von dir beschriebenen Strategien setzt der Pi ja auch alle um, weil eben Linux, und MMU, und so. Und natuerlich auch das "echte" CPython.

Aber er hat eine relative Laufzeitverschlechterung von Python relativ zu "nativem" Code von Faktor 2 gezeigt. Das lag unserer Meinung nach eben an der so viel schlechteren Speicheranbindung. Wir beobachten das natuerlich auch in C++, ultimativ ist Code Code. Aber Python "saut" eben schon ziemlich rum mit Speicher (in allen Faellen), und das bestraft selbst ein Cortex A53 mit cache-misses.

Wir haben das zB dadurch bemerkt, dass der urspruengliche Plan, von 4 Kernen 3 zur Anwendung (intensive DSP Berechnung, bufferweise) und einen fuer's System zu resevieren, unhaltbar war. Der eine Kern hat die Caches der drei anderen (L2 gibt's ja nur fuer alle 512Kb) so zertrampelt, dass sich das *deutlich* in der Performance niedergeschlagen hat. Stattdessen rechnen wir jetzt mit 4 Kernen im sync, weil das eine hoehere Code/Cache-Lokalitaet zur Folge hat, und muessen dafuer aufpassen, dass wir nicht jenseits von 60-70% CPU Last kommen, damit der Rest des Systems noch "atmen" kann.
Antworten