magmalake/mojo-data-performance

Écrire du code Mojo performant pour les données et comprendre où va le temps d'exécution, des boucles par ligne d'un pipeline jusqu'aux lecteurs, rédacteurs et codecs sous-jacents, et obtenir des mesures de performance fiables et reproductibles.

Qu'est-ce que mojo-data-performance ?

Ce guide est conçu pour les développeurs qui écrivent du code Mojo traitant des données tabulaires (Arrow, Parquet, Iceberg) et qui souhaitent optimiser ses performances de manière mesurée et rigoureuse. Il fournit des règles concrètes, chacune accompagnée d'un gain de performance réel mesuré (par exemple, de 1,40 s à 0,78 s pour le travail par ligne), pour éviter les optimisations aveugles. Les domaines couverts incluent la mesure du temps de décodage pur, l'optimisation des boucles internes (accès aux pointeurs bruts, organisation des données en structures de tableaux, filtrage sans branchement), les goulots d'étranglement du pipeline entier (comme l'équilibrage de charge entre threads), et l'optimisation des lecteurs et codecs (éviter les calculs inutiles, travailler par lots). Une section entière est dédiée à la prise de mesures fiables (médiane plutôt que moyenne, compilation séparée, contrôle de l'environnement) et aux pièges spécifiques de Mojo. Le guide est particulièrement utile pour les développeurs utilisant les bibliothèques arrow-mlake, parquet.mojo, iceberg.mojo, dista ou orca, ou comparant leurs performances à pyarrow, parquet-cpp, arrow-rs ou Ray. Il fonctionne en tandem avec le skill 'Modular/mojo-syntax'.

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

Installed? Explore more Développement et programmation skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Le temps d'un pipeline de données se répartit en trois endroits : la lecture et le décodage des données, le travail effectué sur chaque ligne, et l'attente (du stockage, d'un verrou, ou d'un thread occupé). Le décodage est généralement déjà optimisé dans les bibliothèques. Le travail sur chaque ligne est ce que vous écrivez, et c'est là qu'une boucle négligente coûte 2 à 3 fois plus.

La majeure partie de ce fichier concerne le code qui consomme les lots d'un scan. Pour le lecteur, le rédacteur ou le codec sous-jacent, et pour obtenir un nombre fiable, voir les deux dernières sections et leurs références.

D'abord, mesurer où va le temps

Vous avez rarement une seconde implémentation (Ray, DuckDB, pyarrow) à laquelle vous comparer. Utilisez le scan lui-même comme étalon à la place :

  1. Chronométrez le scan seul. Lancez le même scan, avec les mêmes colonnes, filtre et ScanOptions, et jetez les lots. C'est la référence de décodage seul : aucun pipeline ne peut être plus rapide que cela.
  2. Chronométrez votre travail seul. Encapsulez l'appel par lot (fold, add, ou tout ce qui consomme le lot) dans perf_counter_ns() et additionnez les temps.
  3. Comparez-les. Sur un seul thread, un temps total inférieur à environ 1,5 fois le temps de décodage seul signifie que le travail est bon marché ; au-dessus, le travail est le goulot d'étranglement et mérite d'être optimisé. Sur plusieurs threads, vérifiez également si le travail s'exécute sur un seul thread pendant que le décodage s'exécute sur plusieurs.

L'exemple mesuré : un group-by sur 77 millions de trajets a pris 4,6 s sur un thread, dont 2,1 s pour le travail par ligne (45 %). Sur 8 threads, il a pris 2,6 s, dont les mêmes 2,1 s pour le travail (82 %), car un seul thread consumait chaque lot. references/profiling.md contient la recette et ses pièges.

Règles pour la boucle par ligne

Chaque règle est accompagnée de ce qu'elle a économisé dans le benchmark de brassage dista (77 millions de lignes, table locale, un thread). Le code se trouve dans references/hot-loops.md.

RègleEffet mesuré
Prenez le pointeur brut de chaque colonne une fois par lot, puis indexez le pointeur. N'indexez pas List ou Tuple[List, List] à l'intérieur de la boucle.1,40 → 0,78 s pour l'ensemble du travail par ligne
Gardez tout ce qu'une clé accumule côte à côte dans un seul emplacement (tableau de structures), et non dans des tableaux parallèles, afin qu'une mise à jour touche une seule ligne de cache.2,09 → 1,40 s, avec la règle suivante
Filtrez sans branche : calculez chaque ligne, écrivez-la à un index kept, et avancez kept de Int(keep). Ajoutez ensuite les lignes conservées dans une seconde boucle serrée.mesuré avec la règle ci-dessus
Lorsque les clés sont de petits entiers (zone × zone × heure est 1,7 M), indexez un tableau dense par clé au lieu d'utiliser un Dict.non mesuré seul ; les deux versions utilisaient un tableau dense
Ne modifiez pas l'ordre des additions en virgule flottante pendant l'optimisation. Ainsi, la réponse doit correspondre bit pour bit à l'ancienne, ce qui constitue une vérification de correction gratuite.chaque étape ci-dessus correspondait exactement

Règles pour l'ensemble du pipeline

RègleEffet mesuré
Ne laissez pas un seul thread consommer ce que plusieurs threads décodent. Donnez à chaque worker son propre accumulateur et fusionnez-les à la fin, comme le font les tâches map de dista.16 threads n'ont fait passer un group-by mono-processus que de 202 s à 126 s sur AWS
Gardez plusieurs lectures en vol, même avec un seul thread de décodage : ScanOptions.files_at_once (et overlap_io) dans iceberg.mojo.6,6 à 6,9 fois sur un thread lisant depuis S3
Chargez une bibliothèque C partagée une fois par processus et conservez le handle, jamais une fois par appel.avec zstd ouvert à chaque appel, un scan passait 52 % de son temps dans dyld ; corrigé, 2 687 → 262 ms
Mettez en cache tout ce que vous analysez et qui se répète octet par octet, comme les schémas Avro dans les manifestes.le coût fixe de la planification des manifestes a chuté de 3,6 fois (113,7 → 31,9 µs)
N'allouez pas par enregistrement : décodez dans des emplacements réutilisés.le lecteur Avro allouait environ 60 fois par enregistrement avant son curseur
Connaissez le plafond : au-delà de quelques cœurs, un scan est limité par la bande passante mémoire, et davantage de threads n'aideront pas. Lisez plutôt moins d'octets.un scan Iceberg a pris 35,8 ms sur un cœur et 12,1 ms sur quatre, soit environ 3 fois

references/case-studies.md raconte chacune de ces histoires, avec ce qui a révélé le problème.

Lecteurs, rédacteurs et codecs

Lorsque le décodage lui-même est la partie lente, les règles concernent principalement le fait de ne pas effectuer un travail qui sera de toute façon jeté. Les détails et leurs nombres sont dans references/readers-and-codecs.md.

  • Vérifiez que la porte de chaque chemin rapide peut être franchie par les appelants qui l'atteignent, et prouvez par instrumentation qu'il s'exécute. Un chemin vectorisé dont la porte aucune colonne de liste ne pouvait franchir a laissé une colonne de 9 Kio à 43 % d'une lecture.
  • Ne calculez pas ce qu'une décision ultérieure jettera généralement. Construire un dictionnaire uniquement pour choisir un encodage simple représentait 67 % d'une écriture de colonne int64 ; sa suppression a rendu l'écriture 3,3 fois plus rapide avec une sortie identique octet par octet.
  • Empruntez au lieu de copier, et décodez directement dans la disposition finale, comme les niveaux de définition dans le bitmap de validité.
  • Travaillez par séquences là où les données en ont : à 1 % de valeurs nulles, environ 250 memmove remplacent environ 25 000 branches par emplacement.
  • Sachez quand une étape est déjà à la bande passante mémoire : une étape qui déplaçait 18,3 Mo en entrée et sortie en 1,2 ms, soit environ 30 Go/s, ne pouvait qu'être ignorée.
  • Trouvez l'élément le plus lent avant d'ajouter des threads. La division par bloc de colonnes a plafonné une lecture à 1,39 fois, car une colonne volumineuse s'exécutait tout le temps.

Testez l'optimisation ainsi que la réponse : cassez-la intentionnellement et vérifiez qu'un test échoue (references/testing-optimizations.md). Les pièges propres à Mojo, tels que les valeurs qui meurent à leur dernière utilisation et MutUntrackedOrigin, sont dans references/mojo-hazards.md.

Obtenir un nombre fiable

references/measurement.md contient les règles complètes. Celles qui ont changé les conclusions chez magmalake :

  • Rapportez la médiane (p50) avec le p90 à côté, jamais la moyenne seule. Un premier appel lent a déplacé la moyenne de 30 % et la p50 pas du tout.
  • Construisez le benchmark à l'avance ; ne le chronométrez jamais sous mojo run. Le JIT a gonflé un benchmark de 1,9 fois.
  • Vérifiez que la machine est restée stable. Le même benchmark a pris 31 ms au repos et 89 ms à côté de 16 threads occupés.
  • Nommez l'API de référence et le nombre de threads. pq.read_table est le scanneur de datasets de pyarrow, pas le chemin de lecture de parquet-cpp, et le réglage des threads de pyarrow a déplacé la lecture d'un fichier de 2,9 fois.
  • Lorsque deux nombres diffèrent, vérifiez qu'ils ont été produits de la même manière avant d'expliquer la différence.

Avant de revendiquer une accélération

  • Comparez les réponses avant et après, exactement là où vous le pouvez (voir la dernière règle sur les boucles). Une réponse fausse plus rapide est pire qu'une réponse lente correcte.
  • Rapportez les deux nombres et la configuration : lignes, threads, disque local ou S3, et si le cache était chaud.
  • Dites ce qu'il reste. Après la correction du brassage, 325 ms des 777 ms servent encore à copier des colonnes, et sur plusieurs threads, le travail s'effectue encore sur un seul 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.

Skills associés