Smart2Raw
Início Aplicações Como funciona Desempenho Comece

Comercial

Premium Licenciamento Investimento

Mais

Escopo técnico Citar Sobre
Fale conosco

Medido, não estimado.

Desempenho

Todo número desta página saiu de um programa que está no repositório, e todos podem ser reproduzidos por um comando que você roda. Onde um número podia ser conferido contra um laço ingênuo, o programa confere e aborta se houver divergência — um benchmark que imprime um número bonito que não sabe defender é pior que benchmark nenhum.

Sete formas de coluna, 4 milhões de elementos cada, os tempos de consulta, e o comando de cada tabela. Inclusive as linhas em que o Smart2Raw não ganha — elas estão aqui pelo mesmo motivo que as outras.

4231×contagem por faixa numa coluna u8, com o índice cumulativo — duas leituras de 2 KB
0,000034 mscount_gt(220) numa coluna que para em 200: o resumo de zona responde sem ler payload
31suítes de teste, 0 falhas, limpo sob ASan e UBSan
100.950checagens de fuzz diferencial contra uma referência ingênua, com sementes fixas

A mesma coluna em todos os formatos

Sete formas de coluna, 4 milhões de elementos cada, tamanhos em MB. S2R é a menor forma que a biblioteca sabe produzir para aquela forma de coluna.

A mesma coluna em sete formatos 4 milhões de elementos · a régua marca a entrada int64 4 milhões de elementos · a régua marca a entrada int64 Smart2Raw melhor alternativa clássica 0 10 20 30 40 megabytes entrada int64 · 30,52 MB A · uniforme 0..200 3,81 dicionário A · uniforme 0..200 — Smart2Raw 3,81 MB, dicionário 3,82 MB B · 12 valores distintos dicionário 1,91 B · 12 valores distintos — Smart2Raw 3,82 MB, dicionário 1,91 MB C · a mesma, ordenada RLE 0,00 C · a mesma, ordenada — Smart2Raw 0,06 MB, RLE 0,00 MB D · booleana 0/1 bitmap 0,48 D · booleana 0/1 — Smart2Raw 3,81 MB, bitmap 0,48 MB E · timestamps a cada 60 s 4,11 dicionário 41,01 E · timestamps a cada 60 s — Smart2Raw 4,11 MB, dicionário 41,01 MB F · u64 aleatório 30,52 dicionário 41,01 F · u64 aleatório — Smart2Raw 30,52 MB, dicionário 41,01 MB G · ids em 0..1e6 15,26 dicionário G · ids em 0..1e6 — Smart2Raw 15,26 MB, dicionário 17,03 MB

As duas barras vermelhas passaram da entrada: numa coluna de alta cardinalidade o dicionário guarda um dicionário do tamanho do dado, e devolve 41,01 MB para os 30,52 MB que recebeu. O Smart2Raw não tem barra assim — a classe mais larga que ele tem é a entrada. Repare também nas linhas B, C e D, onde o clássico ganha: elas estão no gráfico pelo mesmo motivo que as outras. E repare no que este gráfico não mede: ele conta bytes, e bytes são só um eixo — o outro está logo abaixo.

colunaint64S2RdicionárioRLEbitmapforma S2R
A · uniforme 0..200 (telemetria)30,523,813,8230,37pool plano
B · 12 valores distintos em 500..1150030,523,821,9127,97em blocos
C · a mesma, ORDENADA30,520,061,910,00em blocos
D · booleana 0/130,523,810,4815,260,48pool plano
E · timestamps a cada 60 s30,524,1141,0130,52em blocos
F · u64 aleatório (entropia máxima)30,5230,5241,0130,52pool plano
G · ids em 0..1e6 (alta cardinalidade)30,5215,2617,0330,52pool plano
cc -O2 -std=c11 -I include benchmarks/format_matrix.c -o format_matrix
./format_matrix 4000000

A coluna do dicionário é o piso teórico daquele formato — códigos de ceil(log2(k)) bits mais um dicionário de k valores a 8 bytes, sem somar nenhum overhead de implementação. É o número mais generoso que o formato pode produzir.

Leia a linha E e a linha F. Numa coluna de cardinalidade alta o dicionário fica do tamanho do dado, e o total vai a 41,01 MB contra uma linha de base de 30,52 MB — o formato deixou a coluna maior do que ela era. O RLE faz o mesmo em tudo que não está ordenado, uma corrida por valor. E o bitmap só existe quando há exatamente dois valores distintos.

O Smart2Raw não tem linha assim, e não por ajuste fino: ele classifica por amplitude, e a classe mais larga que tem é o int64 de entrada. A linha F é a prova — entropia máxima, e o resultado empata exatamente com a linha de base. O assert(s <= raw) roda dentro do laço, antes de cada linha ser impressa.

Bytes é só um eixo

A tabela acima responde "quanto ocupa". Ela não responde a pergunta que vem depois, que é a que decide o custo de rodar: o que dá para perguntar aos bytes sem antes transformá-los em outra coisa.

Este gráfico isola exatamente isso. A coluna é a mesma e os dois formatos ocupam praticamente o mesmo espaço — 11,44 MB contra 11,45 MB. Com os bytes empatados, o que sobra no desenho é só processamento.

12 milhões de elementos · os dois formatos ocupam o mesmo tamanho: 11,44 contra 11,45 MB 12 milhões de elementos · os dois formatos ocupam o mesmo tamanho: 11,44 contra 11,45 MB 12 milhões de elementos · os dois formatos ocupam o mesmo tamanho: 11,44 contra 11,45 MB Smart2Raw dicionário, implementado no seu melhor 0 2 4 6 8 milissegundos, um núcleo COUNT(x > 100) 0,60 0,63 empate COUNT(x > 100) — Smart2Raw 0,60 ms, dicionário, implementado no seu melhor 0,63 ms SUM(x) 0,44 7,52 SUM(x) — Smart2Raw 0,44 ms, dicionário, implementado no seu melhor 7,52 ms chegar a um kernel que não é SQL * 0,00 · já está contíguo 7,90 chegar a um kernel que não é SQL — Smart2Raw 0,00 ms, dicionário, implementado no seu melhor 7,90 ms

* Chegar a um kernel que não é SQL: um produto escalar quantizado, uma convolução, um matmul int8, um filtro de DSP — cada um precisa de um buffer contíguo na largura nativa. O formato de armazém precisa produzir esse buffer antes de começar. O nosso pool já é esse buffer.

O par aqui não é um espantalho. O dicionário está implementado no seu melhor, com os valores distintos ordenados — e isso faz o predicado virar uma comparação nos próprios códigos, sem decodificar nada. É por isso que o COUNT dá empate, e é por dizer isso que as outras duas linhas merecem crédito.

E onde perdemos está medido junto. Numa coluna de 12 valores distintos espalhados por uma amplitude larga, o par com códigos de 4 bits ganha: 11,44 MB contra 5,72 MB e 0,468 ms contra 0,326 ms — 2× maior e 1,44× mais lento do nosso lado. O piso é aritmético: 12 valores distintos precisam de log₂(12) = 3,58 bits, o par usa 4, e a menor classe nativa aqui é 8. Essa ausência é a decisão que compra os 7,9 ms da terceira linha acima, não um descuido.

O relatório completo — inclusive o que foi retratado de uma versão anterior dele — está em benchmarks/warehouse/WAREHOUSE_FORMAT_BENCH.md. Ele mede a 3.4.0, quando a mesma perda era de 4× e 3,2×; a fatoração afim da 3.5.0 é o que a reduziu.

Tempos de consulta

o quêantesdepoisde onde vem
count_gt em 8M elementos ordenados0,371 msabaixo do relógioa ordem é mantida, então a busca binária se aplica
count_gt(220) numa coluna que para em 2000,1435 ms0,000034 mso resumo de zona responde sem ler payload
contagem por faixa numa coluna u84231×o índice cumulativo: duas leituras de 2 KB que não crescem com o dado
12M elementos com passo22,89 MB / 1,033 ms11,44 MB / 0,468 mso passo comum é dividido, de forma exata
4M timestamps, forma errada x forma certa15,26 MB / 0,73 ms4,11 MB / 0,04 mso s2r_recommend() escolhe a forma em blocos

O índice cumulativo se paga em 11 consultas e depois não custa mais nada, porque 2 KB não crescem com a coluna. E ele recusa a resposta quando está desatualizado: o pool carrega uma época, toda escrita a incrementa, e o índice registra a época em que foi construído.

Contra um banco de verdade: o SQLite

As tabelas acima comparam contra formatos. Um formato é uma abstração, e quase ninguém tem uma em produção — quase todo mundo tem um SQLite. O benchmarks/maestro/ compara contra ele: mesmo CSV de entrada, um banco .db e um conjunto de .s2r escritos em disco de verdade, os dois abríveis depois.

o quêSQLite / int64Smart2Rawganhotipo
SUM nas 12 colunas que compactam2635 µs16,3 µs161×medido
COUNT com filtro3900 µs69,5 µs56×medido
tamanho em disco412,0 KB275,7 KB1,49× menorexato
memória residente934,8 KB342,2 KB2,73×exato
dado movido por varredura868,1 KB275,4 KB3,15×exato
vazão de SUM em SIMD2131 Mval/s7005 Mval/s3,29×medido
vazão do filtro por faixa1416 Mval/s1668 Mval/s1,18×medido
python benchmarks/maestro/smart2raw_bench.py os_seus_dados.csv

Python aqui é só o maestro: usa a biblioteca padrão para o SQLite e chama os kernels C reais por ctypes — o motor nativo é compilado do s2r_shim.c na primeira execução. Sem compilador, ele ainda imprime memória, disco e dado movido, que são contagens exatas, e pula as duas seções cronometradas. Colunas que de fato precisam de 64 bits, ou que são ponto flutuante, aparecem com 0% de propósito.

O que esses 161× não são. O SQLite entrega SQL, índices, transações e durabilidade; aqui ele está fazendo um trabalho só — varrer e agregar colunas inteiras, sem índice — contra uma coluna feita exatamente para esse trabalho. O número mede a distância entre um motor genérico que interpreta bytecode linha a linha e um laço SIMD sobre bytes contíguos. Não é um defeito do SQLite; é o preço da generalidade, e ele é enorme justamente por isso.

E o dado é pequeno de propósito — o CSV de demonstração tem uns 670 KB, então tudo cabe em cache e o que domina é o custo por linha do interpretador. Num dado muito maior a razão cronometrada não se mantém; o que se mantém são as três linhas marcadas como exatas, porque elas são contagem de bytes, não relógio: 1,49× em disco, 2,73× em memória, 3,15× de dado movido por varredura.

Repare no 1,18×. O filtro por faixa quase não melhora, e a linha está aqui por isso. SUM em u8 processa oito valores por lane onde o int64 processa um, e salta para 3,29×; o filtro já era barato por elemento e o gargalo dele é outro. Um ganho de espaço não vira ganho de tempo por decreto — vira quando a operação era limitada por memória, e essa linha é o contraexemplo dentro da nossa própria tabela.

Meça no seu próprio navegador

A demonstração da página inicial cronometra as mesmas consultas no seu dado, no seu navegador, com aquecimento e argumento variando para que nada possa ser cacheado — e imprime o resultado de cada caminho, para você ver que eles concordam.

Ela também vai te mostrar onde o ganho de espaço não vira ganho de tempo: numa coluna pequena o bastante para caber no cache, ler um quarto dos bytes não leva um quarto do tempo. Aumente o número de elementos e a diferença aparece.

Como tudo isso é conferido

bash scripts/build_and_test.sh

Essa suíte de fuzz existe por causa de um defeito real que ela encontrou: uma coluna sem sinal que cruzava 2^63 devolvia valores truncados na camada em blocos, sem erro, sem aviso e com CRC válido. Vinte e cinco suítes de casos escolhidos não pegaram. Está corrigido, e o caso mínimo {1, UINT64_MAX} hoje é um teste fixo.

Por onde seguir

Onde a troca está

A linha em que o dicionário ganha, dita sem rodeio, com o número.

Escopo técnico →

Reproduza tudo

Um clone e um comando. As sementes do fuzz são fixas de propósito.

Como citar e reproduzir →

Meça o seu dado

A demonstração cronometra as mesmas consultas na sua coluna.

Ir para a demonstração →