magmalake/mojo-data-performance

Escreva código de dados rápido em Mojo e descubra onde seu tempo é gasto, desde os loops por linha de um pipeline até os leitores, escritores e codecs subjacentes, e obtenha números de benchmark confiáveis. Use ao escrever ou revisar código Mojo que itera sobre lotes de registros Arrow, varreduras Parquet ou Iceberg (arrow-mlake, parquet.mojo, iceberg.mojo, dista, orca), ou que filtra, agrupa, junta ou agrega linhas; ao otimizar um parser, leitor, escritor, codificador ou codec; ao escrever um benchmark ou comparar com pyarrow, parquet-cpp, arrow-rs ou Ray; quando um número parecer bom demais ou variar entre execuções; ou antes de alegar um ganho de desempenho. Combina com a habilidade mojo-syntax da Modular.

O que é mojo-data-performance?

Esta habilidade fornece diretrizes baseadas em medições para escrever código de dados Mojo de alto desempenho, com foco em identificar e eliminar gargalos em pipelines de dados. Ela orienta o desenvolvedor a medir onde o tempo é gasto, começando pela varredura (scan) como linha de base, e oferece regras práticas para otimizar loops por linha, pipelines completos e componentes de baixo nível como leitores, escritores e codecs. Cada regra é acompanhada de efeitos mensurados, como redução de tempo de 1,40s para 0,78s ao acessar ponteiros brutos de colunas, ou eliminação de overhead de 52% em dyld ao carregar bibliotecas C compartilhadas uma vez por processo. A habilidade também ensina como obter números de benchmark confiáveis — usando mediana e p90, compilando benchmarks separadamente, e verificando isolamento da máquina — e como evitar alegações enganosas de speedup, comparando resultados exatos. É compatível com as bibliotecas magmalake (arrow-mlake, parquet.mojo, iceberg.mojo, dista, orca) e com ambientes Mojo, e se complementa com a habilidade mojo-syntax para práticas seguras de codificação.

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

Installed? Explore more Programação e Desenvolvimento skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

O tempo de um pipeline de dados vai para três lugares: ler e decodificar os dados, o trabalho feito em cada linha e a espera (no armazenamento, em um bloqueio ou em uma thread ocupada). A decodificação geralmente já é ajustada nas bibliotecas. O trabalho em cada linha é o que você escreve, e é onde um loop descuidado custa 2–3×.

A maior parte deste arquivo é sobre o código que consome os lotes de uma varredura. Para o leitor, escritor ou codec subjacente, e para obter um número em que você possa confiar, veja as duas últimas seções e suas referências.

Primeiro, meça onde o tempo é gasto

Raramente você tem uma segunda implementação (Ray, DuckDB, pyarrow) para comparar. Use a própria varredura como referência:

  1. Meça a varredura sozinha. Execute a mesma varredura, com as mesmas colunas, filtro e ScanOptions, e descarte os lotes. Esta é a linha de base de decodificação pura: nenhum pipeline pode ser mais rápido que ela.
  2. Meça seu trabalho isoladamente. Envolva a chamada por lote (fold, add, ou qualquer coisa que consuma o lote) com perf_counter_ns() e some os tempos.
  3. Compare-os. Em uma thread, um tempo total dentro de cerca de 1,5× do tempo de decodificação pura significa que o trabalho é barato; acima disso, o trabalho é o gargalo e vale a pena otimizar. Em muitas threads, verifique também se o trabalho roda em uma thread enquanto a decodificação roda em muitas.

O exemplo medido: um group-by em 77M viagens levou 4,6 s em uma thread, dos quais 2,1 s foram o trabalho por linha (45%). Em 8 threads levou 2,6 s, dos quais os mesmos 2,1 s foram o trabalho (82%), porque uma thread consumiu cada lote. references/profiling.md tem a receita e suas armadilhas.

Regras para o loop por linha

Cada regra vem com o que economizou no benchmark de shuffle do dista (77M linhas, tabela local, uma thread). O código está em references/hot-loops.md.

RegraEfeito medido
Pegue o ponteiro bruto de cada coluna uma vez por lote e depois indexe o ponteiro. Não indexe List ou Tuple[List, List] dentro do loop.1,40 → 0,78 s para todo o trabalho por linha
Mantenha tudo que uma chave acumula lado a lado em um único slot (array de structs), não em arrays paralelos, para que uma atualização toque uma única linha de cache.2,09 → 1,40 s, junto com a próxima regra
Filtre sem ramificações: compute cada linha, escreva-a em um índice kept e avance kept por Int(keep). Depois, adicione as linhas mantidas em um segundo loop enxuto.medido junto com a regra acima
Quando as chaves são inteiros pequenos (zone × zone × hour é 1,7M), indexe um array denso pela chave em vez de usar um Dict.não medido isoladamente; ambas as versões usaram array denso
Não altere a ordem das adições de ponto flutuante enquanto otimiza. Assim, a resposta deve corresponder exatamente à antiga, bit a bit, o que é uma verificação de correção gratuita.cada etapa acima correspondeu exatamente

Regras para o pipeline inteiro

RegraEfeito medido
Não deixe que uma thread consuma o que muitas threads decodificam. Dê a cada worker seu próprio acumulador e mescle-os ao final, como as tarefas de mapa do dista fazem.16 threads levaram um group-by de um processo de 202 s para apenas 126 s na AWS
Mantenha várias leituras em andamento, mesmo com uma thread de decodificação: ScanOptions.files_at_once (e overlap_io) no iceberg.mojo.6,6–6,9× em uma thread lendo do S3
Carregue uma biblioteca C compartilhada uma vez por processo e mantenha o handle, nunca uma vez por chamada.com zstd aberto em cada chamada, uma varredura gastou 52% do tempo em dyld; corrigido, 2.687 → 262 ms
Coloque em cache qualquer coisa que você analise e que se repita byte a byte, como esquemas Avro em manifestos.o custo fixo do planejamento de manifesto caiu 3,6× (113,7 → 31,9 µs)
Não aloque por registro: decodifique em slots reutilizados.o leitor Avro alocou cerca de 60 vezes por registro antes do cursor
Conheça o teto: além de alguns núcleos, uma varredura é limitada pela largura de banda da memória, e mais threads não ajudarão. Leia menos bytes.uma varredura Iceberg levou 35,8 ms em um núcleo e 12,1 ms em quatro, cerca de 3×

references/case-studies.md conta cada uma dessas histórias, com o que revelou o problema.

Leitores, escritores e codecs

Quando a própria decodificação é a parte lenta, as regras são principalmente sobre não fazer trabalho que é descartado. Os detalhes e seus números estão em references/readers-and-codecs.md.

  • Verifique se cada porta de entrada do caminho rápido pode ser atendida pelos chamadores que o alcançam, e prove por instrumentação que ele é executado. Um caminho vetorizado cuja porta nenhuma coluna de lista poderia atender deixou uma coluna de 9 KiB em 43% de uma leitura.
  • Não compute o que uma decisão posterior geralmente descarta. Construir um dicionário apenas para escolher a codificação simples foi 67% de uma escrita de coluna int64; removê-lo tornou a escrita 3,3× mais rápida com saída idêntica byte a byte.
  • Tome emprestado em vez de copiar, e decodifique diretamente no layout final, como níveis de definição no bitmap de validade.
  • Trabalhe em rajadas onde os dados as têm: com 1% de nulos, cerca de 250 memmoves substituem cerca de 25.000 ramificações por slot.
  • Saiba quando um estágio já está na largura de banda da memória: um que moveu 18,3 MB para dentro e para fora em 1,2 ms, cerca de 30 GB/s, só poderia ser pulado.
  • Encontre o item mais lento antes de adicionar threads. Dividir por bloco de coluna limitou uma leitura a 1,39×, porque uma coluna pesada rodou o tempo todo.

Teste a otimização assim como a resposta: quebre-a de propósito e verifique se um teste falha (references/testing-optimizations.md). As próprias armadilhas do Mojo, como valores morrendo em seu último uso e MutUntrackedOrigin, estão em references/mojo-hazards.md.

Obtendo um número em que você pode confiar

references/measurement.md tem as regras completas. As que mudaram conclusões no magmalake:

  • Relate a mediana (p50) com o p90 ao lado, nunca apenas a média. Uma única chamada inicial lenta moveu a média em 30% e o p50 em nada.
  • Construa o benchmark com antecedência; nunca o cronometre sob mojo run. O JIT inflou um benchmark em 1,9×.
  • Verifique se a máquina ficou parada. O mesmo benchmark levou 31 ms ocioso e 89 ms ao lado de 16 threads ocupadas.
  • Nomeie a API de referência e a contagem de threads. pq.read_table é o scanner de dataset do pyarrow, não o caminho de leitura do parquet-cpp, e a configuração de threads do pyarrow moveu a leitura de um arquivo em 2,9×.
  • Quando dois números discordarem, verifique se foram produzidos da mesma forma antes de explicar a diferença.

Antes de alegar um ganho de velocidade

  • Compare as respostas antes e depois, exatamente onde puder (veja a última regra do loop). Uma resposta errada mais rápida é pior que uma resposta certa lenta.
  • Relate ambos os números e a configuração: linhas, threads, disco local ou S3, e se o cache estava quente.
  • Diga o que sobrou. Após a correção do shuffle, 325 ms dos 777 ms ainda são cópia de colunas, e em muitas threads o trabalho ainda está em uma 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.

Habilidades Relacionadas