Debugging von Performance-Problemen

Diese Seite verwendet Cookies. Durch die Nutzung unserer Seite erklären Sie sich damit einverstanden, dass wir Cookies setzen. Weitere Informationen

  • gritsch schrieb:

    trenne doch mal das laden und das grafische darstellen komplett.


    dann siehst du ja auch welcher der beiden teile viel zeit benötigt.
    Ich denke ich bin schon viel weiter und habe das meiste ja schon am Anfang geschrieben...

    1. ein Breakpoint und Zeitstempel in makeWindowControllers zeigt, dass die allermeiste Zeit beim Umwandeln verbraten wird. Bevor etwas gezeichnet wird. Außerdem wird zunächst auch nur eine von 50 geladenen Seiten angezeigt. Blättern zwischen Seiten ist nicht erkennbar langsam, also unter 0.1 Sekunden
    2. der XML-Parser braucht insgesamt ca. 0.5 Sekunden. D.h. libxml2 würde das nicht wesentlich beschleunigen.
    3. das Langsame ist das Umwandeln. Und dass es so lang dauert liegt daran, dass ich einen eigenen setNeedsDisplay + Caching-Mechanismus habe, der ähnlich wie eine NSView-Heierarchie arbeitet, aber auf meinen CAD-Objekten. Z.B. muß man ein Kreis neu gemalt werden, wenn sich seine Größe ändert. Das mußt er allen darüberliegenden Objekten und dem echten NSView mitteilen. Das geht per Rekursion durch den Objektbaum. D.h. wenn ich Dummy-Objekte anlege wird es natürlich schneller werden, verliert aber die Funktion, so dass es mir auch nichts sagen wird.
    4. NSNumber nehme ich in COUNT_M() nur, um möglichst einfach die Anzahl der Methodencalls in einem Code-Block zu zählen, aber nicht um irgendwie die absolute Rechenzeit zu bestimmen.

    Im Prinzip ist das Problem sehr ähnlich wie wenn man 60000 NSViews hat und die effizient malen will. Dann muß man mit setNeedsDisplay arbeiten, damit das Malen von einem gegebenen Ausschnitt auch nur den wirklich veränderten Anteil neu malt.

    Ws Du mit mehreren Threads meinst ist mir nicht klar und wie das die Zeit von Programmstart bis zur Anzeige des Fensters mit der ersten Seite beschleunigen soll. Der Rechnaufwand ist der gleiche und Anzeigen kann ich erst wenn alles geladen ist (weil die Daten Abhängigkeiten haben die oft erst in der letzten Zeile aufgelöst werden). Das Einzige wo ich eine Möglichkeit sehe ist dass das Umwandeln zunächst nur meine Objekte anlegt und nur eine Referenz auf den XMLNode speichert. Und erst wenn man ein Objekt zum ersten Mal anzeigen will oder sonst irgendwie seine Daten braucht, wird es aus XML gewandelt. Dann zerhacke ich das Laden von 50 Seiten ggf. auf 50 Einzelschritte wenn der User durch eine Seite nach der anderen blättert.

    Aber ich glaube jetzt kommen wir vom Thema "Debugging-Methoden" zu "Softwarearchitektur"... Also vom optimalen Werkzeug (und das war ja meine Frage) zum konkreten Problem.
  • Und was hat setNeedsDisplay und caching mit der umwandlung von XML in eine objektstruktur zu tun?

    Erstelle einfach mal aus dem XML die objektstruktur mit den daten aus dem XML die du benötigst. Diese so effizient wie möglich speichern. Umwandeln oder cachen kannst du das ganze dann nachher in threads auch noch und das beschleunigt das ganze schon mal um den faktor 4, 8 oder gar 16 je nach CPU.

    Im Profiler kannst du dir rekursion übrigens flaten lassen.
  • gritsch schrieb:

    Und was hat setNeedsDisplay und caching mit der umwandlung von XML in eine objektstruktur zu tun?

    Erstelle einfach mal aus dem XML die objektstruktur mit den daten aus dem XML die du benötigst. Diese so effizient wie möglich speichern. Umwandeln oder cachen kannst du das ganze dann nachher in threads auch noch und das beschleunigt das ganze schon mal um den faktor 4, 8 oder gar 16 je nach CPU.

    Im Profiler kannst du dir rekursion übrigens flaten lassen.
    Die Setter der Objekte sind die gleichen sind wie sie später verwendet werden. Dann soll z.B. ein setDiameter: die Grafik triggern und folglich in einem setNeedsDisplay enden. So wie ein setStringValue: bei NSTextField. Also ruft ein Setter indirekt den setNeedsDisplay auf.

    Ja, das kann man in manchen Fällen bei der ersten Initialisierung überspringen, aber nicht immer ist das so einfach. Z.B. wenn die gleiche Umwandlung für einen "Import..." die vorhandenen Daten ergänzen soll. Entweder muß ich also ein extra-Flag "firstpass" einführen oder einen Teil des Codes verdoppeln (z.B. setter ohne und setter mit Notification). Das ist genau eines der Probleme, die ich mit o.g. Methodik bereits identifiziert habe. Und welche Lösung die beste ist muß ich ausprobieren.

    Außerdem interessiert mich hier die beste Methodik des Debuggens von Performance-Problemen, nicht evtl. Lösungsansätze fürs konkrete Problem. Da gibts viele Wege... Nur muß man erst mal wissen wo man ist und warum :)
  • wie bereits von mehreren gesagt: die beste methodik ist der profiler. eventuell mal einen tag dafür aufwenden um den komplett zu verstehen. paar testprojekte schreiben um zu erkennen wie der unterschiedliche probleme finden kann. es gibt ja zig einstellungsmöglichkeiten beim anzeigen der prfilingergebnisse.
  • Ich habe mich auch anfangs gegen Instruments gewehrt und habe bis heute noch nicht ganz den Durchblick. Aber zum testen der Performance finde ich es sehr hilfreich. Man darf halt nicht vergessen ohne Optimierung und mit Profilinginformationen zu komiplieren, also DEBUG verwenden. Sonst werden Funktionen zusammengefasst oder Aufrufe versteckt und nicht mehr ausgewiesen. Dann bei "Call Tree" alles anhaken außer "Top Functions" und man hat eine schöne Liste mit %-Auslastung und den aufrufenden Funktionen.

    Punkto ObjectiveC-Performance ist zu beachten, dass ein Methodenaufruf eine sehr zeitaufwändige, weil dynamische Sache mit mutexen u.ä. ist. Daher sollte man überall dort wo es wirklich auf die Zeit ankommt möglichst wenige ObjectiveC-Methoden und lieber C-Funktionen verwenden. Der Klassiker unter den Zeitfressern ist das [array count] im Kontrollteil einer Schleife. Auch NSString ist wegen des hohen Speicherbedarfs mit Vorsicht zu genießen. Bei Arrays und Dicts gilt außerdem (wie schon oben erwähnt) die Größe initial zu setzen. Das spart Arrays ein späteres resizen und Dictionarys ein rehash. Mit Notifications sollte auch sorgsam umgegangen werden. Die Grundidee war nicht sie als Ersatz für Methoden zu verwenden. Notifications innerhalb einer Schleife zu verschicken würde ich als Konzeptionsfehler ansehen, ebenso den Aufbau eines UI innerhalb der Schleife.

    Die Sache mit dem "display" hab ich nicht ganz verstanden. Prinzipiell sollte nur ein einziges setNeedsDisplay am Ende des gesamten Einlesevorganges stehen. Außer man will während des Imports sehen was passiert. Aber dann wirds richtig langsam. Dabei genügt schon die Anzeige eines einzelnen Textfeldes um die Performance spürbar zu reduzieren. Also besser nicht.
  • Thomas schrieb:

    Man darf halt nicht vergessen ohne Optimierung und mit Profilinginformationen zu komiplieren, also DEBUG verwenden. Sonst werden Funktionen zusammengefasst oder Aufrufe versteckt und nicht mehr ausgewiesen
    Xcode verwendet für die Profile-Action die Release-Einstellungen also eben kein Debug. Das ist auch sinnvoll, da ja gerade die Performance der Produktions-App relevant ist. Den Overhead des Debug-Codes braucht man ja nicht zu messen. Performance-Leaks sollte man immer im optimierten Code suchen.

    Nichtsdestotrotz sollte man sich natürlich die Symbole erzeugen lassen. Am besten als separate dSYM-Datei. Das reicht auch dem Profiler vollkommen aus, und er versteckt auch keine Aufrufe.
    „Meine Komplikation hatte eine Komplikation.“
  • Wenn man in Instruments zu einer laufenden App verbindet dann kann man auch debug verwenden. Ich programmiere hauptsächlich in C und da werden viele Funktionen in der release durch inlining eingebunden. Diese Funktionen scheinen dann nicht in der Liste auf. Bei ObjectiveC mag das natürlich weniger eine Rolle spielen.
  • Thomas schrieb:

    Punkto ObjectiveC-Performance ist zu beachten, dass ein Methodenaufruf eine sehr zeitaufwändige, weil dynamische Sache mit mutexen u.ä. ist. Daher sollte man überall dort wo es wirklich auf die Zeit ankommt möglichst wenige ObjectiveC-Methoden und lieber C-Funktionen verwenden. Der Klassiker unter den Zeitfressern ist das [array count] im Kontrollteil einer Schleife. Auch NSString ist wegen des hohen Speicherbedarfs mit Vorsicht zu genießen. Bei Arrays und Dicts gilt außerdem (wie schon oben erwähnt) die Größe initial zu setzen. Das spart Arrays ein späteres resizen und Dictionarys ein rehash. Mit Notifications sollte auch sorgsam umgegangen werden. Die Grundidee war nicht sie als Ersatz für Methoden zu verwenden. Notifications innerhalb einer Schleife zu verschicken würde ich als Konzeptionsfehler ansehen, ebenso den Aufbau eines UI innerhalb der Schleife.

    Die Sache mit dem "display" hab ich nicht ganz verstanden. Prinzipiell sollte nur ein einziges setNeedsDisplay am Ende des gesamten Einlesevorganges stehen. Außer man will während des Imports sehen was passiert. Aber dann wirds richtig langsam. Dabei genügt schon die Anzeige eines einzelnen Textfeldes um die Performance spürbar zu reduzieren. Also besser nicht.
    Die Obj-C-Methodenaufrufe an sich sind hier nicht das Problem, sondern ein Bug in der Konzeption der nicht offensichtlich ist (und darum ging es ja den zu finden). Es werden nämlich Hilfs-Methoden bis zu 50 mal so oft aufgerufen wie Objekte angelegt werden. Das sollte nicht sein und ist nicht ganz einsichtig warum das so (im Laufe der Code-Evolution wo andere Optimierungen entstanden sind) geworden ist. Da hilft auch der Profiler nicht weiter, sondern Xcode single-stepping und andere Debugging-Tricks.

    Das mit dem setNeedsDisplay ist natürlich ein Kompromiß ob man MVC in Reinkultur implementiert oder nicht. Entweder hat man für die eingelesenen Objekte reine Setter (im Model) und muß dann jedes Mal am Schluß von Änderungen ein setNeedsDisplay explizit aufrufen (im Controller). Oder der Setter macht das selbst (aber dann ggf. mehrfach). Und damit auch beim Einlesen. Klar kann man beim ersten Laden das alles weglassen und genau einen setNeedsDisplay aufrufen, aber wenn man aus einem Algorithmus heraus z.B. einzelne Objekte verschiebt, ist es von der Code-Struktur viel praktischer, wenn jedes Objekt bereits kapselt dass es überhaupt ein Display gibt... D.h. man setzt einfach einen neuen Mittelpunkt für einen Kreis im CAD und schwupp ist er beim nächsten Lauf der RunLoop schon dort. Oder man setzt den Radius neu. Klar kann man dann erst den Algorithmus aufrufen und am Schluß einen setNeedsDisplay. Aber noch komplizierter wird es wenn man wegen den vielen tausend gleichzeitig sichtbaren Objekten das Wissen ausnutzen will/muß, dass da nur ein kleiner Bildschirmbereich verändert wurde (setNeedsDisplayInRect). Das kann man gar nicht mehr hinterher bestimmen. Das ist also eine Entscheidung wie automatisch man Änderungen am Datenmodell in den View propagiert. Und das hat Laufzeitauswirkungen die ich ja inzwischen besser verstehe.

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von hns ()

  • macmoonshine schrieb:

    Du kannst ja das Inlining ggf. abschalten. Mit Debug-Code bekommst du aber ein verzerrtes Ergebnis. Da kann dann Code als problematisch erscheinen, der dank Optimizer völlig harmlos ist.
    Deshalb bin ich ja dazu übergegangen nicht die relate Laufzeit zu messen sondern die Methodenaufrufe in einem Stück Code zählen zu lassen. Deren relatives Verhältnis (Methode a wird 10 mal so häufig aufgerufen wie Methode b) sagt mir schon sehr viel. Das ist auch vom Compiler und Release/Debug (fast) unabhängig, da er ja seltenst Methodenaufrufe wegoptimiert.
    Im Prinzip wie man einen Sortieralgorithmus auch nicht mit der Stoppuhr verbessert sondern mit O(n)-Überlegungen.