OK, Sie definieren das Problem so, dass es anscheinend nicht viel Raum für Verbesserungen gibt. Das ist meiner Erfahrung nach ziemlich selten. Ich habe versucht, dies in einem Artikel von Dr. Dobbs im November 1993 zu erklären, indem ich von einem konventionell gut gestalteten, nicht trivialen Programm ohne offensichtliche Verschwendung ausgegangen bin und es durch eine Reihe von Optimierungen geführt habe, bis die Wanduhrzeit von 48 Sekunden verkürzt wurde auf 1,1 Sekunden, und die Quellcode-Größe wurde um den Faktor 4 reduziert. Mein Diagnosetool war dies . Die Reihenfolge der Änderungen war folgende:
Das erste gefundene Problem war die Verwendung von Listenclustern (jetzt als "Iteratoren" und "Containerklassen" bezeichnet), die mehr als die Hälfte der Zeit ausmachen. Diese wurden durch ziemlich einfachen Code ersetzt, wodurch sich die Zeit auf 20 Sekunden verringerte.
Jetzt ist der größte Zeitnehmer mehr das Erstellen von Listen. In Prozent war es vorher nicht so groß, aber jetzt liegt es daran, dass das größere Problem beseitigt wurde. Ich finde einen Weg, es zu beschleunigen, und die Zeit sinkt auf 17 Sekunden.
Jetzt ist es schwieriger, offensichtliche Schuldige zu finden, aber es gibt einige kleinere, gegen die ich etwas tun kann, und die Zeit sinkt auf 13 Sekunden.
Jetzt scheine ich gegen eine Wand gestoßen zu sein. Die Beispiele sagen mir genau, was es tut, aber ich kann anscheinend nichts finden, was ich verbessern kann. Dann denke ich über das grundlegende Design des Programms und seine transaktionsgesteuerte Struktur nach und frage, ob die gesamte Listensuche, die es durchführt, tatsächlich von den Anforderungen des Problems abhängt.
Dann stieß ich auf ein Re-Design, bei dem der Programmcode tatsächlich (über Präprozessor-Makros) aus einem kleineren Satz von Quellen generiert wird und bei dem das Programm nicht ständig herausfindet, was der Programmierer als ziemlich vorhersehbar kennt. Mit anderen Worten, "interpretieren" Sie die Abfolge der zu erledigenden Aufgaben nicht, "kompilieren" Sie sie.
- Durch diese Neugestaltung wird der Quellcode um den Faktor 4 verkleinert und die Zeit auf 10 Sekunden reduziert.
Jetzt, da es so schnell geht, ist es schwierig zu probieren, also gebe ich zehnmal so viel Arbeit, aber die folgenden Zeiten basieren auf der ursprünglichen Arbeitslast.
Mehr Diagnose zeigt, dass es Zeit in der Warteschlangenverwaltung verbringt. Durch das Einkleiden wird die Zeit auf 7 Sekunden reduziert.
Jetzt ist der Diagnosedruck, den ich gemacht habe, ein großer Zeitvertreib. Spülen Sie das - 4 Sekunden.
Jetzt sind die größten Zeitnehmer Anrufe zu malloc und frei . Objekte recyceln - 2,6 Sekunden.
Wenn ich weiter probiere, finde ich immer noch Operationen, die nicht unbedingt notwendig sind - 1,1 Sekunden.
Gesamtbeschleunigungsfaktor: 43,6
Jetzt sind keine zwei Programme gleich, aber in Nicht-Spielzeug-Software habe ich immer einen solchen Fortschritt gesehen. Zuerst bekommen Sie die einfachen Sachen und dann die schwierigeren, bis Sie zu einem Punkt kommen, an dem die Renditen sinken. Dann kann die gewonnene Erkenntnis durchaus zu einer Neugestaltung führen, die eine neue Runde von Beschleunigungen startet, bis Sie erneut auf sinkende Renditen stoßen. Nun ist dies der Punkt , an dem es Sinn , sich zu fragen , machen könnte , ob ++i
oder i++
oder for(;;)
oder while(1)
sind schneller: die Arten von Fragen , die ich sehen , so oft auf Stack - Überlauf.
PS Es mag sich fragen, warum ich keinen Profiler verwendet habe. Die Antwort ist, dass fast jedes dieser "Probleme" eine Funktionsaufrufstelle war, die Stichprobenstapel punktgenau stapelt. Profiler kommen auch heute noch kaum auf die Idee, dass Anweisungen und Aufrufanweisungen wichtiger zu lokalisieren und einfacher zu reparieren sind als ganze Funktionen.
Ich habe tatsächlich einen Profiler erstellt, um dies zu tun, aber für eine echte Intimität mit dem, was der Code tut, gibt es keinen Ersatz dafür, dass Sie Ihre Finger richtig darin haben. Es ist kein Problem, dass die Anzahl der Proben gering ist, da keines der gefundenen Probleme so klein ist, dass sie leicht übersehen werden.
HINZUGEFÜGT: jerryjvl hat einige Beispiele angefordert. Hier ist das erste Problem. Es besteht aus einer kleinen Anzahl separater Codezeilen, die zusammen mehr als die Hälfte der Zeit in Anspruch nehmen:
/* IF ALL TASKS DONE, SEND ITC_ACKOP, AND DELETE OP */
if (ptop->current_task >= ILST_LENGTH(ptop->tasklist){
. . .
/* FOR EACH OPERATION REQUEST */
for ( ptop = ILST_FIRST(oplist); ptop != NULL; ptop = ILST_NEXT(oplist, ptop)){
. . .
/* GET CURRENT TASK */
ptask = ILST_NTH(ptop->tasklist, ptop->current_task)
Diese verwendeten den Listencluster ILST (ähnlich einer Listenklasse). Sie werden auf die übliche Weise implementiert, wobei "Informationen verbergen" bedeutet, dass die Benutzer der Klasse sich nicht darum kümmern sollten, wie sie implementiert wurden. Als diese Zeilen geschrieben wurden (aus ungefähr 800 Codezeilen), wurde nicht daran gedacht, dass dies ein "Engpass" sein könnte (ich hasse dieses Wort). Sie sind einfach die empfohlene Art, Dinge zu tun. Im Nachhinein ist es leicht zu sagen, dass diese hätten vermieden werden müssen, aber meiner Erfahrung nach sind alle Leistungsprobleme so. Im Allgemeinen ist es gut zu versuchen, Leistungsprobleme zu vermeiden. Es ist sogar noch besser, diejenigen zu finden und zu reparieren, die erstellt wurden, obwohl sie (im Nachhinein) "hätten vermieden werden müssen".
Hier ist das zweite Problem in zwei getrennten Zeilen:
/* ADD TASK TO TASK LIST */
ILST_APPEND(ptop->tasklist, ptask)
. . .
/* ADD TRANSACTION TO TRANSACTION QUEUE */
ILST_APPEND(trnque, ptrn)
Hierbei werden Listen erstellt, indem Elemente an ihre Enden angehängt werden. (Der Fix bestand darin, die Elemente in Arrays zu sammeln und die Listen auf einmal zu erstellen.) Das Interessante ist, dass diese Anweisungen nur 3/48 der ursprünglichen Zeit kosten (dh auf dem Aufrufstapel waren), sodass sie nicht vorhanden waren Tatsache, ein großes Problem am Anfang . Nachdem sie das erste Problem beseitigt hatten, kosteten sie 3/20 der Zeit und waren nun ein "größerer Fisch". Im Allgemeinen geht es so.
Ich könnte hinzufügen, dass dieses Projekt aus einem echten Projekt destilliert wurde, an dem ich mitgearbeitet habe. In diesem Projekt waren die Leistungsprobleme weitaus dramatischer (ebenso wie die Beschleunigungen), z. B. das Aufrufen einer Datenbankzugriffsroutine innerhalb einer inneren Schleife, um festzustellen, ob eine Aufgabe abgeschlossen wurde.
HINWEIS HINZUGEFÜGT: Der Quellcode, sowohl original als auch neu gestaltet, befindet sich unter www.ddj.com für 1993 in den Dateien 9311.zip, den Dateien slug.asc und slug.zip.
EDIT 26.11.2011: Es gibt jetzt ein SourceForge-Projekt, das Quellcode in Visual C ++ und eine ausführliche Beschreibung der Optimierung enthält. Es durchläuft nur die erste Hälfte des oben beschriebenen Szenarios und folgt nicht genau der gleichen Reihenfolge, erhält aber dennoch eine Beschleunigung um 2-3 Größenordnungen.