- Wird derselbe
jq-Quellcode neu gebaut und der Allocator ausgetauscht, sinkt die Verarbeitungszeit für 500 MB GeoJSON von 4,606 Sekunden auf 2,428 Sekunden; damit ist der Build 1,90-mal schneller als das Ubuntu-Binary - Der Benchmark wurde auf einem Ryzen 9 9950X mit der Parcel Map des Alameda County Assessor durchgeführt; extrahiert wurde
SitusCityfür die BedingungTotalNetValue < 193000 - Schon ein einfacher Rebuild brachte 2–4 % Verbesserung; die Kombination aus
clang-18,-O3,-fltound-DNDEBUGerreichte gegenüber dem Ubuntu-Paket 1,20-fache Performance - Im Profil zeigte sich, dass Kosten für Speicherallokationen stark ins Gewicht fallen; verglichen wurden
TCMalloc,jemallocundmimalloc, wobeimimallocinLD_PRELOAD-Experimenten am schnellsten war - Der finale mit
mimallocgelinkte Build erzielte auch in einem separaten Fall mit 2,2 GB JSON 0,755 Sekunden gegenüber 1,424 Sekunden; je nach Workload können sich also große Unterschiede zum Standard-Build der Distribution ergeben
Referenz-Workload und Messmethode
- Testobjekt ist das JSON-Verarbeitungstool
jq; als Eingabedaten dient eine 500-MB-GeoJSON-Datei mit der Parcel Map des Alameda County Assessor - Die ausgeführte Query gibt aus der Parcel-Liste
SitusCityfür Einträge aus, dieTotalNetValue < 193000erfüllen.features[] | select(.properties.TotalNetValue < 193000) | .properties.SitusCity
- Das standardmäßige Ubuntu-
/usr/bin/jqbenötigte bei gecachter Datei etwa 5 Sekunden; die detaillierten Benchmarks wurden wiederholt mithyperfinegemessen - Um Schwankungen während der Ausführung zu reduzieren, wurde der Prozess mit
taskset -c 2auf logische CPU 2 fixiert- Diese Einstellung soll Einflüsse durch System-Interrupts auf CPU 0 und CPU-Migration vermeiden
Einfacher Rebuild desselben Quellcodes
- Der von Ubuntu verwendete
jq-Quellcode wurde heruntergeladen und ohne zusätzliche Flags per configure und Build neu gebaut - Allein dieser einfache Rebuild war etwa 2–4 % schneller als das Ubuntu-Binary-Paket
- Rebuild-Binary: durchschnittlich 4,517 Sekunden
- Ubuntu-
/usr/bin/jq: durchschnittlich 4,641 Sekunden - Daraus ergibt sich etwa 1,03-fache Performance
Einsatz von clang und Optimierungs-Flags
- Im nächsten Schritt wurden
clang-18, ein höheres Optimierungsniveau, LTO sowie Flags im Zusammenhang mit Debugging und Profiling gemeinsam eingesetzt - Die wichtigsten performancewirksamen Flags waren
-O3,-fltound-DNDEBUG-O3nutzt ein höheres Optimierungsniveau als-O2-fltoaktiviert Link-Time Optimization-DNDEBUGreduziert die im Profil deutlich sichtbaren Assertion-Kosten
- Ein Beispiel für die verwendete configure-Konfiguration sieht wie folgt aus
CC=clang-18LDFLAGS="-flto -g -Wl,--emit-relocs -Wl,-z,now -Wl,--gc-sections -fuse-ld=lld"CFLAGS="-flto -DNDEBUG -fno-omit-frame-pointer -gmlt -march=native -O3 -mno-omit-leaf-frame-pointer -ffunction-sections -fdata-sections"
- Dieser Build war 1,20-mal schneller als das Ubuntu-Binary
- Optimierter Rebuild: durchschnittlich 3,853 Sekunden
- Ubuntu-
/usr/bin/jq: durchschnittlich 4,631 Sekunden
Experimente mit Austausch des Allocators
jqist ein komplexes C-Programm, und im Profil zeigte sich Speicherallokation als größter Kostenfaktor- Zunächst wurde mit dem als Ubuntu-Paket bereitgestellten
TCMallocgelinkt und neu gebaut- Zu
LDFLAGSwurde-L/usr/lib/x86_64-linux-gnu -ltcmalloc_minimalhinzugefügt - Rebuild-Binary: durchschnittlich 3,253 Sekunden
- Ubuntu-
/usr/bin/jq: durchschnittlich 4,611 Sekunden - Das Ergebnis ist 1,42-mal schneller als das Ubuntu-Binary
- Zu
- Auch beim unveränderten Ubuntu-Binary lässt sich durch reinen Austausch des Allocators per
LD_PRELOADeine gewisse Verbesserung erzielen- Standard: durchschnittlich 4,601 Sekunden
- TCMalloc per Preload: durchschnittlich 4,082 Sekunden
- Gegenüber dem Standard 1,13-mal schneller
Dynamisches Preloading und THP-Konfiguration
- Die von Ubuntu bereitgestellten
jemalloc,mimallocundTCMallocwurden perLD_PRELOADverglichen - Dieser Vergleich wurde mit den folgenden gesetzten Umgebungsvariablen durchgeführt
MIMALLOC_LARGE_OS_PAGES=1MALLOC_CONF="thp:always,metadata_thp:always"GLIBC_TUNABLES=glibc.malloc.hugetlb=1
- Im Ergebnis war mimalloc am schnellsten
- Standard-glibc: durchschnittlich 4,123 Sekunden
- TCMalloc per Preload: durchschnittlich 4,130 Sekunden
- jemalloc per Preload: durchschnittlich 3,510 Sekunden
- mimalloc per Preload: durchschnittlich 3,154 Sekunden
- Aktiviertes THP bringt sowohl dem glibc-Allocator als auch jemalloc und mimalloc Vorteile
THP + mimallocist 31 % schneller alsTHP + glibcund 48 % schneller als die glibc-Standardeinstellung
Endergebnis des mit mimalloc gelinkten Builds
- Da dynamisches Preloading aus Performance-Sicht nicht als ideal betrachtet wurde, wurde
jqim letzten Schritt mitmimallocgelinkt und neu gebaut - Der finale Build war 1,90-mal schneller als das Ubuntu-Binary-Paket
mimalloc-Rebuild: durchschnittlich 2,428 Sekunden- Ubuntu-
/usr/bin/jq: durchschnittlich 4,606 Sekunden - Jeder Benchmark basiert auf 10 Durchläufen
- Derselbe Build wurde auch in einer anderen Anwendung verwendet
- Verarbeitet wurden 2,2 GB JSON in 13.000 Dateien
- Für die Parallelisierung kam
rushzum Einsatz jqalsmimalloc-Rebuild: 0,755 Sekundenjqaus dem Ubuntu-Paket: 1,424 Sekunden
- Auch in diesem separaten Fall liegt die Beschleunigung nahezu bei 2×
1 Kommentare
Hacker-News-Meinungen
Clickbait wie „Ein Ubuntu-Paket neu bauen und durch Austausch des Memory Allocators 90 % schneller machen“ ist so schlimm, dass man am liebsten einen Schlag über TCP/IP hinweg austeilen würde. Es war genau ein Paket, und ein Teil der Verbesserungen lag nicht einmal am Neukompilieren.
Trotzdem habe ich einmal jemalloc per
LD_PRELOADin ein Programm eingeschleust, um diemalloc-Implementierung auszutauschen, und die Ergebnisse waren ziemlich gut. Ich habe die Performance nicht gemessen, aber der Speicherverbrauch der Anwendung stabilisierte sich, und ein Problem, das wie ein Memory Leak aussah, verschwand ebenfalls. Tatsächlich war es wahrscheinlich weniger ein Problem der App selbst als vielmehr Speicherfragmentierung im Standard-malloc.free()aufgerufen wird, wird Speicher nach außen hin außer in Ausnahmefällen nicht wirklich freigegeben.Je mehr Threads und CPU-Kerne es gibt, desto schlimmer wird dieses Problem. Eine einfache Lösung ist, die „magische“ Umgebungsvariable
MALLOC_ARENA_MAX=2zu setzen und damit die Anzahl der Caches zu begrenzen. Eine andere Möglichkeit ist, dass die Anwendung regelmäßigmalloc_trim()aufruft, um die Caches zu leeren; dafür muss allerdings der Quellcode geändert werden.https://www.joyfulbikeshedding.com/blog/2019-03-14-what-caus...
Umgekehrt bin ich froh, dass nicht mit dem potenziell fehleranfälligen
-O3kompiliert wird. Für einige performancekritische Teile kann das gut sein, aber ich möchte nicht, dass das gesamte System mit-O3kompiliert wird.Vor langer Zeit habe ich angefangen, Mozilla und meinen Linux-Kernel nach meinen Vorlieben zu bauen, und normalerweise brachte das ordentliche Performance-Gewinne. Der ganze Zweck der Linux-Distribution Gentoo besteht beispielsweise darin, Performance-Gewinne dadurch zu erzielen, dass alles aus dem Quellcode optimiert kompiliert wird.
jq,grep,ffmpegoderocrmypdfdie CPU der Flaschenhals ist; solche allgemeinen Unix-Utilities werden oft als universelle Build-Ziele erstellt, nicht für eine bestimmte Anwendung.Engineering bedeutet Kompromisse. Die meisten Gewinne im Artikel stammen daraus, den Speicher-Allokator zu spezialisieren. Man muss bedenken, dass manche Projekte multithreaded sind: Ein Thread alloziert, ein anderer Thread schreibt Daten, und ein dritter Thread gibt sie wieder frei.
Der Allokator muss damit umgehen können; daher kann eine Beschleunigung in einem Projekt in einem anderen zu Abstürzen führen. Auch die Strategie für Re-Allokationen ist ein Problem. Manche Programme allozieren vorab und rühren
mallocdanach nie wieder an, während andere ständig freigeben und erneut Speicher anfordern. Entscheidend ist auch, wie gut Fragmentierung gehandhabt wird und ob die Laufzeit 10 Sekunden oder 10 Jahre beträgt. Manchmal ist die Wahl des Allokators der Unterschied zwischen langfristiger Stabilität und kurzfristiger Geschwindigkeit.Als ich einen Video-Editor baute, der beim Testen von 4K-Video Frames cached, habe ich mehrere Allokatoren ausprobiert. 32 MB pro Frame bei 60 fps sind fast 2 GB pro Sekunde und Spur. Man stößt sofort an die Grenzen des Allokators und merkt zumindest, dass der Standard-Allokator von glibc langfristig am stabilsten ist. In kurzen Benchmarks ist er aber der langsamste.
Natürlich kann das Ausmaß der Beschleunigung variieren, aber Benchmarks zeigen im Allgemeinen Verbesserungen über die Breite. Ob das für eine bestimmte Anwendung relevant ist, ist eine ganz andere Frage. Was Abstürze angeht: Das sind alles universelle, multithreaded Allokatoren, sie verhalten sich also nicht grundlegend anders als glibc; Bugs kann es genauso gut auch in glibc geben.
DEFAULT_MMAP_THRESHOLD_MAX, und auf 64-Bit-Plattformen sind das 32 MiB, sodass man glibc nicht dazu bringen kann, sie zu cachen, wie immallopt-Handbuch dokumentiert.Jedes Mal wird per
mmapdirekt beim Kernel Speicher angefordert und permunmapzurückgegeben. Diese System Calls sind etwas langsam, und in meinem Fall sind die Kosten für Page Faults auf jeder Speicherseite beim ersten Zugriff zu hoch, um die Performance-Ziele zu erreichen. Die Lösung ist wirklich simpel: Nur für Video-Frames eine eigene Free List über einem universellen Allokator oder übermmapverwenden. Da Allokationen exakt derselben Größe sehr stabil wiederholt werden, funktioniert das gut.[1] Im UYVY-Format etwas weniger als 64 MiB, im I420-Format etwas weniger als 48 MiB.
Wenn ich nun diesen übergeordneten Kommentar lese, mache ich mir aber Sorgen, ob ich den Artikel völlig missverstanden habe. Vom Ton her klingt es so, als dürfe man das, was im Gist beschrieben wird, auf keinen Fall tun und als sei es ein schrecklicher Vorschlag, der all diese Komplexität übersieht. Kann jemand helfen zu verstehen, ob der ursprüngliche Gist ein guter Text ist, ob er berechtigte Punkte hat oder ob er völlig wertlos ist? Bis ich diesen Kommentar gesehen habe, hielt ich ihn für wertvoll, aber mir ist klar geworden, dass ich nicht klug genug bin, das zuverlässig zu unterscheiden.
Wenn man den passenden Kompressionsalgorithmus für die Daten verwendet, erklären einem die Leute, warum man dumm sei und einen anderen Algorithmus hätte nehmen sollen. Kürzlich musste ich einen bestimmten langen JSON-String komprimieren, um ihn in Dynamo zu speichern, und nachdem ich alle populären Algorithmen gründlich getestet hatte, lag Brotli weit vorn. Das hielt trotzdem nicht jeden Vorbeikommenden davon ab, mir zu sagen, zlib sei besser. Manchmal ist das ziemlich ermüdend.
Gentoo Linux ist im Grunde eine Distribution, die genau für solche Leute gemacht wurde: damit man den eigenen Linux-Rechner für die eigenen Zwecke optimieren kann
Nach der Ersteinrichtung ist sie ziemlich simpel und leicht zu benutzen. Ich erinnere mich, dass ich im Gentoo-Linux-Kanal auf Matrix viele Freunde gefunden habe; das war eine schöne Zeit
https://www.gentoo.org/
Interessanterweise war das frühe ChromeOS im Grunde eine angepasste Gentoo-Linux-Installation. Ich weiß nicht, ob es intern immer noch Gentoo Linux nutzt
Ich nutze Gentoo seit 20 Jahren, aber nie wegen der Performance. Gentoo ist großartig, wenn man weiß, wie sich das System verhalten soll, und es hilft einem, dorthin zu kommen
Das Ziel von Gentoo ist es, ein Betriebssystem zu haben, das alle Programme aus dem Quellcode baut, statt vorgefertigte Binärpakete zu verwenden. Das ermöglicht fortgeschrittene Geschwindigkeitssteigerungen und Anpassungen, bedeutet aber auch, dass selbst die grundlegendsten Komponenten wie der Kernel aus dem Quellcode kompiliert werden müssen. In der Linux-Community gilt es wegen seines aufwendigen Installationsprozesses als sehr komplexes Betriebssystem. Eine Standardinstallation von Gentoo bootet direkt in eine Eingabeaufforderung; die Nutzer müssen die Festplatte selbst partitionieren, ein Paket namens „Stage 3 tarball“ herunterladen und entpacken, Pakete manuell installieren und daraus das System aufbauen. Neue oder unerfahrene Nutzer wissen oft nicht, was sie tun sollen, wenn sie in ein Installationsprogramm gelangen und keine grafische Oberfläche sehen. Mitglieder von /g/ übertreiben oft den Wert von Gentoo, um neue Nutzer dazu zu bringen, eine Installation zu versuchen
Es kann ein- oder zweimal haken, aber wenn man insgesamt genug Linux-Erfahrung hat, ist es wahrscheinlich, dass man in die Build-Rezepte hineingeht, sie repariert, sie nach Bedarf zum Laufen bringt und die Änderungen upstream beiträgt. Das ist das Ergebnis eines hartnäckigen Fokus auf Minimalismus und darauf, Overengineering jeder Art zu vermeiden. Genau das habe ich während meiner ganzen Gentoo-Zeit vermisst. Bei Gentoo habe ich ständig an USE-Flags und Package-Masks herumgedreht, auf eine Weise, die anderen Nutzern kaum half. Das Build-System war so komplex, dass es über die Jahre hinweg zu schwierig war, es wirklich zu beherrschen, Probleme auf Root-Cause-Ebene zu beheben und upstream beizutragen. Void kann auch eine ideale Basis sein, wenn man nicht das gesamte System aus dem Quellcode bauen will, aber Pakete aus Distributions-Binaries und selbst aus dem Quellcode gebauten Paketen mischen möchte
Danach bin ich zu ArchLinux gewechselt, und für mich war das größtenteils in Ordnung. Wenn man einen ziemlich standardmäßigen Prozessor nutzt, glaube ich nicht, dass Gentoo einen so großen Vorteil bringt
Damit fällt man nicht nur bei
jq, sondern auch bei Sicherheitsupdates für onigurama, die Regex-Parsing-Abhängigkeit, heraus. Früher gab es ein Sicherheitsupdate für onigurama, und wenn so etwas wieder passiert, könnte man verwundbar sein.jqwird häufig zum Parsen nicht vertrauenswürdiger JSON-Daten verwendetEs ging um „Sicherheitsupdate: Behebung mehrerer ungültiger Pointer-Dereferenzierungen, Out-of-bounds-Write-Speicherbeschädigungen und Stack-Buffer-Overflows“; betroffen waren CVE-2017-9224, CVE-2017-9226, CVE-2017-9227, CVE-2017-9228 und CVE-2017-9229
libonig5und wird ganz normal aktualisiert werdenIch will den Wert des CVE-Systems nicht kleinreden, aber es lässt sich schwer bestreiten, dass sich die tatsächlichen Auswirkungen der einzelnen Findings stark unterscheiden
Es ist eine Weile her, dass ich mich mit so etwas beschäftigt habe, aber meiner Erinnerung nach kauft man sich, sobald man über die Flags hinausgeht, die die Upstream-Entwickler verwenden, seltsame Bugs und enorme Gleichgültigkeit ein, wenn Bugs auftreten. Ich meine hier nicht Distributions-Paketierer, sondern Upstream-Entwickler
Einen anderen
mallocals den aus libc habe ich nie verwendet, aber ich vermute, dass dasselbe Prinzip giltAber wenn alle das tun, entsteht eine Monokultur, und Monokulturen sind fragil und schlecht. Code wird erst dadurch ein wenig robuster, dass er in anderen Kontexten gebaut wird, also auf anderen Plattformen, mit anderen Compilern, Optionen, Bibliotheken usw. Ein Bug, den die meisten nie berührt haben, weil ihre Plattform oder ihre Build-Flags zufällig direkt an der Falle vorbeiführen, ist trotzdem ein Bug, und es ist besser für den Code, ihn zu finden und zu beheben. Als Einzelne profitieren wir alle davon, wenn Code insgesamt robuster statt fragiler wird
Allerdings dachte ich auch, dass
-march=nativedie Hauptverbesserung ist, die ich gesehen habe, und dieser Artikel zeigt, dass das nicht unbedingt so ist. Anwendungen, die Gleitkomma verwenden, haben möglicherweise auch mehr raue KantenEs ist zwar eher so, dass es mit einem anderen Allocator neu gebaut wurde, der in bestimmten Workflows gute Benchmark-Ergebnisse liefert.
malloc. Dass Distributionen statt mimalloc oder jemalloc weiterhin glibcmallocverwenden, ist im Grunde Arbeitsverweigerung.mallocgut ist?Mich würde interessieren, wie die Performance im Vergleich zu diesem Rust-basierten
jq-Klon ausfällt.cargo install --locked jaqUm Optimierungen für eine bestimmte CPU-Familie zu aktivieren, kann man auch
RUSTFLAGS="-C target-cpu=native"hinzufügen.cargo installist ein unterschätztes Rust-Feature, das genau zu der im Artikel beschriebenen Art von Nutzung passt. Da das Tool aus dem Source gebaut wird, kann man plattformspezifische Funktionen oder Instruktionen auswählen, die normalerweise aus Kompatibilitätsgründen mit älteren CPUs nicht in Binaries enthalten sind. Man muss kein Repository klonen oder herausfinden, wie es gebaut wird; das kommt einfach mit.jaq[1] undyq[2] sind meine erste Wahl, wenn ichjqnutze und eine schnelle, einfache Performance-Steigerung brauche.[1] https://github.com/01mf02/jaq
[2] https://github.com/mikefarah/yq
jqundgojq, getestet mit meinerjq-Lösung für AoC 2022 Day 13.https://gist.github.com/oguz-ismail/8d0957dfeecc4f816ffee79d...
Es liegt immer noch hinter beiden zurück.
cargo installauch ein--git-Flag, mit dem man eine Repository-URL angeben kann. Das ist nützlich, wenn es kein öffentliches Paket gibt oder man einen aktuellen Commit will, der noch nicht veröffentlicht wurde.Ich habe das früher schon mehrfach genutzt, besonders als einfache Möglichkeit, schnell privat hastig gebaute Tools zu installieren, nachdem ich sie in ein Repository gepusht hatte, ohne einen Release-Prozess aufzusetzen oder Binaries manuell auf meine privaten Geräte zu kopieren und den exakten Commit zu verfolgen, mit dem gebaut wurde.
Wenn man das wirklich so machen will, sollte man Ubuntu einfach das genaue Source-Paket herunterladen lassen, das es erwartet. In diesem Fall nimmt man
apt-get source jq.Danach kann man in das Paket gehen und nach Belieben neu kompilieren. Man kann es für Verteilung oder Archivierung auch wieder paketieren. So erhält man ein Ergebnis, das dem Upstream-Ubuntu deutlich näher ist, statt eine Menge seltsamer Fehler und Inkonsistenzen zu bekommen.
Der Titel ist irreführend. Gemeint sind 90% der schneller gewordenen Zeit; tatsächlich ist es etwa 45% schneller.
Wenn einen interessiert, wie wir Sprache verwenden, ist das ein bisschen spannend. Man könnte auch sagen, dass in derselben Zeit 90% mehr Arbeit erledigt wird, und das passt zu anderen gebräuchlichen Geschwindigkeitseinheiten wie Meilen pro Stunde, Wörter pro Minute oder Bits pro Sekunde. Bei Computer-Performance ist es aber üblich, die Zeit zu messen, die für eine feste Arbeitsmenge benötigt wird. Vermutlich, weil die Arbeitsmenge meistens fest ist und sich nur die Wartezeit ändert. Genau das gilt auch für diesen Blogpost, deshalb landet die Zeit im Zähler. Der Artikel selbst ist sehr interessant und gut geschrieben, aber es ist nicht 90% schneller.
Außerdem würde man bei Zeiteinheiten nicht das Wort „faster“ verwenden. „45% less time“ und „45% faster“ sind sehr unterschiedliche Aussagen, und beide haben innerhalb wie außerhalb der Programmierung eine Bedeutung.
Wenn man sagt, man habe „etwas um N% reduziert“, nimmt man normalerweise an, dass diese N% sich auf das reduzierte Objekt selbst beziehen, nicht auf einen anderen Wert.
Als ich las, dass man mit so einer einfachen Änderung eine große Beschleunigung erzielen kann, war mein erster Gedanke, dass man die jq-Autoren informieren sollte. Es könnte Fallstricke geben, auf die man achten muss, und nach Tests könnte man es für alle schneller machen.
Unabhängig vom Ergebnis scheint es sinnvoll, kurz Bescheid zu sagen. Im Artikel scheint diese Option aber nicht einmal erwogen zu werden, und auch hier in den Kommentaren sehe ich sie nicht. Übersehe ich etwas?
Ich weiß nicht, ob dort der glibc-Allocator ebenfalls Standard ist.
https://en.m.wikipedia.org/wiki/Clear_Linux_OS