
Vor ein paar Monaten kam ein historischer Moment fĂŒr mich. Standard-Betriebssystem-Tools zum Messen der Zeit reichen fĂŒr mich nicht mehr aus. Es dauerte einige Zeit, um mit Nanosekundengenauigkeit und mit Nanosekunden-Overhead zu messen.
Ich beschloss, eine Bibliothek zu schreiben, die dieses Problem lösen wĂŒrde. Auf den ersten Blick schien es nichts Besonderes zu geben. Bei nĂ€herer Betrachtung stellte sich jedoch wie immer heraus, dass es viele interessante Probleme gab, die gelöst werden mussten. In diesem Artikel werde ich ĂŒber die Probleme sprechen und wie sie gelöst wurden.
Da Sie viele verschiedene Arten von Zeit auf einem Computer messen können, werde ich sofort klarstellen, dass wir hier ĂŒber "Zeit mit Stoppuhr" sprechen werden. Oder Wanduhrzeit. Es ist Echtzeit, verstrichene Zeit usw. Das heiĂt, eine einfache "menschliche" Zeit, die wir zu Beginn der Aufgabe erkennen und am Ende anhalten.
Mikrosekunde - fast fĂŒr immer
Entwickler von Hochleistungssystemen haben sich in den letzten Jahren an die Mikrosekunden-Zeitskala gewöhnt. In Mikrosekunden können Sie Daten von einem NVMe-Laufwerk lesen. In Mikrosekunden können Daten ĂŒber das Netzwerk gesendet werden. NatĂŒrlich nicht fĂŒr jedermann, aber fĂŒr InifiniBand-Netzwerk - ganz einfach.
Gleichzeitig hatte die Mikrosekunde auch eine Struktur. Ein vollstĂ€ndiger E / A-Stapel besteht aus mehreren Software- und Hardwarekomponenten. Die von einigen von ihnen eingefĂŒhrten Verzögerungen liegen im Submikrosekundenbereich.
Um Verzögerungen dieser GröĂenordnung zu messen, reicht die Mikrosekundengenauigkeit nicht mehr aus. Es ist jedoch nicht nur die Genauigkeit wichtig, sondern auch der Aufwand fĂŒr die Messzeit. Der Linux-Systemaufruf clock_gettime () gibt die Zeit mit einer Genauigkeit von Nanosekunden zurĂŒck. Auf einem Computer, der mir direkt zur VerfĂŒgung steht (IntelÂź XeonÂź CPU E5-2630 v2 bei 2,60 GHz), wird dieser Anruf in ca. 120 ns abgeschlossen. Sehr gute Figur. AuĂerdem funktioniert clock_gettime () ziemlich vorhersehbar. Auf diese Weise können Sie den Overhead seines Anrufs berĂŒcksichtigen und tatsĂ€chlich Messungen mit einer Genauigkeit in der GröĂenordnung von zehn Nanosekunden durchfĂŒhren. Lassen Sie uns jetzt jedoch darauf achten. Um das Zeitintervall zu messen, mĂŒssen Sie zwei solche Anrufe tĂ€tigen: am Anfang und am Ende. Das heiĂt, 240 ns ausgeben. Wenn dicht beabstandete Zeitintervalle in der GröĂenordnung von 1-10 ÎŒs gemessen werden, verzerrt in einigen solchen FĂ€llen der Messprozess selbst den beobachteten Prozess erheblich.
Ich habe diesen Abschnitt damit begonnen, wie sich der E / A-Stapel in den letzten Jahren beschleunigt hat. Dies ist neu, aber bei weitem nicht der einzige Grund, die Zeit schnell und genau messen zu wollen. Ein solches BedĂŒrfnis war schon immer. Zum Beispiel gab es immer einen Code, den ich um mindestens 1 Taktzyklus des Mikroprozessors beschleunigen wollte. Oder ein anderes Beispiel aus dem Originalartikel ĂŒber die sensationelle Spectre-SicherheitslĂŒcke:

Hier messen die Zeilen 72 bis 74 die AusfĂŒhrungszeit einer einzelnen Speicherzugriffsoperation. Specter interessiert sich zwar nicht fĂŒr Nanosekunden. Die Zeit kann in "Papageien" gemessen werden. Wir werden zu den Papageien und Sekunden zurĂŒckkehren.
ZeitstempelzÀhler
Der SchlĂŒssel zur schnellen und genauen Zeitmessung ist ein spezieller MikroprozessorzĂ€hler. Der Wert dieses ZĂ€hlers wird normalerweise in einem separaten Register gespeichert und ist normalerweise - aber nicht immer - vom Benutzerbereich aus zugĂ€nglich. Auf verschiedenen Architekturen wird der ZĂ€hler unterschiedlich genannt:
- x86 ZeitstempelzÀhler
- Zeitbasisregister auf PowerPC
- IntervallzeitzÀhler auf Itanium
- usw.
Im Folgenden werde ich immer den Namen "ZeitstempelzÀhler" oder TSC verwenden, obwohl ich in Wirklichkeit jeden solchen ZÀhler unabhÀngig von der Architektur im Auge behalten werde.
Das Lesen des TSC-Werts ist normalerweise - aber auch nicht immer - mit einer einzigen Anweisung möglich. Hier ist ein Beispiel fĂŒr x86. Genau genommen ist dies keine reine Assembler-Anweisung, sondern der GNU-Inline-Assembler:
uint32_t eax, edx; __asm__ __volatile__( "rdtsc" : "=a" (eax), "=d" (edx));
Der Befehl rdtsc platziert zwei 32-Bit-HÀlften des TSC-Registers in den Registern eax und edx. Von diesen können Sie einen einzelnen 64-Bit-Wert "kleben".
Ich stelle noch einmal fest: Diese (und Àhnliche) Anweisungen können in den meisten FÀllen direkt aus dem Benutzerbereich aufgerufen werden. Keine Systemaufrufe. Minimaler Overhead.
Was muss jetzt getan werden, um die Zeit zu messen?
- FĂŒhren Sie eine solche Anweisung zu Beginn des fĂŒr uns interessanten Zeitintervalls aus. ZĂ€hlerwert merken
- FĂŒhren Sie am Ende eine solche Anweisung aus. Wir glauben, dass der Wert des ZĂ€hlers von der ersten Anweisung zur zweiten steigen wird. Warum wird es sonst benötigt? Denken Sie an den zweiten Wert
- Wir betrachten den Unterschied zwischen den beiden gespeicherten Werten. Dies ist unsere Zeit
Es sieht einfach aus, aber ...
Die nach dem beschriebenen Verfahren gemessene Zeit wird in âPapageienâ ausgedrĂŒckt. Es ist nicht in Sekunden. Aber manchmal sind Papageien genau das, was Sie brauchen. Es gibt Situationen, in denen nicht die absoluten Werte der Zeitintervalle wichtig sind, sondern die Beziehung zwischen den verschiedenen Intervallen. Das obige Spectre-Beispiel zeigt genau diese Situation. Die Dauer jedes einzelnen Speicherzugriffs spielt keine Rolle. Es ist nur wichtig, dass Aufrufe an einige Adressen viel schneller ausgefĂŒhrt werden als an andere (abhĂ€ngig davon, ob die Daten im Cache oder im Hauptspeicher gespeichert sind).
Aber was ist, wenn keine Papageien benötigt werden, sondern Sekunden / Mikrosekunden / Nanosekunden usw.? Hier lassen sich zwei grundsÀtzlich unterschiedliche FÀlle unterscheiden:
- Aber dann werden Nanosekunden benötigt. Das heiĂt, es ist zulĂ€ssig, zuerst alle erforderlichen Messungen in Papageien durchzufĂŒhren und sie zur weiteren Verarbeitung irgendwo zu speichern (z. B. im Speicher). Und erst nach Abschluss der Messungen werden die gesammelten Papageien langsam in Sekunden umgewandelt
- Nanosekunden werden "on the fly" benötigt. Zum Beispiel hat Ihr Messprozess eine Art âVerbraucherâ, den Sie nicht kontrollieren und der Zeit im âmenschlichenâ Format erwartet
Der erste Fall ist einfach, der zweite - erfordert Einfallsreichtum. Die Konvertierung sollte so effektiv wie möglich sein. Wenn es viele Ressourcen verbraucht, kann es den Messprozess stark verzerren. Wir werden im Folgenden ĂŒber eine effektive Konvertierung sprechen. Hier haben wir dieses Problem bisher identifiziert und gehen zu einem anderen ĂŒber.
ZeitstempelzÀhler sind nicht so einfach, wie wir es gerne hÀtten. Auf einigen Architekturen:
- Es kann nicht garantiert werden, dass die TSC mit hoher Frequenz aktualisiert wird. Wenn die TSC beispielsweise einmal pro Mikrosekunde aktualisiert wird, ist es nicht möglich, Nanosekunden damit zu reparieren.
- Die HĂ€ufigkeit, mit der die TSC aktualisiert wird, kann im Laufe der Zeit variieren
- Auf verschiedenen im System vorhandenen CPUs können TSCs mit unterschiedlichen Frequenzen aktualisiert werden
- Es kann zu einer Verschiebung zwischen TSCs kommen, die auf verschiedenen CPUs ticken
Hier ist ein Beispiel, das das letzte Problem veranschaulicht. Angenommen, wir haben ein System mit zwei CPUs: CPU1 und CPU2. Angenommen, die TSC auf der ersten CPU liegt um die Anzahl der Ticks hinter der zweiten, was 5 Sekunden entspricht. Angenommen, im System wird ein Stream gestartet, der die Zeit der Berechnungen misst, was er selbst tut. Dazu liest der Stream zuerst den TSC-Wert, fĂŒhrt dann die Berechnung durch und liest dann den zweiten TSC-Wert. Wenn ein Thread wĂ€hrend seiner gesamten Lebensdauer nur auf einer CPU verbleibt - auf einer beliebigen -, gibt es keine Probleme. Was aber, wenn der Thread auf CPU1 gestartet, dort den ersten TSC-Wert gemessen und dann in der Mitte der Berechnungen vom Betriebssystem auf CPU2 verschoben wurde, wo er den zweiten TSC-Wert las? In diesem Fall erscheinen die Berechnungen 5 Sekunden lĂ€nger als sie tatsĂ€chlich sind.
Aufgrund der oben aufgefĂŒhrten Probleme kann TSC auf einigen Systemen nicht als zuverlĂ€ssige Zeitquelle dienen. Auf anderen Systemen, die unter denselben Problemen "leiden", kann TSC jedoch weiterhin verwendet werden. Möglich wird dies durch besondere architektonische Merkmale:
- Das GerĂ€t kann jedes Mal einen speziellen Interrupt erzeugen, wenn sich die HĂ€ufigkeit Ă€ndert, mit der die TSC aktualisiert wird. Gleichzeitig bietet das GerĂ€t die Möglichkeit, die aktuelle Frequenz herauszufinden. Alternativ kann die TSC-Aktualisierungsrate unter die Kontrolle des Betriebssystems gestellt werden (siehe âPower ISA Version 2.06 Revision B, Buch II, Kapitel 5â).
- Zusammen mit dem TSC-Wert kann das GerĂ€t auch die ID der CPU bereitstellen, auf der dieser Wert gelesen wird (siehe Intel RDTSCP-Anweisung, âEntwicklerhandbuch fĂŒr Intel 64- und IA-32-Architekturen-Softwareâ, Band 2).
- Auf einigen Systemen können Sie den TSC-Wert fĂŒr jede CPU programmgesteuert anpassen (siehe Intel WRMSR-Anweisung und Register IA32_TIME_STAMP_COUNTER, âEntwicklerhandbuch fĂŒr Intel 64- und IA-32-Architekturen-Softwareâ, Band 3).
Im Allgemeinen ist das Thema, wie Zeitmesser auf verschiedenen Architekturen implementiert werden, faszinierend und umfangreich. Wenn Sie Zeit und Interesse haben, empfehle ich Tauchen. Unter anderem erfahren Sie beispielsweise, dass Sie mit einigen Systemen programmgesteuert bestimmen können, ob TSC als zuverlÀssige Zeitquelle dienen kann.
Es gibt also viele architektonische Implementierungen von TSC, von denen jede ihre eigenen Eigenschaften hat. Es ist jedoch interessant, dass sich in diesem ganzen Zoo ein allgemeiner Trend etabliert hat.
Moderne Hardware und Betriebssysteme bemĂŒhen sich darum, dass :
- TSC tickt auf jeder CPU im System mit der gleichen Frequenz
- Diese Frequenz Àndert sich zeitlich nicht
- Es gibt keine Verschiebung zwischen TSCs, die auf verschiedenen CPUs ticken
Beim Entwerfen meiner Bibliothek habe ich mich entschieden, von dieser PrÀmisse auszugehen und nicht von der Vinaigrette der Hardware-Implementierungen.
Die Bibliothek
Ich habe nicht angefangen, auf Hardware-Chips einer Reihe verschiedener Architekturen zu legen. Stattdessen entschied ich mich fĂŒr eine trendorientierte Bibliothek. Sie hat einen rein empirischen Fokus:
- Damit können Sie die ZuverlĂ€ssigkeit von TSC als Zeitquelle experimentell ĂŒberprĂŒfen
- AuĂerdem können Sie die Parameter experimentell berechnen, die fĂŒr die schnelle Umwandlung von Zecken in Nanosekunden erforderlich sind
- NatĂŒrlich bietet die Bibliothek praktische Schnittstellen zum Lesen von TSC und zum Konvertieren von Zecken in Nanosekunden "on the fly".
Den Bibliothekscode finden Sie hier. Es wird nur unter Linux kompiliert und ausgefĂŒhrt.
Im Code sehen Sie die Details der Implementierung aller Methoden, die spÀter erlÀutert werden.
TSC-ZuverlÀssigkeitsbewertung
Die Bibliothek bietet eine Schnittstelle, die zwei Bewertungen zurĂŒckgibt:
- maximale Verschiebung zwischen ZĂ€hlern, die zu verschiedenen CPUs gehören. Es werden nur CPUs berĂŒcksichtigt, die dem Prozess zur VerfĂŒgung stehen. Wenn fĂŒr einen Prozess beispielsweise drei CPUs verfĂŒgbar sind und die TSC auf diesen CPUs gleichzeitig 50, 150, 20 betrĂ€gt, betrĂ€gt die maximale Verschiebung 150-20 = 130. NatĂŒrlich wird die Bibliothek nicht in der Lage sein, experimentell eine echte maximale Verschiebung zu erhalten, aber sie wird eine SchĂ€tzung geben, in die diese Verschiebung passt. Was tun als nĂ€chstes mit der Bewertung? Wie benutzt man? Dies löst bereits den Client-Code. Die Bedeutung ist jedoch ungefĂ€hr die folgende. Die maximale Verschiebung ist der maximale Wert, um den die Dimension, die der Clientcode erstellt, verzerrt werden kann. Angenommen, in unserem Beispiel mit drei CPUs begann der Client-Code, die Zeit auf CPU3 (wo TSC 20 war) zu messen, und endete auf CPU2 (wo TSC 150 war). Es stellt sich heraus, dass sich zusĂ€tzliche 130 Ticks in das gemessene Intervall einschleichen. Und nie wieder. Der Unterschied zwischen CPU1 und CPU2 wĂŒrde nur 100 Ticks betragen. Mit einer SchĂ€tzung von 130 Ticks (in der Tat wird es viel konservativer sein) kann der Kunde entscheiden, ob ein solcher Verzerrungswert zu ihm passt oder nicht
- Erhöhen sich die TSC-Werte, die nacheinander auf derselben oder verschiedenen CPUs gemessen werden? Hier ist die Idee. Nehmen wir an, wir haben mehrere CPUs. Angenommen, ihre Uhr ist synchronisiert und tickt mit derselben Frequenz. Wenn Sie dann zuerst die Zeit auf einer CPU messen und dann erneut messen - bereits auf einer der verfĂŒgbaren CPUs -, sollte die zweite Ziffer gröĂer sein als die erste.
Ich werde diese SchÀtzung unterhalb der TSC-Monotonie-SchÀtzung nennen
Nun wollen wir sehen, wie wir die erste SchÀtzung erhalten können:
- Einer der fĂŒr den Prozess verfĂŒgbaren Prozessoren wird als "grundlegend" deklariert.
- dann werden alle anderen CPUs sortiert und fĂŒr jede von ihnen wird die Verschiebung berechnet:
TSC___CPU â TSC___CPU
. Dies geschieht wie folgt:
- a) Drei Messwerte werden nacheinander (nacheinander!)
TSC_base_1, TSC_current, TSC_base_2
: TSC_base_1, TSC_current, TSC_base_2
. Hier zeigt der Strom an, dass der Wert auf der aktuellen CPU und die Basis auf der Basis gemessen wurde - b) Die Verschiebung
TSC___CPU â TSC___CPU
muss im Intervall [TSC_current â TSC_base_2, TSC_current â TSC_base_1]
. Dies setzt voraus, dass TSCs auf beiden CPUs mit der gleichen Frequenz ticken. - c) Die Schritte a) -b) werden mehrmals wiederholt. Der Schnittpunkt aller in Schritt b) erhaltenen Intervalle wird berechnet. Das resultierende Intervall wird als SchÀtzung der Verschiebung
TSC___CPU â TSC___CPU
- Nachdem fĂŒr jede CPU eine VerschiebungsschĂ€tzung relativ zur Basis erhalten wurde, ist es einfach, eine SchĂ€tzung der maximalen Verschiebung zwischen allen verfĂŒgbaren CPUs zu erhalten:
- a) Das Mindestintervall wird berechnet, das alle in Schritt 2 erhaltenen resultierenden Intervalle enthÀlt
- b) Die Breite dieses Intervalls wird als SchÀtzung der maximalen Verschiebung zwischen TSCs verwendet, die auf verschiedenen CPUs ticken
Um die Monotonie in der Bibliothek zu bewerten, wird der folgende Algorithmus implementiert:
- Angenommen, ein Prozess verfĂŒgt ĂŒber N CPU
- TSC auf CPU1 messen
- TSC auf CPU2 messen
- ...
- TSC auf CPUN messen
- TSC erneut an CPU1 messen
- Wir ĂŒberprĂŒfen, ob die gemessenen Werte vom ersten bis zum letzten monoton ansteigen
Hierbei ist es wichtig, dass der erste und der letzte Wert auf derselben CPU gemessen werden. Und hier ist warum. Nehmen wir an, wir haben 3 CPUs. Angenommen, die TSC auf CPU2 ist gegenĂŒber der TSC auf CPU1 um +100 Ticks verschoben. Angenommen, die TSC auf CPU3 ist gegenĂŒber der TSC auf CPU2 um +100 Ticks verschoben. Betrachten Sie die folgende Ereigniskette:
- Lesen Sie die TSC auf CPU1. Es sei ein Wert von 10 erhalten
- 2 Zecken bestanden
- Lesen Sie TSC auf CPU2. Muss 112 sein
- 2 Zecken bestanden
- Lesen Sie TSC auf CPU3. Muss 214 sein
Bisher sieht die Uhr synchronisiert aus. Aber lassen Sie uns noch einmal TSC auf CPU1 messen:
- 2 Zecken bestanden
- Lesen Sie die TSC auf CPU1. Muss 16 sein
Ups! Monotonie ist gebrochen. Es stellt sich heraus, dass Sie durch Messen des ersten und letzten Werts auf derselben CPU mehr oder weniger groĂe Verschiebungen zwischen den Uhren erkennen können. Die nĂ€chste Frage natĂŒrlich: "Wie groĂ sind die Schichten?" Das AusmaĂ der Verschiebung, das erkannt werden kann, hĂ€ngt von der Zeit ab, die zwischen aufeinanderfolgenden TSC-Messungen vergeht. Im angegebenen Beispiel sind dies nur 2 Ticks. Verschiebungen zwischen Stunden, die 2 Ticks ĂŒberschreiten, werden erkannt. Im Allgemeinen werden Verschiebungen, die kĂŒrzer als die zwischen aufeinanderfolgenden Messungen verstrichene Zeit sind, nicht erkannt. Je dichter die Messungen in der Zeit sind, desto besser. Die Genauigkeit beider SchĂ€tzungen hĂ€ngt davon ab. Je dichter die Messungen sind:
- Je niedriger die maximale SchichtschÀtzung
- desto mehr Vertrauen in die Beurteilung der Monotonie
Im nĂ€chsten Abschnitt werden wir darĂŒber sprechen, wie man enge Messungen vornimmt. Ich möchte hier hinzufĂŒgen, dass die Bibliothek bei der Berechnung der TSC-ZuverlĂ€ssigkeitsschĂ€tzungen viel mehr einfache "LĂ€use" -PrĂŒfungen durchfĂŒhrt, zum Beispiel:
- eingeschrĂ€nkte ĂberprĂŒfung, ob TSCs auf verschiedenen CPUs mit derselben Geschwindigkeit ticken
- ĂberprĂŒfen Sie, ob sich die ZĂ€hler im Laufe der Zeit wirklich Ă€ndern und nicht nur den gleichen Wert anzeigen
Zwei Methoden zum Sammeln von ZĂ€hlerwerten
In der Bibliothek habe ich zwei Methoden zum Sammeln von TSC-Werten implementiert:
- Zwischen CPU wechseln . Bei dieser Methode werden alle Daten, die zur Bewertung der ZuverlĂ€ssigkeit der TSC erforderlich sind, von einem einzelnen Thread erfasst, der von einer CPU zur anderen springt. Beide im vorherigen Abschnitt beschriebenen Algorithmen sind fĂŒr diese Methode geeignet und fĂŒr die andere nicht.
"Umschalten zwischen der CPU" hat keinen praktischen Nutzen. Die Methode wurde nur implementiert, um "herumzuspielen". Das Problem bei der Methode ist, dass die Zeit, die erforderlich ist, um einen Stream von einer CPU auf eine andere zu ziehen, sehr groĂ ist. Dementsprechend vergeht zwischen aufeinanderfolgenden TSC-Messungen viel Zeit, und die Genauigkeit der SchĂ€tzungen ist sehr gering. Beispielsweise wird eine typische SchĂ€tzung fĂŒr die maximale Verschiebung zwischen TSC im Bereich von 23.000 Zecken erhalten.
Das Verfahren hat jedoch einige Vorteile:
- es ist absolut deterministisch. Wenn Sie TSC nacheinander an CPU1, CPU2, CPU3 messen mĂŒssen, nehmen wir es einfach und tun es: Wechseln Sie zu CPU1, lesen Sie TSC, wechseln Sie zu CPU2, lesen Sie TSC und wechseln Sie schlieĂlich zu CPU3, lesen Sie TSC
- Wenn die Anzahl der CPUs im System sehr schnell zunimmt, sollte die Umschaltzeit zwischen ihnen vermutlich viel langsamer ansteigen. Daher kann theoretisch anscheinend ein System existieren - ein sehr groĂes System! - in denen die Anwendung der Methode gerechtfertigt ist. Dies ist jedoch unwahrscheinlich
- Messungen mit CAS bestellt . Bei dieser Methode werden Daten parallel von mehreren Threads erfasst. Jede verfĂŒgbare CPU startet einen Thread. Messungen, die von verschiedenen Threads durchgefĂŒhrt werden, werden in einer einzigen Sequenz unter Verwendung der Operation "Vergleichen und Tauschen" angeordnet. Unten finden Sie einen Code, der zeigt, wie dies gemacht wird.
Die Idee der Methode stammt von fio , einem beliebten Tool zur Erzeugung von E / A-Lasten.
Die ZuverlĂ€ssigkeitsschĂ€tzungen, die mit der Leistung dieser Methode erhalten wurden, sehen bereits ziemlich gut aus. Beispielsweise wird bereits eine SchĂ€tzung der maximalen Verschiebung auf der Ebene von mehreren hundert Ticks erhalten. Eine ĂberprĂŒfung der Monotonie ermöglicht es Ihnen, die Uhr innerhalb von Hunderten von Ticks nicht synchron zu erfassen.
Die im vorherigen Abschnitt angegebenen Algorithmen sind jedoch fĂŒr diese Methode nicht geeignet. FĂŒr sie ist es wichtig, dass die TSC-Werte in einer vorgegebenen Reihenfolge gemessen werden. Die Methode der von CAS bestellten Messungen erlaubt dies nicht. Stattdessen wird zuerst eine lange Folge von Zufallsmessungen gesammelt, und dann versuchen (bereits unterschiedliche) Algorithmen, Werte zu finden, die auf âgeeignetenâ CPUs in dieser Folge gelesen werden.
Ich werde diese Algorithmen hier nicht angeben, um Ihre Aufmerksamkeit nicht zu missbrauchen. Sie können sie im Code sehen. Es gibt viele Kommentare. Theoretisch sind diese Algorithmen gleich. Ein grundlegend neuer Punkt ist die ĂberprĂŒfung, wie zufĂ€llig typisierte TSC-Sequenzen statistisch âqualitativâ sind. Es ist auch möglich, ein akzeptables MindestmaĂ an statistischer Signifikanz fĂŒr TSC-ZuverlĂ€ssigkeitsschĂ€tzungen festzulegen.
Theoretisch kann bei SEHR groĂen Systemen die Methode âCAS-geordnetâ zu schlechten Ergebnissen fĂŒhren. Das Verfahren erfordert, dass Prozessoren um den Zugriff auf einen gemeinsamen Speicherort konkurrieren. Wenn es viele Prozessoren gibt, kann sich der Wettbewerb als sehr intensiv herausstellen. Infolgedessen wird es schwierig sein, eine Messsequenz mit guten statistischen Eigenschaften zu erstellen. Im Moment scheint diese Situation jedoch unwahrscheinlich.
Ich habe Code versprochen. So sieht es aus, wenn Sie mit CAS Messungen in einer einzigen Kette erstellen.
for ( uint64_t i = 0; i < arg->probes_count; i++ ) { uint64_t seq_num = 0; uint64_t tsc_val = 0; do { __atomic_load( seq_counter, &seq_num, __ATOMIC_ACQUIRE); __sync_synchronize(); tsc_val = WTMLIB_GET_TSC(); } while ( !__atomic_compare_exchange_n( seq_counter, &seq_num, seq_num + 1, false, __ATOMIC_ACQ_REL, __ATOMIC_RELAXED)); arg->tsc_probes[i].seq_num = seq_num; arg->tsc_probes[i].tsc_val = tsc_val; }
Dieser Code wird auf jeder verfĂŒgbaren CPU ausgefĂŒhrt. Alle Threads haben Zugriff auf die gemeinsam genutzte Variable
seq_counter
. Vor dem Lesen der TSC liest der Stream den Wert dieser Variablen und speichert ihn in der Variablen
seq_num
. Dann liest TSC. Dann wird versucht, seq_counter atomar um eins zu erhöhen, jedoch nur, wenn sich der Wert der Variablen seit dem Lesen nicht geÀndert hat. Wenn die Operation erfolgreich ist, bedeutet dies, dass es dem Thread gelungen ist, die in
seq_num
hinter dem gemessenen TSC-Wert gespeicherte
seq_num
. Die nÀchste Seriennummer, die (möglicherweise bereits in einem anderen Thread) eingesetzt werden kann, ist eine weitere. Diese Nummer wird der Variablen
seq_counter
entnommen, und jeder erfolgreiche Aufruf von
__atomic_compare_exchange_n()
erhöht diese Variable um eins.
__atomic mit __sync ???Aus GrĂŒnden der __atomic
sollte beachtet werden, dass die Verwendung der integrierten Funktionen der __atomic
Familie zusammen mit einer Funktion aus der veralteten __sync
Familie hÀsslich aussieht. __sync_synchronize()
im Code verwendet, um zu vermeiden, dass die TSC-Leseoperation mit der Upstream-Operation neu angeordnet wird. Dies erfordert eine vollstÀndige Speicherbarriere. Die __atomic
Familie hat formal keine Funktion mit den entsprechenden Eigenschaften. Obwohl es tatsÀchlich gibt: __atomic_signal_fence()
. Diese Funktion organisiert Stream-Berechnungen mit Signalhandlern, die auf demselben Stream ausgefĂŒhrt werden. In der Tat ist dies eine vollstĂ€ndige Barriere. Dies wird jedoch nicht ausdrĂŒcklich angegeben. Und ich bevorzuge Code ohne versteckte Semantik. Daher ist __sync_synchronize()
eine Stop-Full-Speicherbarriere.
Ein weiterer erwĂ€hnenswerter Punkt ist die Sorgfalt, dass alle an Messungen beteiligten FlĂŒsse mehr oder weniger gleichzeitig beginnen. Wir sind daran interessiert, dass die auf verschiedenen CPUs gelesenen TSC-Werte so gemischt wie möglich sind. Wir sind nicht zufrieden mit der Situation, in der beispielsweise ein Thread zuerst startet, seine Arbeit beendet und erst dann alle anderen beginnen. Die resultierende TSC-Sequenz hat nutzlose Eigenschaften. Daraus können keine SchĂ€tzungen gezogen werden. Der gleichzeitige Start aller Threads ist wichtig - und dafĂŒr wurden MaĂnahmen in der Bibliothek ergriffen.
Konvertieren Sie Zecken im laufenden Betrieb in Nanosekunden
Nach ĂberprĂŒfung der ZuverlĂ€ssigkeit von TSC besteht der zweite Hauptzweck der Bibliothek darin, Zecken im laufenden Betrieb in Nanosekunden umzuwandeln. Die Idee dieser Umstellung habe ich dem bereits erwĂ€hnten Fio entlehnt. Ich musste jedoch einige signifikante Verbesserungen vornehmen, da das Konvertierungsverfahren, wie meine Analyse gezeigt hat, in fio selbst nicht gut genug funktioniert. Dort erhalten Sie eine geringe Genauigkeit.
Ich beginne mit einem Beispiel.
Idealerweise möchte ich Zecken wie folgt in Nanosekunden umwandeln:
ns_time = tsc_ticks / tsc_per_ns
Wir möchten, dass der Zeitaufwand fĂŒr die Konvertierung minimal ist. Daher möchten wir ausschlieĂlich ganzzahlige Arithmetik verwenden. Mal sehen, wie uns das bedrohen kann.
Wenn
tsc_per_ns = 3
,
tsc_per_ns = 3
eine einfache Ganzzahldivision unter dem Gesichtspunkt der Genauigkeit
ns_time = tsc_ticks / 3
:
ns_time = tsc_ticks / 3
.
Aber was ist, wenn
tsc_per_ns = 3.333
?
Wenn diese Zahl auf 3 gerundet wird, ist die Konvertierungsgenauigkeit sehr gering. Zur Lösung dieses Problems wie folgt:ns_time = (tsc_ticks * factor) / (3.333 * factor)
.Wenn der Faktor factor
groĂ genug ist, ist die Genauigkeit gut. Aber etwas wird schlecht bleiben. Konvertierungsaufwand. Die Ganzzahldivision ist eine sehr teure Operation. Auf x86 sind beispielsweise mehr als 10 Taktzyklen erforderlich. AuĂerdem werden Operationen zur Ganzzahldivision nicht immer per Pipeline ausgefĂŒhrt.Wir schreiben unsere Formel in der entsprechenden Formns_time = (tsc_ticks * factor / 3.333) / factor
.Die erste Division ist kein Problem. Wir können (factor / 3.333)
im Voraus vorberechnen . Aber die zweite Liga ist immer noch Schmerz. Um sie loszuwerden, wÀhlen wirfactor
gleich der Potenz von zwei. Danach kann die zweite Abteilung durch eine Bitverschiebung ersetzt werden - eine einfache und schnelle Operation.Wie groà kannst du wÀhlen factor
? Leider kann factor
es nicht beliebig groĂ sein. Es ist durch die Bedingung begrenzt, dass die Multiplikation im ZĂ€hler nicht zu einem Ăberlauf des 64-Bit-Typs fĂŒhren darf. Ja, wir möchten nur "native" Typen verwenden. Auch hier, um den Conversion-Overhead auf ein Minimum zu beschrĂ€nken.Nun wollen wir sehen, wie groĂ es factor
in unserem speziellen Beispiel sein kann. Angenommen, wir möchten mit Zeitintervallen von bis zu einem Jahr arbeiten. Im Laufe des Jahres tiknet TSC mal folgenden: 3.333 * 1000000000 * 60 * 60 * 24 * 365 = 105109488000000000
. Unterteile ein Maximalwert von 64-Bit - Typennummer ist: 18446744073709551615 / 105109488000000000 ~ 175.5
. Also der Ausdruck(factor / 3.333)
sollte nicht gröĂer als dieser Wert sein. Dann haben wir factor <= 175.5 * 3.333 ~ 584.9
. Die gröĂte Zweierpotenz, die diese Zahl nicht ĂŒberschreitet, ist 512. Daher hat unsere Umrechnungsformel die Form:ns_time = (tsc_ticks * 512 / 3.333) / 512
Oder:ns_time = tsc_ticks * 153 / 512
Fein. Lassen Sie uns nun mit Genauigkeit sehen, was diese Formel hat. Ein Jahr enthÀlt 1000000000 * 60 * 60 * 24 * 365 = 31536000000000000
Nanosekunden. Unsere Formel ergibt: 105109488000000000 * 153 / 512 = 31409671218750000
. Die Differenz zum Barwert betrÀgt 126328781250000 Nanosekunden oder 126328781250000 / 1000000000 / 60 / 60 ~ 35
Stunden.Das ist ein groĂer Fehler. Wir wollen eine bessere Genauigkeit. Was ist, wenn wir Zeitintervalle von nicht mehr als einer Stunde messen? Ich werde die Berechnungen weglassen. Sie sind völlig identisch mit denen, die gerade gemacht wurden. Die endgĂŒltige Formel lautet:ns_time = tsc_ticks * 1258417 / 4194304
(1)Der Konvertierungsfehler betrÀgt nur 119.305 Nanosekunden pro 1 Stunde (was weniger als 0,2 Millisekunden entspricht). Sehr sehr gut. Wenn der maximale konvertierbare Wert noch weniger als eine Stunde betrÀgt, ist die Genauigkeit sogar noch besser. Aber wie nutzen wir das? Zeitmessungen nicht auf eine Stunde beschrÀnken?Achten Sie auf den folgenden Punkt:tsc_ticks = (tsc_ticks_per_1_hour * number_of_hours) + tsc_ticks_remainder
Wenn wir Vorkalkulation tsc_ticks_per_1_hour
, können wir extrahieren number_of_hours
aus tsc_ticks
. Als nĂ€chstes wissen wir, wie viele Nanosekunden in einer Stunde enthalten sind. Daher wird es fĂŒr uns nicht schwierig sein, den Teil tsc_ticks
, der einer ganzen Anzahl von Stunden entspricht, in Nanosekunden zu ĂŒbersetzen . Um die Konvertierung abzuschlieĂen, mĂŒssen wir in Nanosekunden ĂŒbersetzentsc_ticks_remainder
. Wir wissen jedoch, dass diese Anzahl von Zecken in weniger als einer Stunde auftrat. Um es in Nanosekunden umzuwandeln, können wir die Formel (1) verwenden.Fertig. Ein solcher Umwandlungsmechanismus passt zu uns. Lassen Sie es uns jetzt zusammenfassen und optimieren.ZunĂ€chst möchten wir eine flexible Kontrolle ĂŒber Konvertierungsfehler haben. Wir möchten die Konvertierungsparameter nicht an ein Zeitintervall von 1 Stunde binden. Es sei ein beliebiges Zeitintervall: Denken Sie nochtsc_ticks = modulus * number_of_moduli_periods + tsc_ticks_remainder
einmal daran, wie der Rest in Nanosekunden konvertiert wird:ns_per_remainder = (tsc_ticks_remainder * factor / tsc_per_nsec) / factor
Wir berechnen die Konvertierungsparameter (das wissen wir tsc_ticks_remainder < modulus
):modulus * (factor / tsc_per_nsec) <= UINT64_MAX
factor <= (UINT64_MAX / modulus) * tsc_per_nsec
2 ^ shift <= (UINT64_MAX / modulus) * tsc_per_nsec
Aus GrĂŒnden der Langeweile ist anzumerken, dass die letzte Ungleichung im Rahmen der Ganzzahlarithmetik nicht der ersten entspricht. Aber ich werde nicht lange darauf eingehen. Ich kann nur sagen, dass die letzte Ungleichung schwerwiegender ist als die erste und daher sicher zu verwenden ist.Nachdem wir aus der letzten Ungleichung erhalten haben shift
, berechnen wir: Und dann werden diese Parameter verwendet, um den Rest in Nanosekunden umzuwandeln: Also haben wir die Restumrechnung herausgefunden. Das nÀchste zu lösende Problem - ist die Entfernung und aus . Wie immer wollen wir es schnell machen. Wie immer wollen wir keine Division verwenden. Deshalb wÀhlen wir einfach gleich der Zweierpotenz: Dann:factor = 2 ^ shift
mult = factor / tsc_per_nsec
ns_per_remainder = (tsc_ticks_remainder * mult) >> shift
tsc_ticks_remainder
number_of_moduli_periods
tsc_ticks
modulus
modulus = 2 ^ remainder_bit_length
number_of_moduli_periods = tsc_ticks >> remainder_bit_length
tsc_ticks_remainder = tsc_ticks & (modulus - 1)
GroĂartig.
Wir wissen jetzt, wie man aus tsc_ticks
number_of_moduli_periods
und extrahiert tsc_ticks_remainder
. Und wir wissen, wie man tsc_ticks_remainder
in Nanosekunden umwandelt . Es bleibt zu verstehen, wie dieser Teil der Zecken, der ein Vielfaches ist, in Nanosekunden umgewandelt wird modulus
. Aber alles ist einfach:ns_per_moduli = ns_per_modulus * number_of_moduli_periods
ns_per_modulus
Sie können im Voraus rechnen. DarĂŒber hinaus nach der gleichen Formel, nach der wir den Rest umrechnen. Diese Formel kann fĂŒr ZeitrĂ€ume verwendet werden, die nicht lĂ€nger als sind modulus
. Er selbst modulus
natĂŒrlich nicht lĂ€nger als modulus
.ns_per_modulus = (modulus * mult) >> shift
Das ist alles! Wir konnten alle Parameter berechnen, die erforderlich sind, um Zecken im laufenden Betrieb in Nanosekunden umzuwandeln. Fassen Sie nun das Konvertierungsverfahren kurz zusammen:- wir haben
tsc_ticks
number_of_moduli_periods = tsc_ticks >> remainder_bit_length
tsc_ticks_remainder = tsc_ticks & (modulus - 1)
ns = ns_per_modulus * number_of_moduli_periods + (tsc_ticks_remainder * mult) >> shift
Bei diesem Verfahren werden Parameter remainder_bit_length
, modulus, ns_per_modulus
, mult
und shift
Vorkalkulation voraus.Wenn Sie diesen Beitrag noch lesen, sind Sie groĂartig oder groĂartig. Es ist sogar möglich, dass Sie ein Performance-Analyst oder Entwickler von Hochleistungssoftware sind.Also.
Es stellt sich heraus, dass wir noch nicht fertig sind :)Erinnerst du dich, wie wir den Parameter berechnet haben mult
? Es war so:mult = factor / tsc_per_nsec
Frage: Woher kommt es tsc_per_nsec
?Die Anzahl der Zecken in einer Nanosekunde ist ein sehr kleiner Wert. TatsÀchlich tsc_per_nsec
wird stattdessen meine Bibliothek verwendet (tsc_per_sec / 1000000000)
. Das heiĂt:
mult = factor * 1000000000 / tsc_per_sec
Und es gibt zwei interessante Fragen:- Warum
tsc_per_sec
und nicht tsc_per_msec
zum Beispiel? - Woher bekommen diese
tsc_per_sec
?
Beginnen wir mit dem ersten. Fio verwendet jetzt tatsÀchlich die Anzahl der Ticks pro Millisekunde. Und damit gibt es Probleme. Auf der Maschine, die Parameter , von denen ich oben genannt tsc_per_msec = 2599998
. WĂ€hrend tsc_per_sec = 2599998971
. Wenn wir diese Zahlen auf eine Skala bringen, liegt ihr VerhĂ€ltnis sehr nahe bei Eins: 0,999999626. Wenn wir jedoch die erste und nicht die zweite verwenden, haben wir fĂŒr jede Sekunde einen Fehler von 374 Nanosekunden. Deshalb - tsc_per_sec
.Weiter ... Wie zÀhlt man tsc_per_sec
?Dies erfolgt auf der Grundlage einer direkten Messung: âeinige Zeitâ ist ein konfigurierbarer Parameter. Sie kann gröĂer, kleiner oder gleich einer Sekunde sein. Nehmen wir an, es ist eine halbe Sekunde. Nehmen wir weiter an, dass der tatsĂ€chliche Unterschied zwischen und 0,6 Sekunden betrug. Dann .start_sytem_time = clock_gettime()
start_tsc = WTMLIB_GET_TSC()
-
end_system_time = clock_gettime()
end_tsc = WTMLIB_GET_TSC()
end_system_time
start_system_time
tsc_per_sec = (end_tsc â start_tsc) / 0,6
Die Bibliothek berĂŒcksichtigt auf diese Weise mehrere Werte tsc_per_sec
. Mit Standardmethoden werden sie dann von statistischem Rauschen âgereinigtâ und erhalten einen einzigen tsc_per_sec
vertrauenswĂŒrdigen Wert .Im obigen Zeitmessdiagramm ist die Reihenfolge der Anrufe clock_gettime()
und wichtig WTMLIB_GET_TSC()
. Es ist wichtig, WTMLIB_GET_TSC()
dass zwischen zwei Anrufen dieselbe Zeit vergeht wie zwischen zwei Anrufen clock_gettime()
. Dann ist es möglich, die Systemzeit leicht mit TSC-Ticks zu korrelieren. Und dann kann die Streuung der Werte tsc_per_sec
wirklich als zufÀllig angesehen werden. Bei diesem Messschema tsc_per_sec
weichen die Werte mit der gleichen Wahrscheinlichkeit in beide Richtungen vom Durchschnittswert ab. Und es wird möglich sein, Standardfiltermethoden auf sie anzuwenden.Fazit
Vielleicht ist das alles.Das Thema der effektiven Zeitmessung ist jedoch nicht darauf beschrĂ€nkt. Es gibt viele Nuancen. FĂŒr Interessierte schlage ich vor, unabhĂ€ngig an folgenden Themen zu arbeiten:- Speichern von Konvertierungsparametern im Cache oder - noch besser - in Registern
- bis zu welchen Grenzen kann reduziert werden
modulus
(wodurch die Genauigkeit der Konvertierung erhöht wird)? - Wie wir gesehen haben, wird die Konvertierungsgenauigkeit nicht nur beeinflusst
modulus
, sondern auch durch die GröĂe des Zeitintervalls, das mit Ticks ( tsc_per_msec
oder tsc_per_sec
) korreliert . Wie kann der Einfluss beider Faktoren ausgeglichen werden? - TSC in der virtuellen Maschine. Kann ich es benutzen?
- . , fio timespec. :
tp->tv_sec = nsecs / 1000000000ULL;
, TSC . , ,
Mit den in diesem Artikel beschriebenen Methoden können wir die Zeitskala einer Sekunde mit einer Genauigkeit in der GröĂenordnung von mehreren zehn Nanosekunden messen. Dies ist die Genauigkeit, die ich bei der Verwendung meiner Bibliothek tatsĂ€chlich beobachte.Interessanterweise verliert das Fio, von dem ich einige Methoden ausgeliehen habe, auf einer zweiten Skala genau 700-900 Nanosekunden (und dafĂŒr gibt es drei GrĂŒnde). AuĂerdem verliert die Konvertierungsgeschwindigkeit aufgrund der Speicherung von Zeit in einem Standard-Linux-Format. Ich beeile mich jedoch, Fio-Fans zu beruhigen. Ich habe Entwicklern eine Beschreibung aller Konvertierungsprobleme gesendet , die ich entdeckt habe . Die Leute arbeiten bereits, sie werden es bald beheben.Ich wĂŒnsche Ihnen viele angenehme Nanosekunden!