Performance Problem

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.
Antworten
Benutzeravatar
Dennis89
User
Beiträge: 1711
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo zusammen,

ich benötige mal wieder eure Hilfe. Mit Performance habe ich mich noch nie intensiv beschäftigt, da das für mich noch nie ein Problem war. Jetzt habe ich aber einen Code erhalten, der ist sehr langsam. Mit messen habe ich dann herausgefunden, dass ein Problem darin liegt, dass Ergebnisse mit `append` ein er Liste hinzugefügt wurden. Die Messung hat auch gezeigt, dass mit steigender Listenlänge, die Zeit hierfür auch anstieg. Hier wird ja nichts angehängt, sondern jedes mal eine neue Liste erzeugt, richtig? Da der Zugriff bzw. das raussuchen bestimmter Ergebnisse mittels Indexzugriff auch noch Zeit beanspruchte, habe ich mir überlegt, was ich tun kann um das zu ändern.
Mit `pd.DataFrame` habe ich schöne Optionen und habe damit angefangen. Mein Problem ist, dass das nicht schneller ist.
Ich habe ein Minimalbeispiel geschrieben, die gewünschte Logik müsste darin gut erkennbar sein. Die Berechnung über die Letzten 3 Reihen ist wichtig. Für eine Darstellung im Diagramm benötige ich aber alle Reihen, daher kann ich die vorherigen nicht einfach verwerfen. Ziel ist, dass ich `results` , `mass`, `volume` und `general_results` in einem Objekt habe und dann je nach dem die gewünschten Werte ausgeben kann oder Diagramme erstellen.

Wie macht man so etwas schneller, wenn man die Berechnung selbst nicht ändern kann? Wie und wo speichere ich die Daten sinnvoll und schnell zwischen? In eine Datei schreiben? Bin schon über `h5py` gestolpert. `pandas` hatte für mich den Vorteil mit `groupy` weil ich dann so schön die Berechnung jeder Stufe abfragen kann und keine Schleife benötige. `groupy` sei laut StackOverflow allerdings nicht so schnell. Für mich bin ich jetzt nach sehr vielen Stunden des Code-Uschreibens an einem Punkt, an dem ich sehr dankbar über einen Wegweiser wäre.

Code: Alles auswählen

#!/usr/bin/env python
import dataclasses

import pandas as pd



@dataclasses.dataclass
class State:
    volume: float = 0
    mass: float = 0
    stage: int = 0


def get_important_results(results):
    last_rows = results.tail(3)
    mass = last_rows.groupby("stage")["mass"].sum()
    volume = last_rows.groupby("stage")["volume"].sum()
    return mass, volume


def main():
    # In reality there are many more values, as shown here
    states = [State() for _ in range(3)]
    result = pd.DataFrame([dataclasses.asdict(states[0])])
    for value in range(10):
        for stage, state in enumerate(states, 1):
            # Placeholder for calculating
            state.stage = stage
            state.volume = 300 + value * stage
            state.mass = 200 + value * stage
            result = pd.concat(
                [result, pd.DataFrame([dataclasses.asdict(state)])], ignore_index=True
            )
    mass, volume = get_important_results(result)
    general_results = pd.DataFrame([{"something": 123}])
    print(mass)
    print(mass[2])


if __name__ == "__main__":
    main()
Danke vorab und viele Grüße
Dennis

Edit: Auch die `dataclass` kann ich rauswerfen/ersetzen, die ist drin, weil sie halt drin war und ich noch nicht weiß ob das Sinn macht oder nicht.
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 14358
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Dennis89: `list.append()` erzeugt nicht jedes mal eine neue Liste, da wäre die Laufzeit unterirdisch. Das ist armotisiert O(1), das heisst es ist in der Regel eine sehr schnelle Operation, aber ab und zu muss der interne Speicher der Liste wachsen und dann muss gegebenfalls etwas umkopiert werden.@Dennis89: `list.append()` erzeugt nicht jedes mal eine neue Liste, da wäre die Laufzeit unterirdisch. Das ist armotisiert O(1), das heisst es ist in der Regel eine sehr schnelle Operation, aber ab und zu muss der interne Speicher der Liste wachsen und dann muss gegebenfalls etwas umkopiert werden.

Das mit dem `pd.concat()` ist langsam, weil da tatsächlich jedes mal ein neuer Dataframe in jedem Schleifendurchlauf erstellt wird. Da würde man die Daten erst in einer Liste speichern und dann zum Schluss das `Dataframe`-Objekt erstellen.

Was mir daneben so auf Anhieb auffällt ist das ``last_rows.groupby("stage")`` zweimal direkt hintereinander mit dem gleichen Ergebnis gemacht wird.

Kurzzeitiges Zwischenspeichern kann man mit `to_pickle()` machen. Sonst würde ich Parquet oder Feather als Speicherformat verwenden.

Das mit dem `pd.concat()` ist langsam, weil da tatsächlich jedes mal ein neuer Dataframe in jedem Schleifendurchlauf erstellt wird. Da würde man die Daten erst in einer Liste speichern und dann zum Schluss das `Dataframe`-Objekt erstellen.

Was mir daneben so auf Anhieb auffällt ist das ``last_rows.groupby("stage")`` zweimal direkt hintereinander mit dem gleichen Ergebnis gemacht wird.

Kurzzeitiges Zwischenspeichern kann man mit `to_pickle()` machen. Sonst würde ich Parquet oder Feather als Speicherformat verwenden.
“It is easier to optimize correct code than to correct optimized code.” — Bill Harlan
Benutzeravatar
Dennis89
User
Beiträge: 1711
Registriert: Freitag 11. Dezember 2020, 15:13

Danke für die schnelle Antwort.
Du meinst ich hätte `last_rows.groupby("stage")[["mass", "volume"]].sum()` schreiben können um mir einen `groupby`-Aufruf zu sparen?

Ich teste mal die Variante, mit allem in eine Liste schreiben und dann ein `DataFrame` zu erzeugen und sollte dass nicht reichen, lese ich mich in die anderen 3 Optionen ein.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Dennis89
User
Beiträge: 1711
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo zusammen,

nun muss ich das angehen, vor dem ich mich gedrückt habe, weil ich keine sinnvolle Lösung gefunden habe und das die größte Bremse ist.
Ich habe ein Konstrukt, dass soooooo *ähnlich* aussieht:

Code: Alles auswählen

class Results:
    def __init__(self):
        self.xy = []

    def __repr__(self):
        return f"Tadaa: {self.xy!r}"

def calculate(result, a, b):
    result.xy.append(a + b)


def main():
    values = [(1, 2), (3, 4), (5, 6)]
    results = [Results() for _ in values]
    for _ in range(10):
        for result, (a, b) in zip(results, values):
            calculate(result, a, b)
    for result in results:
        print(result)

if __name__ == '__main__':
    main()
Die drei Berechnungen in der zweiten `for`- Schleife könnten "gleichzeitig" gemacht werden, daher habe ich sowas probiert:

Code: Alles auswählen

from threading import Thread

class Results:
    def __init__(self):
        self.xy = []

    def __repr__(self):
        return f"Tadaa: {self.xy!r}"

def calculate(result, a, b):
    result.xy.append(a + b)


def main():
    values = [(1, 2), (3, 4), (5, 6)]
    results = [Results() for _ in values]
    for _ in range(10):
        stages = [(result, a, b) for result, (a, b) in zip(results, values)]
        thread = Thread(target=calculate, args=stages[0])
        thread.start()
        thread.join()
        calculate(*stages[1])
        calculate(*stages[2])
    for result in results:
        print(result)

if __name__ == '__main__':
    main()
Das auch mit zwei Threads und nur eine Berechnung im Hauptthread, allerdings ist das nicht merkbar schneller.

Habt ihr eine Idee, wie ich die innere Schleife schneller abgearbeitet bekomme? Die äußere Schleife muss in meinem Fall ca. 6000 mal durchlaufen, weil es da mathematische Model dahinter so benötigt und darauf habe ich keinen Einfluss. Ich habe ja in einem anderen Thread schon mal nach der Einbindung von Rust in Python gefragt, weil ich wusste das so ein Problem auf mich zu kommt. Allerdings denke ich, dass mein Problem mit Python und richtigen Programmierkenntnissen noch deutlich optimierbar ist und genau dass ist ja das was ich lernen will.

Danke und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 14358
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Das mit dem Thread macht so keinen Sinn. Für das erste `stage`-Element wird ein Thread gestartet, der aber nicht mal theoretisch zu irgend etwas anderem parallel läuft, weil nach dem `start()` ja erst einmal mit `join()` gewartet wird bis der fertig ist. Das erzeugt an der Stelle einfach nur zusätzliche, überflüssige Mehrarbeit.

In CPython steht zudem noch das „global interpreter lock“ einer parallelen Verarbeitung von reinem Python-Code in Threads im Weg. Aber selbst wenn dem nicht so wäre: auch in C würde man keinen Thread für eine so popelig kleine ”Rechnung” starten, denn das Thread starten und danach wieder aufräumen braucht ja auch Zeit.

Multiprocessing wäre in CPython der Weg um parallel zu rechnen. Da ist das Hochziehen und Aufräumen aber _noch_ teurer als bei Theads. Dazu kommt dann auch noch die Kommunikation der Argumente und Ergebnisse zwischen den Prozessen.
“It is easier to optimize correct code than to correct optimized code.” — Bill Harlan
Sirius3
User
Beiträge: 18380
Registriert: Sonntag 21. Oktober 2012, 17:20

@Dennis89: Performance-Probleme zu diagnostizieren ist schwierig, wenn Du nicht den wirklichen Code zeigst.
Hier würde ich sagen, Du hast zwei Vektoren mit Zahlen, also nimmt man numpy und hat kein Geschwindigkeitsproblem.
Wahrscheinlich ist das Problem irgendwo anders.

Threads / Multiprocessing können helfen, aber das ist der letzte Schritt, wenn alles andere schon optimal läuft. Dann benutzt man aber sowas wie concurrent.futures und startet keine Prozesse von Hand.
Benutzeravatar
Dennis89
User
Beiträge: 1711
Registriert: Freitag 11. Dezember 2020, 15:13

Danke für eure Antworten.
Den originalen Code kann ich nicht öffentlich zeigen, der gehört nicht mir und enthält firmeninternes. Eine gekürzte Version könnte ich per PN zukommen lassen. Das ist dann aber keine Forums-Philisophie mehr.
Ich vermute dass der `numpy`- Ansatz funktioniert, allerdings sehe ich noch nicht wie, da der Code schon "wild" ist und jede Änderung Auswirkung auf "vorne" und "hinten" hat. Das ist super nervig, weil ich nach jeder Änderung den alten und den neuen Code laufen lassen muss um zu sehen, ob die Ergebnisse noch stimmen und die ständigen ~200 Sekunden warten, sind nicht so cool.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
sparrow
User
Beiträge: 4645
Registriert: Freitag 17. April 2009, 10:28

@Dennis89: Deshalb hat man kleine Funktionen und dafür Tests.
Benutzeravatar
noisefloor
User
Beiträge: 4310
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

so richtig nachvollziehen kann ich das Problem auch nicht, weil ein bisschen abstrakt beschrieben... gut, wenn es interne Daten sind, die nicht 1:1 gepostet werden können, ist es halt so.

_Was_ ist denn eigentlich der Ursprung des Problems? Das Daten für einen Zeitraum von X Sekunden so hochfrequent dazu kommen, dass das zu Problemen führt? Dann wäre eine schnelle Speicherstruktur im RAM der Weg. Also Python Listen oder sowas wie z.B. Valkey oder Redis, wenn man die Daten noch asynchron im Hintergrund persistent machen will / muss. Oder eventuell auch SQLite mit einer in-memory DB.
Oder ist die Berechnung an sich das Problem? Wenn es die Rechnung an sich ist, dann würde ja ganz platt eine CPU mit hoher Single Thread Performance (z.B. Apple Silicon M Serie) was bringen oder ggf. auch einfach PyPy statt CPython oder der Einsatz von z.B. Numba als JIT-Zusatz zu CPython.

Pandas macht IMHO wenig Sinn, weil a) der Overhead relativ hoch ist (der Import dauert schon spürbar, selbst auf schnellen Rechnern) und b) die Stärke von Pandas ja zeit-basierte Daten und Datensätze mit Lücken / NaN Werten ist.

Kannst du vielleicht mal Kontext liefern, was das für Daten sind und wo die her kommen?

Gruß, noisefloor
Benutzeravatar
grubenfox
User
Beiträge: 651
Registriert: Freitag 2. Dezember 2022, 15:49

sparrow hat geschrieben: Donnerstag 19. März 2026, 22:21 @Dennis89: Deshalb hat man kleine Funktionen und dafür Tests.
Genau... und ideal wäre es wenn die kleinen Funktionen unabhängig von dem mathematischen Modell sind und man sagen kann 'wenn diese Funktion mit einer Handvoll Testdaten einmal alles richtig macht, dann macht sie auch bei den restlichen 5999 Durchläufen alles richtig'. Das hilft direkt erst mal nichts beim Performance-Problem vom endgültigen produktiven Durchlauf, aber wenn die Tests vielleicht nur noch 10 Sekunden brauchen, dann macht das ganze doch gleich mehr Spaß.

Ein schönes Beispiel ist da:

Code: Alles auswählen

def calculate(result, a, b):
    result.xy.append(a + b)
Da reicht fast ein Durchlauf mit der Liste

Code: Alles auswählen

[(1, 2), (3, 4), (5, 6)]
um zu testen ob die Funktion nach Codeänderungen noch das macht, was sie soll. Weil bei Listen im Zweifel die Reihenfolge bzw. eine Sortierung ein Thema sein könnte, würde ich auf jeden Fall noch einen Test mit z.b.

Code: Alles auswählen

[(4, 6) ,(5, 1), (3, 2)]
machen. Könnte ja sein dass beim `calculate` in den Anforderungen eine sortierte Liste als Ergebnis vorgesehen ist und im Code dann vergessen wurde. Naja, für's prüfen was bei anderen Eingabetypen als nur Integer passiert, da noch ein paar Test mit anderen Datentypen... je länger ich darüber nach denke, desto mehr Tests werden es.

Aber der wesentliche Punkt ist: um die Funktion der Funktion nach Code-Änderungen zu prüfen, muss man sie mit jedem Test-Datensatz nur einmal und nicht 6000mal aufrufen. Falls das so nicht machbar ist... hat man ein Problem. ;)
Benutzeravatar
grubenfox
User
Beiträge: 651
Registriert: Freitag 2. Dezember 2022, 15:49

zum eigentlichen Thema Performance-Probleme kann ich leider wenig beitragen. Meine einzigen Performance-Probleme mit Python an die ich mich erinnern kann, ergaben sich zur Weihnachtszeit beim "Advent of Code" und da hatte ich das Programm einfach zwei oder dreimal ein paar Stunden durchlaufen lassen und war dann fertig.
Was noisefloor oben schrieb "PyPy statt CPython" kann ich bestätigen. Ich habe es selbst nur einmal ausprobiert: wenn die passenden Vorraussetzungen vorliegen, dann bringt PyPy erstaunlich viel bei Null Änderungen am Code!
Ansonsten das wichtigste beim Thema Performance: messen wo sich die Flaschenhälse den eigentlich verstecken...
Wo ich kürzlich drüber gestolpert bin: py-spy und https://codilime.com/blog/spying-on-python-with-py-spy/... sieht für mich cool aus (für die, die es mögen: ist in Rust geschrieben), vielleicht hilft es ja.
Benutzeravatar
Dennis89
User
Beiträge: 1711
Registriert: Freitag 11. Dezember 2020, 15:13

Danke für eure Antworten.

Das mit den kleinen Funktionen und Tests ist mir bewusst, nur ist der Code nicht von mir und bis der auf einem Stand ist, muss er nach jeder Änderung komplett durchlaufen. Je nach dem wie geübt man ist, muss man das sicherlich nicht nach jeder kleinen Änderung machen, mir persönlich passieren aber immer mal wieder kleine Fehler und die will ich zum "Schluss" dann nicht noch suchen müssen.

Das Problem ist recht simple, es wird wie gezeigt Schleifen in Schleifen ausgeführt und immer wieder gerechnet und gerechnet und dass ist der Punkt, der die meiste Zeit beansprucht. Die Lösung wird hier ziemlich sicher, die von @Sirius3 angesprochene Methode sein. Zurzeit kämpfe ich damit, eine Struktur in den Code zu bekommen um besser erkennen zu können, was abhängig von den Schleifen berechnet wird und wie ich das dann ein einem Vektor darstelle.
`PyPy`und Co. ist mir bekannt und habe ich erst mal ausgeschlossen, genau so wie die Auslagerung von Berechnungen in eine andere Sprache, weil ich weiß, dass das in Python schneller geht und dann mag ich das auch hinbekommen. Zumindest will ich es nicht unversucht lassen.

Ich habe die Hoffnung, dass sich nach der Umstrukturierung auch ein besseres Minimalbeispiel ableiten lässt, das ich dann hier zeigen kann.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
noisefloor
User
Beiträge: 4310
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

"Schleifen in Schleifen" spricht schon dafür, dass PyPy oder Numba einen Boost bringen, ohne den Code zu ändern.

Ansonsten würde ich das auch so sehen, dass numpy und die Verwendung von numpy Arrays was bringen könnte.

Gruß, noisefloor
Antworten