CommunityCodierung & Entwicklunggithub.com

magmalake/mojo-data-performance

Schreibe schnellen Mojo-Daten-Code und finde heraus, wo die Zeit bleibt – von den pro-Zeile-Schleifen einer Pipeline bis hin zu Lesern, Schreibern und Codecs darunter – und ermittle Benchmark-Zahlen, die auch Nachprüfungen standhalten.

Was ist mojo-data-performance?

Diese Skill-Anleitung ist für Entwickler gedacht, die mit Mojo Datenpipelines optimieren und messen möchten. Sie erklärt, wo die Zeit in einer Datenpipeline typischerweise verbracht wird (Dekodieren, pro-Zeile-Arbeit, Wartezeit) und liefert konkrete, messungsbasierte Regeln, um Engpässe zu identifizieren und zu beseitigen. Unterstützte Plattformen sind insbesondere magmalake mit den Bibliotheken arrow-mlake, parquet.mojo, iceberg.mojo, dista und orca. Die Anleitung zeigt, wie man mit einfachen Mitteln (z.B. Messung der reinen Dekodierzeit als Baseline) herausfindet, ob die eigene Schleife oder das I/O-System der Flaschenhals ist. Jede Regel ist mit einem gemessenen Effekt aus realen MagmaLake-Benchmarks belegt, z.B. dass das einmalige Holen eines rohen Zeigers pro Batch die pro-Zeile-Arbeit von 1,40 s auf 0,78 s senkt. Darüber hinaus behandelt die Anleitung Regeln für die gesamte Pipeline (z.B. mehrere Lesevorgänge gleichzeitig mit `files_at_once`), Optimierungen für Leser, Schreiber und Codecs (Vermeidung von Arbeit, die später verworfen wird), sowie die korrekte Methodik für verlässliche Benchmarks (Median statt Mittelwert, keine JIT-Messung unter `mojo run`). Abgerundet wird sie durch Hinweise, wie man vor der Behauptung eines Speedups vorgehen sollte (Vergleich der Ergebnisse, Angabe beider Zahlen).

Funktioniert mit~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/magmalake/skills/tree/HEAD/mojo-data-performance

Installed? Explore more Codierung & Entwicklung skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

Die Zeit einer Datenpipeline verteilt sich auf drei Orte: Lesen und Dekodieren der Daten, die Arbeit an jeder Zeile und Wartezeit (auf Speicher, auf eine Sperre oder auf einen ausgelasteten Thread). Das Dekodieren ist normalerweise bereits in den Bibliotheken optimiert. Die Arbeit an jeder Zeile ist das, was Sie schreiben, und dort kostet eine nachlässige Schleife das 2–3-fache.

Der Großteil dieser Datei handelt von dem Code, der die Batches eines Scans konsumiert. Für den darunterliegenden Leser, Schreiber oder Codec und für die Ermittlung einer vertrauenswürdigen Zahl siehe die letzten beiden Abschnitte und deren Referenzen.

Zuerst messen, wo die Zeit bleibt

Sie haben selten eine zweite Implementierung (Ray, DuckDB, pyarrow) zum Vergleich. Nutzen Sie stattdessen den Scan selbst als Maßstab:

  1. Zeitmessung des Scans allein. Führen Sie denselben Scan mit denselben Spalten, demselben Filter und denselben ScanOptions aus und verwerfen Sie die Batches. Dies ist die reine Dekodier-Baseline: Keine Pipeline kann schneller sein als sie.
  2. Zeitmessung Ihrer Arbeit allein. Wickeln Sie den pro-Batch-Aufruf (fold, add oder was auch immer den Batch konsumiert) in perf_counter_ns() ein und addieren Sie die Zeiten.
  3. Vergleichen Sie sie. Auf einem Thread bedeutet eine Gesamtzeit innerhalb des etwa 1,5-fachen der reinen Dekodierzeit, dass die Arbeit billig ist; darüber hinaus ist die Arbeit der Engpass und eine Optimierung wert. Prüfen Sie bei vielen Threads auch, ob die Arbeit auf einem Thread läuft, während die Dekodierung auf vielen läuft.

Das gemessene Beispiel: Eine Gruppierung über 77M Fahrten dauerte 4,6 s auf einem Thread, davon 2,1 s für die pro-Zeile-Arbeit (45%). Auf 8 Threads dauerte es 2,6 s, davon dieselben 2,1 s für die Arbeit (82%), weil ein Thread jeden Batch konsumierte. references/profiling.md enthält das Rezept und seine Fallstricke.

Regeln für die pro-Zeile-Schleife

Jede Regel wird mit dem geliefert, was sie im dista-Shuffle-Benchmark (77M Zeilen, lokale Tabelle, ein Thread) eingespart hat. Der Code befindet sich in references/hot-loops.md.

RegelGemessener Effekt
Holen Sie den rohen Zeiger jeder Spalte einmal pro Batch, dann indizieren Sie den Zeiger. Indizieren Sie List oder Tuple[List, List] nicht innerhalb der Schleife.1,40 → 0,78 s für die gesamte pro-Zeile-Arbeit
Halten Sie alles, was ein Schlüssel akkumuliert, nebeneinander in einem Slot (Array of Structs), nicht in parallelen Arrays, sodass ein Update eine Cache-Zeile berührt.2,09 → 1,40 s, zusammen mit der nächsten Regel
Filtern Sie ohne Verzweigungen: Berechnen Sie jede Zeile, schreiben Sie sie an einen kept-Index und erhöhen Sie kept um Int(keep). Fügen Sie dann die behaltenen Zeilen in einer zweiten, engen Schleife hinzu.zusammen mit der Regel oben gemessen
Wenn Schlüssel kleine Ganzzahlen sind (zone × zone × hour ist 1,7M), indizieren Sie ein dichtes Array nach Schlüssel, anstatt ein Dict zu verwenden.nicht allein gemessen; beide Versionen verwendeten ein dichtes Array
Ändern Sie nicht die Reihenfolge von Gleitkomma-Additionen während der Optimierung. Dann muss das Ergebnis bitweise mit dem alten übereinstimmen, was eine kostenlose Korrektheitsprüfung ist.Jeder Schritt oben stimmte exakt überein

Regeln für die gesamte Pipeline

RegelGemessener Effekt
Lassen Sie nicht einen Thread konsumieren, was viele Threads dekodieren. Geben Sie jedem Arbeiter seinen eigenen Akkumulator und führen Sie sie am Ende zusammen, wie es die Map-Tasks von dista tun.16 Threads brachten eine Ein-Prozess-Gruppierung von 202 s auf 126 s auf AWS
Halten Sie mehrere Lesevorgänge gleichzeitig in der Luft, selbst mit einem Dekodier-Thread: ScanOptions.files_at_once (und overlap_io) in iceberg.mojo.6,6–6,9× auf einem Thread beim Lesen von S3
Laden Sie eine gemeinsame C-Bibliothek einmal pro Prozess und behalten Sie das Handle, niemals einmal pro Aufruf.Bei jeder Öffnung von zstd pro Aufruf verbrachte ein Scan 52% seiner Zeit in dyld; behoben: 2.687 → 262 ms
Cachen Sie alles, was Sie parsen und sich Byte für Byte wiederholt, wie Avro-Schemata in Manifesten.Die fixen Kosten der Manifest-Planung fielen um das 3,6-fache (113,7 → 31,9 µs)
Allokieren Sie nicht pro Datensatz: Dekodieren Sie in wiederverwendete Slots.Der Avro-Leser allokierte etwa 60 Mal pro Datensatz vor seinem Cursor
Kennen Sie die Obergrenze: Ab einigen Kernen ist ein Scan durch die Speicherbandbreite begrenzt, und mehr Threads helfen nicht. Lesen Sie stattdessen weniger Bytes.Ein Iceberg-Scan dauerte 35,8 ms auf einem Kern und 12,1 ms auf vier, etwa das 3-fache

references/case-studies.md erzählt jede dieser Geschichten mit dem, was das Problem verraten hat.

Leser, Schreiber und Codecs

Wenn das Dekodieren selbst der langsame Teil ist, geht es bei den Regeln hauptsächlich darum, keine Arbeit zu leisten, die später verworfen wird. Die Details und ihre Zahlen finden Sie in references/readers-and-codecs.md.

  • Überprüfen Sie, dass das Tor jedes schnellen Pfades von den Aufrufern erreicht werden kann, die es erreichen, und beweisen Sie durch Instrumentierung, dass es läuft. Ein vektorisierter Pfad, dessen Tor keine Listenspalte erreichen konnte, ließ eine 9 KiB-Spalte bei 43% eines Lesevorgangs zurück.
  • Berechnen Sie nicht, was eine spätere Entscheidung normalerweise verwirft. Das Erstellen eines Wörterbuchs nur um dann eine einfache Kodierung zu wählen, machte 67% eines int64-Spaltenschreibens aus; dessen Entfernung machte das Schreiben 3,3× schneller bei byteidentischer Ausgabe.
  • Entleihen Sie anstatt zu kopieren, und dekodieren Sie direkt in das endgültige Layout, wie Definitionsstufen in die Gültigkeits-Bitmap.
  • Arbeiten Sie in Läufen, wo die Daten sie haben: Bei 1% Nulls ersetzen etwa 250 memmoves etwa 25.000 Pro-Slot-Verzweigungen.
  • Wissen Sie, wann eine Stufe bereits an der Speicherbandbreite ist: Eine, die 18,3 MB in 1,2 ms ein- und auslagerte, etwa 30 GB/s, konnte nur übersprungen werden.
  • Finden Sie das langsamste Element, bevor Sie Threads hinzufügen. Die Aufteilung nach Spalten-Chunk begrenzte einen Lesevorgang auf das 1,39-fache, weil eine dicke Spalte die ganze Zeit lief.

Testen Sie die Optimierung ebenso wie das Ergebnis: Unterbrechen Sie sie absichtlich und prüfen Sie, dass ein Test fehlschlägt (references/testing-optimizations.md). Mojos eigene Fallstricke, wie Werte, die bei ihrer letzten Verwendung sterben, und MutUntrackedOrigin, finden Sie in references/mojo-hazards.md.

Eine Zahl ermitteln, der Sie vertrauen können

references/measurement.md enthält die vollständigen Regeln. Diejenigen, die Schlussfolgerungen in magmalake geändert haben:

  • Geben Sie den Median (p50) mit p90 daneben an, niemals den Mittelwert allein. Ein langsamer erster Aufruf verschob einen Mittelwert um 30% und den p50 überhaupt nicht.
  • Erstellen Sie den Benchmark im Voraus; messen Sie ihn niemals unter mojo run. Der JIT überhöhte einen Benchmark um das 1,9-fache.
  • Überprüfen Sie, dass die Maschine stillgehalten hat. Derselbe Benchmark dauerte 31 ms im Leerlauf und 89 ms neben 16 ausgelasteten Threads.
  • Nennen Sie die API und die Thread-Anzahl der Referenz. pq.read_table ist der Dataset-Scanner von pyarrow, nicht der Lesepfad von parquet-cpp, und die Thread-Einstellung von pyarrow verschob das Lesen einer Datei um das 2,9-fache.
  • Wenn zwei Zahlen nicht übereinstimmen, überprüfen Sie, ob sie auf die gleiche Weise erzeugt wurden, bevor Sie den Unterschied erklären.

Vor der Behauptung eines Speedups

  • Vergleichen Sie die Ergebnisse vorher und nachher, wo immer möglich (siehe die letzte Schleifenregel). Eine schnellere falsche Antwort ist schlimmer als eine langsame richtige.
  • Geben Sie beide Zahlen an sowie das Setup: Zeilen, Threads, lokale Festplatte oder S3 und ob der Cache warm war.
  • Sagen Sie, was übrig bleibt. Nach der Shuffle-Korrektur entfallen 325 ms der 777 ms immer noch auf das Kopieren von Spalten, und bei vielen Threads läuft die Arbeit immer noch auf einem Thread.

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

magmalake/dista

Write and run distributed Mojo programs with dista, magmalake's Ray-style task framework, where one compiled binary is the driver, the head and every worker. Use when writing or reviewing Mojo code that imports dista or mentions distributed Mojo, a remote task, `Task`/`Task2`/`TaskMany`/`TaskWith`, `ObjectRef`, `rt.remote`, `rt.get`, `Seq`, an actor with methods or checkpoints, a cluster, a head, workers, `DISTA_ROLE`/`DISTA_HEAD`, the `dista` command (status, drain, scale, timeline, stop), draining, autoscaling, or a dista runtime inside an orca pipeline step; and when a dista job hangs, is refused with a registry mismatch, or fails with a lost result. Pairs with Modular's mojo-syntax skill and magmalake's mojo-data-performance skill.

magmalake/magmalake-apps

Build a data application on magmalake as one Mojo project, with pipelines run by orca, a lake of Iceberg tables over S3, and a long-running flare web app reading them, then deploy it to AWS with `orca aws`. Use when starting a new magmalake project or laying one out (pixi, mojoshelf, `bin/`, `pipelines/`, `src/`); when a pipeline step writes Iceberg tables that an app reads; when writing a flare web app, console or dashboard over the lake; when running the project locally with Postgres and a local warehouse; or when deploying to AWS (`orca aws up`, `push`, `register`, `app`, `status`, `down`), setting up its hostnames, secrets, public bucket or costs. Pipelines in depth (partitions, keyed, sensors, dista tasks) are the `orca` skill's.

Verwandte Skills