2 Punkte von GN⁺ 2024-10-03 | 1 Kommentare | Auf WhatsApp teilen
  • In Situationen mit hoher Contention werden die Unterschiede zwischen Mutex-Implementierungen besonders deutlich, und pthread_mutex_t aus Cosmopolitan Libc zeigt kürzere Laufzeiten und eine geringere CPU-Auslastung als die wichtigsten Implementierungen unter Windows und Linux
  • Im Test auf einem 24-Core Threadripper 29070WX unter Windows ist Cosmopolitan 2,75-mal schneller als Microsoft SRWLOCK und verbraucht 18-mal weniger CPU-Ressourcen
  • Auf einem 96-Core Threadripper Pro 7995WX unter Linux ist es 3-mal schneller als glibc und 11-mal schneller als musl libc; beim CPU-Zeit-Unterschied ist der Abstand noch größer
  • Auf einem MacOS M2 Ultra liegt Apple Libc knapp vorn, und Cosmopolitan verwendet in ARM-Umgebungen einen einfachen Algorithmus, der auf dem ulock-Systemaufruf von XNU basiert
  • Grundlage der Leistung ist die Integration von Googles nsync; entscheidend sind der CAS-Fast-Path, die Warteschlange der Wartenden, futex/ulock/WaitOnAddress(), Starvation-Vermeidung und das Design mit designated waker

Benchmark-Methode für Mutexe unter Contention

  • Der Test erstellt 30 Threads, und jeder Thread erhöht dieselbe globale Ganzzahl g_chores 100.000-mal
  • Jede Inkrement-Operation wird in einer sehr kleinen kritischen Sektion zwischen pthread_mutex_lock() und pthread_mutex_unlock() ausgeführt
  • Die Messwerte sind in Mikrosekunden angegeben; unterschieden werden drei Zeiten
    • wall time: die tatsächliche Laufzeit des Programms einschließlich Overhead für Thread-Erzeugung und join
    • user time: in User Space verbrauchte CPU-Zeit
    • system time: im Kernel verbrauchte CPU-Zeit
  • Da mehrere Threads parallel laufen, kann die Summe aus user time und system time größer sein als die wall time
  • In Situationen ohne Contention sind die Leistungsunterschiede zwischen Implementierungen meist gering, aber unter Contention treten die Unterschiede im Mutex-Design deutlich zutage

Windows: Schneller als SRWLOCK

  • Der Windows-Test wurde auf einem 24-Core Threadripper 29070WX durchgeführt
  • Mark Watermans MutexShootout bewertete Windows-SRWLOCK in Szenarien mit hoher Contention als die stärkste Implementierung
  • Unter denselben Bedingungen erreichte Cosmopolitan pthread_mutex_t eine kürzere wall time und eine geringere CPU-Auslastung als SRWLOCK
Implementierung wall time user time system time
Cosmopolitan pthread_mutex_t 148,940µs 328,125µs 62,500µs
Microsoft SRWLOCK 410,416µs 5,515,625µs 1,640,625µs
Microsoft CRITICAL_SECTION 949,187µs 7,937,500µs 5,078,125µs
MSVC 2022 std::mutex 991,750µs 12,156,250µs 4,031,250µs
spin lock 1,165,435µs 24,515,000µs 15,000µs
Cygwin pthread_mutex_t 9,780,803µs 1,937,000µs 6,156,000µs
  • Cosmopolitan Mutex ist 2,75-mal schneller als Microsoft SRWLOCK und verbraucht 18-mal weniger CPU-Ressourcen
  • Im Vergleich zum Cygwin-Mutex, der eine POSIX-Implementierung unter Windows bereitstellt, ist es 65-mal schneller
  • Der Cygwin-Mutex ist in diesem Anwendungsfall sogar langsamer als ein spin lock

Linux: Noch größere Unterschiede bei der CPU-Zeit als bei der wall time

  • Der Linux-Test wurde auf einem 96-Core Threadripper Pro 7995WX durchgeführt
Implementierung wall time user time system time
Cosmopolitan pthread_mutex_t 36,905µs 44,511µs 23,492µs
glibc pthread_mutex_t 101,353µs 150,706µs 2,724,851µs
spin lock 202,423µs 4,694,749µs 2,000µs
Musl libc pthread_mutex_t 411,013µs 2,167,898µs 9,926,850µs
  • Cosmopolitan Mutex ist 3-mal schneller als glibc und 11-mal schneller als musl libc
  • Gemessen an der CPU-Zeit verbraucht es 42-mal weniger als glibc und 178-mal weniger als musl libc
  • Bei Workloads, in denen alle Threads eine serialisierte Arbeit ausführen müssen, kann Cosmopolitan in htop so aussehen, als wäre nur ein Kern aktiv
  • Im gleichen Szenario können glibc und musl libc die CPU-Auslastung stark erhöhen, was bei mehreren gleichzeitigen Jobs auf demselben Server belastender ist

MacOS: Apple Libc knapp vorn

  • Der MacOS-Test wurde auf einem M2 Ultra durchgeführt
Implementierung wall time user time system time
Apple Libc 52,263µs 43,202µs 911,009µs
Cosmopolitan pthread_mutex_t 54,700µs 63,055µs 1,003,674µs
  • Auf MacOS M2 ARM64 ist Apple Libc etwas schneller als Cosmopolitan Mutex
  • Die allgemeine Mutex-Implementierung von Cosmopolitan funktioniert auf dieser Plattform nicht gut
  • Auf MacOS ARM verwendet Cosmopolitan einen einfacheren Algorithmus auf Basis von Ulrich Dreppers Futexes Are Tricky
  • Dabei wird der Großteil der schweren Arbeit an den ulock-Systemaufruf von XNU delegiert, was am Ende nahezu dieselbe Leistung wie Apples Implementierung liefert

Grundlage der Leistung: nsync-Integration

  • Der Schlüssel zur Leistung von Cosmopolitan Mutex ist die Integration von Googles nsync-Bibliothek
  • nsync ist eine Bibliothek mit 371 GitHub-Stars und wurde von Mike Burrows von Google geschrieben
  • Bei der Integration in Cosmopolitan wurden folgende Arbeiten durchgeführt
    • Ein lange unentdeckter Bug in der Mutex-Unlock-Funktion von nsync wurde gefunden und behoben
    • Die Portierung auf AARCH64 mit atomaren C11-Operationen machte den nsync-Mutex unter Contention 30 % schneller als upstream nsync
    • Neue Systemintegration ähnlich wie futex wurde geschrieben, um Portabilität zur Laufzeit zu ermöglichen
    • Es wurde dafür gesorgt, dass es reibungslos mit POSIX-Thread-Cancellation zusammenarbeitet

Funktionsweise von nsync

  • nsync versucht zunächst sofort einen optimistischen CAS (compare and swap), um das Lock schnell zu bekommen
  • Wenn das Lock nicht erworben werden kann, wird der aufrufende Thread in eine doppelt verkettete Liste der Wartenden eingetragen
    • Jeder Wartende besitzt sein eigenes Semaphor auf einer separaten Cache-Line
    • Ein Thread im Wartezustand berührt das Haupt-Lock danach nicht mehr
    • Das ist wichtig, um den Kommunikations-Overhead zu reduzieren, der entsteht, wenn mehrere Kerne dieselbe Cache-Line berühren
    • Als Hintergrund ist Ulrich Dreppers What Every Programmer Should Know About Memory verlinkt
  • nsync verwendet das futex des Betriebssystems, um Threads schlafen zu legen
    • Auf MacOS heißt futex ulock
    • Unter Windows übernimmt WaitOnAddress() die Rolle von futex
    • Unter den von Cosmo unterstützten Betriebssystemen fehlt nur NetBSD futex; dort werden POSIX-Semaphore im Kernel-Space implementiert und jedes Semaphor benötigt einen neuen File Descriptor
  • nsync vermeidet Starvation mit dem Konzept des „long wait“
    • Wenn ein Wartender 30-mal aufgeweckt wurde, aber intern jedes Mal beim Lock-Erwerb scheitert, wird dem Lock ein Bit hinzugefügt, damit Threads, die noch nicht gewartet haben, das Lock nicht bekommen
    • Ist dieses Bit gesetzt, schlägt der anfängliche CAS neu eintreffender Threads fehl, bis sich die Warteschlange ein Stück geleert hat
  • Einsatzfälle mit Contention in kleinen kritischen Sektionen werden durch das Konzept des designated waker beschleunigt
    • Wenn ein Thread aufgeweckt wurde und das Lock zu erwerben versucht, wird ein Bit im Haupt-Lock gesetzt
    • In nsync ist die Unlock-Funktion dafür verantwortlich, den nächsten wartenden Thread aufzuwecken
    • Dank dieses Bits muss ein Thread beim Unlock keinen zweiten Wartenden aufwecken, wenn bereits ein aufgeweckter Thread existiert
  • Relevanter Quellcode findet sich in cosmopolitan/third_party/nsync/mu.c und cosmopolitan/libc/intrin/pthread_mutex_lock.c

Reale Dienste und Prüfcode

  • Als Live-Demo mit Cosmo Mutex kann der Server http://ipv4.games/ angesehen werden
  • Dieser Dienst läuft auf einer 2-Core GCE-VM und hat bislang einem Botnet-DDoS mit bis zu 49.131.669 IPs standgehalten
  • Dank nsync konnten SQL-Abfragen in Hintergrund-Threads verlagert und eine Struktur genutzt werden, in der Threads sich gegenseitig Nachrichten senden
  • Statusmetriken sind unter /statusz verfügbar
  • Der Benchmark-Code misst die wall time mit gettimeofday() und user time sowie system time mit getrusage()
  • Am Ende wird mit g_chores == THREADS * ITERATIONS geprüft, ob alle Inkrement-Operationen ausgeführt wurden

Hinweise zu spin locks

  • In Situationen ohne Contention sind die Unterschiede zwischen Mutex-Implementierungen gering, und ein wenige Zeilen langer spin lock kann besser sein
  • Trotzdem sollte ein spin lock nur verwendet werden, wenn es wirklich keine andere Wahl gibt
  • In Umgebungen wie dem Kernel, wo extrem niedrige Systemebenen komplexere Verfahren erschweren, kann er nützlich sein
  • spin locks können auch als internes Implementierungsdetail in nsync-Locks eingesetzt werden
  • Wenn man Lock-Performance nur anhand der wall time betrachtet, kann ein spin lock gut aussehen; daher sollte man mit getrusage() immer auch die CPU-Zeit mitprüfen

1 Kommentare

 
GN⁺ 2024-10-03
Meinungen auf Hacker News
  • Neue Mutex-Implementierungen und Vergleiche sind immer interessant, aber diese Benchmark-Methode gefällt mir nicht. Das wirkt fast wie ein Microbenchmark.
    Leute, die schnelle Locks tatsächlich ausliefern, verwenden meist sehr große Multithread-Programme als primäres Mittel für Performance-Tests. In komplexen Workloads, in denen die Länge kritischer Abschnitte, die Zahl konkurrierender Threads und der Grad der Contention variieren, scheinen sich die Faktoren zu unterscheiden, die einen Mutex schnell oder langsam machen.
    Zur Einordnung: Ich habe den schnellen Lock von WebKit geschrieben, die ParkingLot-Abstraktion für Lock-Implementierungen erfunden (wird auch in Rust und der Unreal Engine verwendet) und früher Forschung sowie ein Paper zu schnellen Locks für Java gemacht.

    • Aus Sicht von jemandem, der Desktop-Apps gebaut hat, möchte ich ergänzen: Bei Apps mit Dutzenden häufig laufenden Threads würde ich gern Performance-Zahlen für Fälle mit geringer Contention sehen.
      Als Echtzeit-Audio-Programmierer ist mir die Kostenfrage wichtiger, einen Mutex zu nehmen, der noch nicht gelockt ist. In unserer App ist das mit Abstand der häufigste Fall. Ebenso würde ich gern die Kosten einer try-lock-Operation kennen, die fehlschlagen wird — nicht den Fall, in dem N Threads konkurrieren.
      Da Cosmopolitan Open Source ist, könnte ich es zwar selbst messen, aber schade ist es trotzdem.
    • Ging mir genauso. Es gibt mehrere Arten von Mutexen, und für bestimmte Workloads ist der eine oder andere besser. DistributedMutex und SharedMutex kommen mir in den Sinn (https://github.com/facebook/folly/blob/main/folly/synchroniz..., https://github.com/facebook/folly/blob/main/folly/SharedMute...)
      Wie bei Hashmaps gilt: Es ist selten, dass eine einzelne Hashmap für alle möglichen Workloads besser ist.
    • Dieser Stil von Mutex wird auch in PyMutex von Python 3.13 verwendet werden. Es gibt reale Benchmarks, die zeigen, wie viel schneller PyMutex als PyThread_type_lock vor 3.13 ist.
    • Das ist eindeutig ein Microbenchmark und dürfte allgemeine Performance kaum repräsentieren. Diese Seite bietet einen guten Maßstab für Benchmarking-Praktiken bei Betriebssystemen, ist allerdings stärker auf die akademische Welt ausgerichtet: https://gernot-heiser.org/benchmarking-crimes.html
    • Dieser konkrete Benchmark dürfte sogar unerwünschtes Verhalten begünstigen, zum Beispiel pathologische Unfairness. Optimales Scheduling würde alle Inkrement-Operationen des ersten Threads ausführen und danach alle des zweiten Threads usw., weil dadurch der Traffic zwischen Prozessoren minimiert wird.
      Ein Mutex, der bei einem fehlgeschlagenen Lock-Versuch eine feste Zeit schläft (z. B. 100 µs), wird die Arbeit fast immer bündeln und sich diesem Verhalten annähern — und kann dadurch den Benchmark „gewinnen“. In realen Anwendungen ist ein solcher Mutex aber schrecklich, sobald es auch nur etwas Contention gibt.
      Das heißt nicht, dass dieser Mutex schlecht oder der pthread-Mutex gut ist; es heißt nur, dass dieser Microbenchmark nichts misst, was die Performance realer Anwendungen vorhersagen würde.
  • Zu der Stelle „Cosmopolitan Mutex ist gut, weil es eine Bibliothek namens nsync verwendet“: nsync kannte ich bisher nicht, aber Mike Burrows hat auch Googles produktive Mutex-Implementierung geschrieben: https://github.com/abseil/abseil-cpp/blob/master/absl/synchr...
    Deshalb frage ich mich, warum diese Mutex-Implementierung im Benchmark fehlt. Und wenn unter macOS an __ulock delegiert wird, ließe sich das vermutlich einfacher erreichen, indem man einfach die Member-Funktionen wait() und notify_one() aus der Atomic-Bibliothek von libc++ verwendet.
    Es gab früher auch einen großen Thread zur Verbesserung der Mutex-Implementierung von Rust: https://github.com/rust-lang/rust/issues/93740#issuecomment-... Interessant daran ist, dass das Innenleben fast aller populären Mutex-Implementierungen ausführlich diskutiert wird.

    • Als ich zu AV kam, war Mike bereits eine Legende. Es hieß, immer wenn die Suchmaschine schneller werden musste, kam er vorbei, schrieb ein paar Kernfunktionen neu und ging wieder zu seiner eigentlichen Arbeit zurück.
      Das könnte stimmen, aber ich kann es nicht direkt bestätigen. Er war ein extrem kluger Ingenieur mit starkem Fokus auf Effizienz. Allerdings ließen wir einen Server nicht lange laufen.
    • Burrows war auch an der Burrows-Wheeler-Transformation, Bigtable, Dapper, Chubby usw. beteiligt.
    • Der Rust-Thread kommt letztlich dort an, aber im Kern geht es um Maras Arbeit, und deshalb wird auch ihr Buch von Januar 2023 erwähnt.
      Die aktuelle Rust-Mutex-Implementierung wurde Anfang dieses Jahres eingeführt; unter Linux ist sie vielleicht nicht sehr anders, aber unter Windows und Mac ist es meines Wissens neue Arbeit.
      Maras Beschreibung der Interna anderer Implementierungen ist trotzdem weiterhin interessant, aber man sollte für den eigenen Fall prüfen, ob die Informationen veraltet sind.
    • Der Grund, warum Abseils Mutex-Implementierung im Benchmark fehlt, könnte sein, dass es keine C-, sondern eine C++-Implementierung ist. Nur eine Vermutung.
    • Mike Burrows hat offenbar auch einen ACM-Preis bekommen, und dort ist er auch auf einem Foto zu sehen.
      https://awards.acm.org/award-recipients/burrows_9434147
  • Der Satz „Es ist zwar noch eine neue C-Bibliothek mit rauen Kanten, aber sie verbessert sich so schnell, dass es allmählich wie eine berufliche Pflichtverletzung wirkt, sie nicht in der Produktion einzusetzen“ ist ziemlich merkwürdig. Ich schätze das Cosmopolitan-Projekt sehr, aber solche überzogenen Überlegenheitsbehauptungen sind normalerweise ein ziemlich schlechtes Warnsignal.

    • Ich denke, Justines Aussagen sind im Großen und Ganzen zutreffend. Nur scheint es sein Stil, oder vielleicht seine Persönlichkeit, zu sein, Übertreibungen und selbstdarstellerische Formulierungen zu verwenden.
      Ich verstehe auch, dass das auf manche schroff wirken kann. Früher gab es bei llamacpp schon einmal auf diese Weise Drama.
    • Justine wirkt wie eine ziemlich brillante und kreative Person, aber ich möchte in der Produktion keine „neue“ libc mit „rauen Kanten“ einsetzen.
      In der Produktion haben Stabilität, Vorhersagbarkeit und Zuverlässigkeit oberste Priorität, nicht „sich rasend schnell zu verbessern“. Natürlich ist Performance ebenfalls wichtig. Schnellerer Code kann Infrastruktur reduzieren und damit bei Kosten und Umwelt helfen. Aber Geschwindigkeit kommt an letzter Stelle.
    • Wenn man lange allein vor dem Computer sitzt und programmiert, kommt vielleicht durch fehlenden sozialen Kontakt ein gewisses Maß an Arroganz dazu. Wenn es keine Korrektive gibt, die einen selbst oder die Bedeutung der eigenen Arbeit einordnen, können die Ergebnisse, auch wenn sie beeindruckend sind, größer erscheinen als das, was breit anerkannt ist.
      APE etwa empfinde ich als sehr beeindruckenden Hack, aber man kann auch kritisieren: „Heißt das jetzt, dass es nicht nur auf einer Plattform unsicher ist, sondern auf mehreren Plattformen gleichzeitig unsicher werden kann?“
      Je länger man in der Technikbranche ist, desto mehr merkt man, dass vollständige gegenseitige Vorteile extrem selten sind und das meiste ein Trade-off ist, bei dem man zugleich etwas gewinnt und verliert.
    • Zumindest für mich sah es wie ein Witz aus.
    • Ich frage mich, ob du schon einmal darüber nachgedacht hast, dass du und Justine vielleicht einen unterschiedlichen Sinn für Humor habt. Ich weiß auch nicht, wem es helfen soll, das hier zu posten.
  • Völlig am Rande: Als Spieleentwickler habe ich angefangen, langsame Mutexes zu mögen, die in allen Developer-Builds viel Debug-Arbeit leisten. Sie haben Debug-Namen/IDs, verfolgen den Besitzer, melden die in Contention verbrachte Zeit an den Profiler und melden auch Besitzerwechsel an den Profiler.
    Spiele strukturieren Parallelität tendenziell anders, und es haben sich Muster entwickelt, um Locks zu vermeiden. Aber solche Muster sind schwer zu verwenden und verlangen, dass Programmierer die Struktur ändern. Der meiste Code beginnt mit „Setzen wir hier erst mal einen Lock und schaffen den Milestone“.
    Auch schnelle Locks können unvorhersehbar langsam werden und würden etwaige Echtzeitgarantien zerstören. Im Durchschnitt mögen sie schnell sein, aber die Tail-Latenz verschwindet nicht. Ich möchte nicht die Person sein, die zurückkommt, um „unser Spiel ruckelt“ nachzuverfolgen, aber meistens bin ich genau diese Person.
    Also benutze ich lieber langsame Locks. Solche, die im Profiler groß und rot auffallen. Wenn man sieht, dass sie getroffen werden, refaktoriert man sie weg.
    Ich weiß, dass das eine schwierige Anforderung ist. In AAA-Produktionen kann man die Leute, die einen Profiler bedienen können, an einer Hand abzählen. Selbst über mehrere Produktionen hinweg war es immer so.
    Sorry fürs Jammern, aber ich hoffe, die Forschung an schnellen Parallelitäts-Primitiven und Algorithmen geht weiter.

    • Noch weiter am Rande: Das ist einer der Gründe, warum Spieleentwicklung in Rust Spaß macht.
      In Spielen will man Lock Contention nach Möglichkeit absolut vermeiden, und in vielen Fällen kann man beweisen, dass das Nehmen eines Locks unnötig ist. Zum Beispiel ist jedes Frame in Phasen unterteilt, und veränderlicher Zugriff auf eine bestimmte geteilte Ressource wird nur in einer bestimmten Phase benötigt, etwa bei update() vor render() oder beim Hot Reloading von Assets.
      Mit Scoped Threads und Rusts Borrowing-Regeln kann man die Struktur so anlegen, dass gar kein Mutex nötig ist, und man kann sicher sein, dass der Compiler später strikt einen Fehler ausgibt, sobald Codeänderungen ihn erforderlich machen würden.
      Wenn möglich, nehme ich immer lieber einen Compilerfehler als einen Spike im Profiler.
    • Stimme völlig zu. Debugging-Funktionen wie Deadlock-Erkennung oder Prüfung des internen Zustands machen sich schnell bezahlt. Wenn man Locks so häufig nimmt, dass es die Performance beeinflusst, sollte man sich das Design noch einmal ansehen. Geteilten veränderlichen Zustand zwischen Threads sollte man vermeiden.
  • Einerseits wirkt die Cosmo/APE/redbean-Familie wirklich beeindruckend, und die Kommentare zu den entsprechenden Artikeln sind meist positiv; auch gibt es kaum Einwände gegen das Konzept an sich. Andererseits hört man fast nie davon, dass andere Leute es tatsächlich verwenden.
    Natürlich teilt nicht jeder seine Arbeit ausführlich, aber nach ein paar Jahren hätte ich erwartet, zumindest ein paar Projekt-Retrospektiven zu sehen. Alle Erwähnungen von Cosmo/APE/redbean, die ich gesehen habe, stammen von Justines Website.
    Deshalb frage ich mich: Gibt es versteckte Fallstricke? Sind das Tools, die für ihre Ergebnisse irgendwelche unsauberen Dinge tun? Ist das ein tom7-artiger Witz oder Troll, den ich nicht verstehe, weil ich Compiler oder Runtimes nicht tief genug kenne? Oder sind es wirklich geniale Tools, die sich einfach noch nicht weit verbreitet haben?

    • APE funktioniert mit einem raffinierten Trick, der jederzeit blockiert werden kann; unter OpenBSD wurde er tatsächlich blockiert.
      Die meisten Leute, die plattformübergreifende Software bauen, wollen nicht eine einzige ausführbare Datei, die auf allen Plattformen läuft, sondern eine einzige Codebasis, die auf jeder unterstützten Plattform korrekt funktioniert.
      Aus dieser Sicht sind Sprachen wie Go angenehm, bei denen man, wenn man CGO vermeidet, für alle Ziele cross-kompilieren kann. Aber APEs Magie, auf drei Arten ausführbar zu sein, ist zwar wirklich clever, vermittelt aber kein Vertrauen, dass sie für immer funktionieren wird, und für die meisten bringt sie auch kaum praktischen Nutzen.
      Jede Plattform hat ihre eigenen Packaging- und Signaturanforderungen, daher ist es besser, separat für die jeweiligen Plattformziele zu kompilieren.
    • Persönlich finde ich, dass cosmo und ape sehr clever wirken, aber wenn gewöhnliche Tools bereits gut funktionieren, brauche ich diese Art von Cleverness im Arbeitsalltag nicht.
      Wenn man ein Projekt zum Beispiel bereits für andere Betriebssysteme und Plattformen cross-kompilieren kann oder eine entsprechende Build-Infrastruktur hat, gibt es keinen Grund, nach einer Lösung zu suchen, die ein einzelnes überall lauffähiges Binary erzeugt.
      Außerdem nutzt APE clevere Hacks, um auf mehreren Betriebssystemen zu laufen. Was, wenn diese Hacks eines Tages brechen, weil sich die Formate ausführbarer Dateien weiterentwickeln? Was, wenn niemand Zeit hat, APE an diese Änderungen anzupassen?
      Dagegen werden langweilige Tools wie gcc, clang, go und rust auch auf Betriebssystemen, die ständig aktualisiert werden und sich weiterentwickeln, weiterhin funktionieren. Deshalb bleibe ich einfach bei der langweiligen Seite. Der Grund, warum ich mich nicht um das Clevere kümmere, ist, dass das Langweilige für mich schlicht gut funktioniert.
    • Mozillas llamafile verwendet das. Es bündelt Modellgewichte und ausführbare Datei in einem, sodass es überall auf der cosmo/ape-Plattform laufen kann, und startet außerdem einen redbean-HTTP-Server für die Interaktion.
      Man kann es auch ohne eingebettete Gewichte ausführen und die Gewichte aus dem Dateisystem lesen lassen. Das könnte die einfachste Art sein, ein lokales LLM „herunterzuladen und sofort auszuführen“.
    • Cosmopolitan fühlte sich für mich immer wie eine technische Kuriosität an, die ständig Stoff für unterhaltsame Blogposts liefert. Genau die Art von Einfallsreichtum und Besessenheit fürs Setup, mit der man auf Seiten wie HN fast garantiert auf die Startseite kommt.
      Aber als Basistechnologie wie libc scheint es vor allem für unterhaltsame Spielereien oder kleine persönliche Projekte nützlich zu sein.
      In diesem Kontext wirkt es etwas seltsam, wenn es als ernsthafte Alternative zu Dingen wie glibc, musl oder msvcrt präsentiert wird. Ein sehr niedlicher Hack, aber wenn ich ihn in etwas fände, auf das ich mich ernsthaft verlasse, wäre ich ziemlich irritiert.
    • Mozilla hat das auf Cosmopolitan libc basierende Llamafile-Projekt: https://github.com/Mozilla-Ocho/llamafile
      Auch auf Hugging Face werden regelmäßig populäre Modelle hochgeladen, die in dieses Format neu paketiert wurden: https://huggingface.co/models?search=llamafile
      Ob es allerdings über das schnelle Ausprobieren kleiner Modelle hinaus praktischen Nutzen hat, ist eine andere Frage.
  • Wenn das so gut ist, frage ich mich, warum nicht alle C-Bibliotheken denselben Trick übernommen haben.
    Meine Vermutung ist, dass diese Tricks wahrscheinlich nur auf bestimmten Architekturen, bestimmten CPU-Modellen, bestimmten Workloads oder Zugriffsmustern immer schnell sind. Wenn man verschiedene Workloads auf der gesamten unterstützten Hardware ordentlich benchmarken würde, käme derselbe Vorteil womöglich nicht heraus.
    Oder die Semantik der pthread-API, die Cosmopolitan implementieren will, unterscheidet sich subtil, und diese Implementierung hält sich vielleicht nicht strikt an die Spezifikation.
    Es fällt schwer, sich vorzustellen, dass mehrere libc-Autoren mit der aktuellen Forschung zu Betriebssystem-Primitiven nicht Schritt halten.

    • Solche Projekte haben neben einer bestimmten API noch Dutzende andere Prioritäten. Sich auf einzelne APIs zu versteifen, ist keine gute Nutzung der begrenzten Zeit. Und als Gegenbeispiel kann man sich malloc und String-Routinen in den üblichen libcs unter Linux ansehen.
      Das malloc von glibc ist halbwegs brauchbar, wird bei Gesamtgeschwindigkeit und Skalierbarkeit aber leicht von moderneren Alternativen überholt. Es fragmentiert stark und wird mit der Zeit schlechter; außerdem gibt es viele Stellschrauben wie MALLOC_ARENA_MAX, die reale Workloads deutlich beeinflussen. Das malloc von musl ist in Sachen Performance auf jeder Ebene furchtbar. In Multithread-Programmen hat die Verwendung des musl-Allokators die Performance so stark ruiniert, dass man es fast fahrlässig nennen konnte.
      musl hat auch keine SIMD-optimierten String-Vergleichsroutinen oder Ähnliches. Man wäre überrascht, wie viele CPU-Zyklen in nichttrivialen Programmen für solche Operationen draufgehen; das zeigt sich auch klar in echten Profilen, und Verbesserungen daran kommen praktisch jedem Programm allgemein zugute. Die optimierten Routinen von glibc sind gut, aber es sieht so aus, als könnten sie immer noch schneller sein.
      Das sind keine „Optimierungen, die nur auf eine Architektur spezialisiert und nicht verallgemeinerbar sind“. Gerade diese beiden Bereiche sind gut erforscht und verstanden und können bei fast allen Workloads die Wall-Clock-Zeit um das 2- bis 5-Fache senken sowie die langfristige Nutzung der Working Sets deutlich verbessern. Warum wurden sie also nicht übernommen? Wie immer wohl, weil es andere Dinge zu tun gab oder weil es konkurrierende Prioritäten gab, etwa bei musl Einfachheit statt maximaler Performance.
      Ich mache solchen Projekten keinen Vorwurf. Niemand sagt: „Mein Programm ist miserabel langsam und so entworfen, dass es nichts richtig kann, und darauf bin ich stolz.“ Aber die Vorstellung, die Mitarbeitenden dieser Projekte hätten beim Design ausschließlich die perfekte Pareto-Grenze gewählt, ist völlig unrealistisch und bildet nicht ab, wie die meisten Projekte tatsächlich funktionieren.
    • Es könnte an Politik, NIH-Syndrom oder alten Maintainern liegen.
      In glibc oder dem entsprechenden C++-Pendant dauert es ewig, etwas zu ändern.
      Es gibt mehrere Arten von Synchronisations-Primitiven, und pthreads unterstützt nur einen Teil davon. Wenn man sich darauf beschränkt, gibt man meist Performance auf und gewinnt dafür Portabilität.
    • Ich frage mich, ob „Es fällt schwer, sich vorzustellen, dass mehrere libc-Autoren mit der aktuellen Forschung zu Betriebssystem-Primitiven nicht Schritt halten“ sarkastisch gemeint ist.
      Zu libc-Maintainern kann ich nichts sagen, aber als jemand, der ein paar Dinge maintained, versuche ich nicht, die neueste Forschung zu implementieren. Ich versuche, Stabilität zu erhalten und sicherzustellen, dass die Performance akzeptabel ist. Forschungsimplementierungen liegen außerhalb meines „Maintenance“-Budgets.
    • Ich frage mich, ob es beim Ändern der pthread-Mutex-Implementierung ABI-Überlegungen gibt.
    • Bei der Frage „Wenn das so gut ist, warum haben dann nicht alle C-Bibliotheken denselben Trick übernommen?“ muss ich an diesen Witz denken:
      Ein Mann und ein Statistiker gehen die Straße entlang und sehen einen 50-Euro-Schein. Der Statistiker geht weiter, der Mann bleibt stehen und sagt: „Schauen Sie, da liegt Geld auf dem Boden.“ Darauf sagt der Statistiker: „Der ist bestimmt gefälscht. Wenn er echt wäre, hätte ihn schon jemand aufgehoben“, und geht weiter. Der andere Mann hebt das Geld auf.
  • Threads und Mutexe gehören zu den Dingen, die in der Informatik am meisten Komplexität erzeugen. Eine neue Implementierung betrachte ich immer skeptisch, bis sie jahrelang in großem Maßstab eingesetzt wurde.
    Bugs in solchen Threading-Mechanismen entgehen oft selbst der intensivsten Prüfung. Als Java Mitte der 90er aufkam, brachte es alle möglichen Thread- und Mutex-Bugs in Solaris ans Licht.
    Was man braucht, ist nicht die schnellste Mutex-Implementierung, sondern eine zuverlässige Implementierung.

    • Mutexe sind bei weitem nicht das „Komplexeste“. Es gibt auch nicht besonders viele Möglichkeiten, sie effizient zu implementieren. In den meisten Fällen, besonders auf dem Read-Pfad, ist es am besten, sie zu vermeiden.
  • Dieser Code benchmarked nicht die Performance von Mutex-Locks, sondern Mutex-Contention. Wenn man Locks so verwendet, sollte man den Code neu bewerten.
    Jeder Thread sperrt und entsperrt den Mutex jedes Mal, wenn er g_chores erhöht. Dadurch entsteht der Overhead, den Mutex häufig zu erwerben und wieder freizugeben, und zwar 100.000-mal pro Thread.
    Dieser Overhead verdeckt die tatsächlichen Performance-Unterschiede zwischen den Locking-Mechanismen, weil der Benchmark von Lock-Contention dominiert wird statt von echter Arbeit. Solche Benchmarks sind nutzlos.

  • Ich bin Fan von Justine und ihrer Arbeit, aber das hier ist vermutlich einer der am wenigsten interessanten Testfälle für einen Mutex-Benchmark. Eine Situation, in der mehrere Threads ständig auf denselben Mutex einhämmern, sollte man von vornherein vermeiden.
    Deshalb finde ich es nicht besonders interessant, welche Mutex-Implementierung diesen Fall am besten handhabt.

    • Mich würde interessieren, was du für einen guten Benchmark-Testfall für Mutexe hältst.
    • In den meisten Fällen, in denen ich Locks oder Semaphore verwende, geht es um sehr teure Ressourcen. Die Nutzung dieser Ressource überwiegt den Performance-Overhead des Locks bei weitem.
    • Was sollte man dann messen? Der Fall ohne Contention ist wichtig und dient als Basislinie, aber darüber hinaus liegt genau hier die Schwachstelle von Mutexen. Wenn sie mit Contention schlecht umgehen, steht Hardware untätig herum, der Scheduler hat mehr Arbeit oder es gibt mehr Kernel-Eintritte.
      Eine wichtige Sache habe ich vergessen: Locks mit schlechter Performance unter Contention können sehr negative systemische Effekte haben, etwa Hotspots im Speichernetzwerk erzeugen, und auch das würde sich hier zeigen.
    • Der Einschätzung „Mehrere Threads sollten nicht ständig auf denselben Mutex einhämmern“ kann ich nicht ganz zustimmen.
      Mir fallen einige Fälle ein, in denen mehrere Threads auf denselben Mutex zulaufen. Ein einfaches Beispiel ist das parallele Befüllen einer Datenstruktur wie einer Liste oder eines Dictionary.
      Das kann man auch per Message Passing machen, aber es kann mehr Speicher verbrauchen und langsamer sein, als darauf zu warten, an eine gemeinsame Stelle schreiben zu können.
  • Produktion dreht sich nicht um Geschwindigkeit, Effizienz oder offensichtlich „clevere Hacks“.
    Wenn ich 50 % Effizienz opfern muss, um die Garantie zu haben, nicht sonntagmorgens um 3 Uhr gerufen zu werden, um ein kaputtes System zu reparieren, würde ich diese Entscheidung jedes Mal treffen.
    Produktion dreht sich um Zuverlässigkeit, und zuverlässigen Code zu schreiben ist 10-mal schwieriger, als „schnellen“ Code zu schreiben.