Erfassen der Funktionsausgangszeit mit __gnu_mcount_nc

Lesezeit: 6 Minuten

Ich versuche, ein Leistungsprofil auf einer schlecht unterstützten Prototyp-Embedded-Plattform zu erstellen.

Ich stelle fest, dass das -pg-Flag von GCC Thunks zu veranlasst __gnu_mcount_nc bei Eintritt in jede Funktion einzufügen. Keine Umsetzung von __gnu_mcount_nc ist verfügbar (und der Anbieter ist nicht daran interessiert, zu helfen), aber da es trivial ist, einen zu schreiben, der einfach den Stack-Frame und die aktuelle Zykluszahl aufzeichnet, habe ich dies getan; Dies funktioniert einwandfrei und liefert nützliche Ergebnisse in Bezug auf Anrufer/Aufgerufene-Diagramme und die am häufigsten aufgerufenen Funktionen.

Ich würde wirklich gerne auch Informationen über die Zeit erhalten, die in Funktionskörpern verbracht wird, aber ich habe Schwierigkeiten zu verstehen, wie ich dies angehen soll, wenn nur der Eingang, aber nicht der Ausgang, zu jeder Funktion süchtig wird: Sie können genau sagen, wann jede Funktion eingegeben wird, aber ohne die Exit-Punkte einzuhaken, können Sie nicht wissen, wie viel Zeit bis Sie die nächste Information erhalten, die dem Angerufenen zuzuordnen ist, und wie viel den Anrufern.

Trotzdem sind die GNU-Profiling-Tools tatsächlich nachweislich in der Lage, Laufzeitinformationen für Funktionen auf vielen Plattformen zu sammeln, also haben die Entwickler vermutlich ein Schema im Sinn, um dies zu erreichen.

Ich habe einige bestehende Implementierungen gesehen, die Dinge wie das Verwalten eines Shadow-Callstacks und das Drehen der Rücksendeadresse beim Eintritt in __gnu_mcount_nc so tun, dass __gnu_mcount_nc erneut aufgerufen wird, wenn der Angerufene zurückkehrt; es kann dann das Anrufer/Aufgerufene/SP-Triplet mit der Spitze des Schattenaufrufstapels abgleichen und so diesen Fall vom Anruf beim Eintritt unterscheiden, die Austrittszeit aufzeichnen und korrekt zum Anrufer zurückkehren.

Dieser Ansatz lässt zu wünschen übrig:

  • Es scheint, als wäre es in Gegenwart von Rekursion und Bibliotheken, die ohne das Flag -pg kompiliert wurden, spröde
  • Es scheint, als wäre es schwierig, es mit geringem Overhead oder überhaupt in eingebetteten Multithread-/Multicore-Umgebungen zu implementieren, in denen Toolchain-TLS-Unterstützung fehlt und die Beschaffung der aktuellen Thread-ID teuer/komplex sein kann

Gibt es einen offensichtlich besseren Weg, um ein __gnu_mcount_nc zu implementieren, damit ein -pg-Build in der Lage ist, die Funktionsbeendigung sowie die Eintrittszeit zu erfassen, die mir fehlt?

Benutzer-Avatar
Mike Dunlavey

gprof verwendet diese Funktion nicht für das Timing des Eintritts oder Exit, sondern zum Zählen der Aufrufe von Funktion A, die eine beliebige Funktion B aufruft. Stattdessen verwendet es die Eigenzeit, die durch Zählen von PC-Abtastwerten in jeder Routine gesammelt wurde, und verwendet dann die Anzahl der Aufrufe von Funktion zu Funktion, um abzuschätzen, wie viel von diesem Selbst -Zeit sollte den Anrufern zurückverrechnet werden.

Wenn beispielsweise A 10 Mal C anruft und B 20 Mal C anruft und C 1000 ms Eigenzeit hat (dh 100 PC-Samples), dann gprof weiß, dass C 30 Mal angerufen wurde, und 33 der Samples können A in Rechnung gestellt werden, während die anderen 67 B in Rechnung gestellt werden können. In ähnlicher Weise propagieren Sample-Zählungen die Aufrufhierarchie nach oben.

Sie sehen also, es funktioniert zeitlich nicht Ein- und Aussteigen. Die Messungen, die es erhält, sind sehr grob, da es nicht zwischen kurzen Anrufen und langen Anrufen unterscheidet. Auch wenn ein PC-Sample während der E/A oder in einer Bibliotheksroutine auftritt, die nicht mit -pg kompiliert wurde, wird es überhaupt nicht gezählt. Und wie Sie bemerkt haben, ist es bei Rekursion sehr spröde und kann bei kurzen Funktionen einen erheblichen Overhead verursachen.

Ein weiterer Ansatz ist Stack-Sampling statt PC-Sampling. Zugegeben, es ist teurer, ein Stack-Sample zu erfassen als ein PC-Sample, aber es werden weniger Samples benötigt. Wenn zum Beispiel eine Funktion, eine Codezeile oder eine beliebige Beschreibung, die Sie machen möchten, in Bruchteil F von insgesamt N Stichproben offensichtlich ist, dann wissen Sie, dass der Bruchteil der Zeit, die es kostet, F ist, mit einer Standardabweichung von sqrt(NF(1-F)). Wenn Sie also beispielsweise 100 Proben nehmen und eine Codezeile auf 50 davon erscheint, dann können Sie die Zeilenkosten für 50 % der Zeit schätzen, mit einer Unsicherheit von sqrt(100*.5*.5) = +/- 5 Proben oder zwischen 45 % und 55 %. Wenn Sie 100-mal so viele Samples nehmen, können Sie die Unsicherheit um den Faktor 10 reduzieren. (Rekursion spielt keine Rolle. Wenn eine Funktion oder Codezeile dreimal in einem einzelnen Sample vorkommt, zählt das als 1 Sample, nicht als 3 . Es spielt auch keine Rolle, ob Funktionsaufrufe kurz sind – wenn sie oft genug aufgerufen werden, um einen erheblichen Bruchteil zu kosten, werden sie abgefangen.)

Beachten Sie, wenn Sie nach Dingen suchen, die Sie reparieren können, um die Geschwindigkeit zu erhöhen, spielt der genaue Prozentsatz keine Rolle. Das Wichtigste ist, es zu finden. (Tatsächlich müssen Sie nur ein Problem sehen zweimal um zu wissen, dass es groß genug ist, um es zu reparieren.)

Das ist diese Technik.


PS Lassen Sie sich nicht in Call-Graphs, Hot-Paths oder Hot-Spots verstricken. Hier ist ein typisches Call-Graph-Rattennest. Gelb ist der Hot-Path und Rot ist der Hot-Spot.

Geben Sie hier die Bildbeschreibung ein

Und dies zeigt, wie einfach es für eine saftige Beschleunigungsmöglichkeit ist, an keinem dieser Orte zu sein:

Geben Sie hier die Bildbeschreibung ein

Das Wertvollste, was man sich ansehen sollte, sind etwa ein Dutzend zufällige Raw-Stack-Samples und deren Verknüpfung mit dem Quellcode. (Das bedeutet, das Back-End des Profilers zu umgehen.)

HINZUGEFÜGT: Nur um zu zeigen, was ich meine, habe ich zehn Stack-Samples aus dem obigen Aufrufdiagramm simuliert, und hier ist, was ich gefunden habe

  • 3/10 Proben rufen an class_existseine zum Abrufen des Klassennamens und zwei zum Einrichten einer lokalen Konfiguration. class_exists Anrufe autoload was ruft requireFileund zwei davon rufen an adminpanel. Wenn dies direkter erfolgen kann, könnten etwa 30 % eingespart werden.
  • 2/10 Proben rufen an determineIdder ruft fetch_the_id was ruft getPageAndRootlineWithDomaindie drei weitere Ebenen aufruft und in endet sql_fetch_assoc. Das scheint eine Menge Ärger zu sein, um eine ID zu bekommen, und es kostet ungefähr 20 % der Zeit, und das zählt E/A nicht mit.

Die Stack-Beispiele sagen Ihnen also nicht nur, wie viel Inklusivzeit eine Funktion oder Codezeile kostet, sie sagen Ihnen auch, warum es gemacht wird und welche Albernheiten nötig sind, um es zu erreichen. Ich sehe das oft – galoppierende Allgemeinheit – Fliegen mit Hämmern erschlagen, nicht absichtlich, sondern nur nach gutem modularem Design.

HINZUGEFÜGT: Eine andere Sache, in die man sich nicht hineinziehen lassen sollte, ist Flammendiagramme. Hier ist zum Beispiel ein Flammendiagramm (um 90 Grad nach rechts gedreht) der zehn simulierten Stack-Samples aus dem Anrufdiagramm oben. Die Routinen sind alle nummeriert und nicht benannt, aber jede Routine hat ihre eigene Farbe.
Geben Sie hier die Bildbeschreibung ein

Beachten Sie das Problem, das wir oben identifiziert haben, mit class_exists (Routine 219) auf 30 % der Proben liegt, ist überhaupt nicht offensichtlich, wenn man sich das Flammendiagramm ansieht. Mehr Samples und andere Farben würden den Graphen “flammenartiger” aussehen lassen, aber Routinen, die viel Zeit in Anspruch nehmen, weil sie viele Male von verschiedenen Orten aus aufgerufen werden, nicht bloßstellen.

Hier sind die gleichen Daten, sortiert nach Funktion und nicht nach Zeit. Das hilft ein wenig, aggregiert aber keine Ähnlichkeiten, die von verschiedenen Orten aufgerufen werden:
Geben Sie hier die Bildbeschreibung ein

Auch hier ist das Ziel, die Probleme zu finden, die sich vor Ihnen verstecken. Jeder kann die einfachen Dinge finden, aber die Probleme, die sich verstecken, sind diejenigen, die den Unterschied ausmachen.

HINZUGEFÜGT: Eine andere Art von Augenschmaus ist diese hier:
Geben Sie hier die Bildbeschreibung ein
wo die schwarz umrandeten Routinen alle gleich sein könnten, nur von verschiedenen Orten aufgerufen. Das Diagramm aggregiert sie nicht für Sie. Wenn eine Routine einen hohen inklusiven Prozentsatz hat, weil sie viele Male von verschiedenen Orten aus aufgerufen wurde, wird sie nicht angezeigt.

1013550cookie-checkErfassen der Funktionsausgangszeit mit __gnu_mcount_nc

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy