Azure Native Qumulo jetzt in der EU, im Vereinigten Königreich und in Kanada verfügbar – Erfahren Sie mehr

IOPS-Leistungsverbesserung und Sperreffizienz

Geschrieben von:

Heute möchte ich über die Arbeit eines bestimmten Tages sprechen, die ich interessant fand.

Unser Team war kürzlich mitten in der Untersuchung Leistung lesen für kleine, aber zahlreiche Anfragen, die sich als CPU-gebunden herausgestellt hatten. Ungefähr die Hälfte unserer CPU-Zeit wurde damit verbracht, um Sperren zu kämpfen, und wir hatten einigen Erfolg, als wir die Liste der Top-Anwärter abgearbeitet haben, um mehr herauszuholen IOPS. Angesichts der Tatsache, dass wir bereits in mehreren Bereichen daran gearbeitet hatten, Sperrkonflikte zu reduzieren, begann ich mich zu fragen, wie viel Einfluss unsere Sperren selbst auf die Leistung hatten.

Ich habe mir etwas Zeit genommen, um uns unsere am häufigsten verwendete Sperrimplementierung anzusehen. Meine Idee war zu sehen, ob es Möglichkeiten gibt, alle unsere umkämpften Sperren ein wenig schneller zu machen. Ich habe zwei Implementierungen entdeckt. Ein „Spinlock“ war ein Wrapper für pthread_mutex, und der andere, weniger häufig verwendete war ein Wrapper für futex. Ein pthread_mutex ist eine massive 40-Byte-Struktur, die von der pthreads-Bibliothek bereitgestellt wird, während ein futex 4 Bytes umfasst. Ich beschloss, einige Daten über ihre Leistung zu erhalten, aber ich begann bereits, die Idee zu mögen, die futex-basierte Implementierung zu optimieren und standardisierter zu machen.

Bevor ich loslief und größere Änderungen vornahm, beschloss ich, beide Sperren zu testen. Meiner Erfahrung nach waren die meisten Sperren, die Leistungsprobleme verursachten, stark umkämpft. Ich habe einen Microbenchmark geschrieben, der vier Threads hochfährt, wobei jeder Thread eine Sperre erwirbt, eine Ganzzahl erhöht und die Sperre freigibt. Ich erkenne auch an, dass jeder Overhead in der unangefochtenen Erfassungsgeschwindigkeit im Produkt wahrscheinlich ziemlich schwer zu erkennen wäre, also habe ich auch eine Single-Thread-Variante geschrieben. Ich ließ jeden von ihnen 30 Sekunden lang laufen, bevor ich den Zähler überprüfte. Hier die ersten Ergebnisse:

Unbestritten Umstritten (4 Threads)
Pthread-Mutex ~47 mA/s* ~13 mA/s*
Futex ~53 mA/s* ~8.4 mA/s*

* Millionen Akquisitionen/Sekunde

Ich habe diese Ergebnisse mit einem Schnelltest am tatsächlichen Produkt validiert, bei dem ich die Pthreads-Implementierung gegen die Futex-Implementierung ausgetauscht habe. Tatsächlich hat die Futex-Implementierung die Leistung zurückgefahren. Dies ist ein frustrierendes Ergebnis, da ich zufällig weiß, dass Pthread Mutex für diese Plattform auf Futex implementiert ist. Ich habe mir die Futex-Implementierung genauer angeschaut. Hier sind die wichtigen Teile im Pseudocode. Ich vereinfache absichtlich die Details der Funktionsweise von Futexen, da dies für diese Geschichte weitgehend irrelevant ist.

bool trylock(futex_lock *self) { return !atomic_bool_exchange(&self->is_locked, true); } void lock(futex_lock *self) { for (int i = 0; i < SOME_NUMBER; ++i) {if (trylock(self)) return; pause() // Hinweis an die CPU, dass wir uns in einer Spinlock-Schleife befinden } while (true) { futex_sleep(&self->is_locked); if (trylock(self)) return; } } void unlock(futex_lock *self) { atomic_bool_set(&self->is_locked, false); futex_wake_all(&self->is_locked); }

Das Naheliegendste, was ich hier ändern sollte, war SOME_NUMBER, vielleicht gefolgt von einer Pause. Nach ein paar Versuchen mit verschiedenen Zahlen und einigen verschiedenen Ideen, wie man pausieren kann, entdeckte ich, dass es am effizientesten war, überhaupt nicht zu drehen. Das neue Schloss sah ungefähr so ​​aus.

void lock(futex_lock *self) { while (true) { if (trylock(self)) return; futex_sleep(&self->is_locked); } }
1 Thema 4-Threads
Pthread-Mutex ~47 mA/s* ~13 mA/s*
Futex ~53 mA/s* ~8.4 mA/s*
Futex (ohne Schleudern) ~53 mA/s* ~17 mA/s*

* Millionen Akquisitionen/Sekunde

Dies mag überraschen, da das Schlafen auf einem Futex ein Syscall und daher ziemlich teuer ist. Etwas anderes Teures muss passieren, während wir drehen. Auf Intel-CPUs gibt es drei Zustände für Cache-Zeilen. „I“ bedeutet, dass dieser Kern exklusiven Zugriff hat (sicher zu schreiben). „S“ bedeutet, dass die Cache-Zeile sauber ist, sich aber möglicherweise auch im Cache eines anderen Kerns befindet (sicher zu lesen). „A“ bedeutet, dass ein anderer Kern möglicherweise exklusiven Zugriff auf die Cache-Zeile hat (unsicher, ohne zu fragen). Wenn mehrere Kerne atomar auf Daten in derselben Cache-Zeile zugreifen, tauschen sie aus, welcher Kern exklusiven Zugriff erhält, und die Daten ping-pongs zwischen I- und A-Zuständen. Dies ist eine kostspielige Operation, die möglicherweise das Rundsenden von Nachrichten zwischen CPU-Sockeln beinhaltet.

Vor diesem Hintergrund macht unsere Performance-Steigerung sehr viel Sinn. Es eröffnet auch einige weitere Möglichkeiten zur Optimierung. futex_wake_all scheint hier ein besonders wichtiges Problem zu sein. Für den Fall, dass es mehr als einen Kellner gibt, wird es nur einem gelingen, das Schloss zu bekommen, während alle um den exklusiven Zugriff auf die Cache-Line kämpfen. Nach einigem Fummeln kam ich auf etwas wie das folgende.

bool trylock(futex_lock *self) {auto old_state = atomic_uint_compare_exchange(&self->state, LOCK_FREE, LOCK_HELD); return old_state == LOCK_FREE; } void lock(futex_lock *self) {auto old_state = atomic_uint_compare_exchange(&self->state, LOCK_FREE, LOCK_HELD); if (old_state == LOCK_FREE) return if (old_state == LOCK_HELD) old_state = atomic_uint_exchange(&self->state, LOCK_CONTENDED) while (old_state != spinlock_free) { futex_wait_if_value_equals(&self->state, LOCK_CONTENDED) old_state = atomic_uint_exchange(&self-> Zustand, LOCK_CONTENDED) } } void unlock(futex_lock *self) {auto old_state = atomic_uint_exchange(&self->state, LOCK_FREE); if (old_state == LOCK_CONTENDED) futex_wake_one(&self->state) }

Dieses neue Design hat einige ziemlich große Vorteile für die umkämpfte Leistung. Indem wir einen neuen umkämpften Zustand hinzufügen, können wir sicherstellen, dass nur jeweils einer aktiviert wird. Obwohl es immer noch möglich ist, Wake unnötigerweise aufzurufen, hätten wir die Quelle für zusätzliche Cache-Line-Konkurrenz von Extra Wake entfernen sollen. Dies führte zu einigen zusätzlichen Leistungssteigerungen bei Mikrobenchmarks.

1 Thema 4-Threads
Pthread-Mutex ~47 mA/s* ~13 mA/s*
Futex ~53 mA/s* ~8.4 mA/s*
Futex (ohne Schleudern) ~53 mA/s* ~17 mA/s*
Futex (3 Zustände) ~53 mA/s* ~20 mA/s*

* Millionen Akquisitionen/Sekunde

Während es noch einige kleinere Bereiche für Verbesserungen zu geben scheint, scheint dies dem Pthread Mutex weit genug voraus zu sein, dass wir einige Gewinne im realen Produkt sehen sollten, wenn wir es austauschen. Bei der Ausführung unseres Random-Read-IOPS-Benchmarks gab uns dies einen Anstieg von ~4 Prozent. Es ist nicht viel, aber die Änderung hatte eine sehr breite Auswirkung auf viele Locking-gebundene Workloads für das Produkt. Die Größenreduzierung um 40 -> 4 Byte reduzierte auch unseren gesamten RAM-Verbrauch in einigen Szenarien um bis zu einem Gigabyte.

Verwandte Artikel

Nach oben scrollen