← ONTO GitLab

Benchmark comparativo

ONTO (OntoEngine) confrontato con tre alternative generaliste su un carico di lavoro tipico del paradigma: record con proprietà booleane trivalenti, query per congiunzione di proprietà, distribuzione dei valori e ranking per punteggio.

Metodologia

  • Dataset: 8 proprietà booleane trivalenti (febbre, tosse, dispnea, perdita_olfatto, mal_di_testa, dolori_muscolari, congiuntivite, stanchezza), distribuzione 55% V / 30% F / 15% N, generato in modo riproducibile con seed fisso (StdRng::seed_from_u64(42)).
  • Dimensioni: N ∈ {1 000, 10 000, 100 000, 500 000}.
  • Query:
    • Q1 — scan singolo: febbre = V;
    • Q2 — AND su 3 proprietà: febbre = V ^ tosse = V ^ perdita_olfatto = V;
    • Q3 — AND su 6 proprietà: profilo "COVID" (selettività massima);
    • Q4 — AND trivalente (compressione: V^V=V, F^N=N^F=N, altrimenti F) tra febbre e tosse, materializzazione lazy del risultato;
    • Q5 — distribuzione V/F/N di una proprietà (conteggio aggregato);
    • Q7 — top-20 per peso epistemico (V=2, N=1, F=0);
    • Q8 — scan a soglia sul punteggio totale, con classificazione V/N/F.
  • Motori:
    • onto — questo motore (indice bitset trivalente in memoria);
    • sqlite — rusqlite con feature bundled (SQLite incorporato, un indice B-tree per proprietà, nessun server richiesto);
    • naive_scan — Vec<HashMap> senza indici, baseline "applicativo che filtra a mano";
    • sled_kv — KV embedded puro Rust, usato come proxy di Redis (vedi nota).

Riprodurre:

sh
cargo run -p onto-cli --release -- bench --sizes 1000,10000,100000,500000 --out risultati_bench.csv

Ogni query è misurata una volta per esecuzione. I tempi riportati sono la mediana di 7 esecuzioni dello sweep sulla stessa macchina.

Risultati a N = 500 000 (tempi in ms)

Query onto sqlite naive_scan sled_kv sqlite/onto naive/onto sled/onto
costruzione 1789.2 3507.5 192.5 484.3 2.0× 0.11× 0.27×
Q1 scan singolo 3.87 22.36 48.39 8.17 5.8× 13× 2.1×
Q2 AND 3 prop 1.40 57.27 50.57 69.32 41× 36× 49×
Q3 AND 6 prop 1.10 55.86 43.87 88.73 51× 40× 81×
Q4 AND trivalente (lazy) 3.39 188.4 56.79 157.1 56× 17× 46×
Q5 distribuzione 0.017 15.79 55.44 11.02 ~950× ~3320× ~660×
Q7 top-20 8.36 155.9 37.91 1089.0 19× 4.5× 130×
Q8 scan a soglia 3.76 408.6 38.42 589.5 109× 10× 157×

Risultati completi

Tempi grezzi (ms) per tutte le dimensioni testate.

N = 1 000

Motore costruzione Q1 Q2 Q3 Q4 Q5 Q7 Q8
onto 3.61 0.011 0.011 0.0085 0.017 0.0002 0.022 0.0079
sqlite 4.47 0.075 0.107 0.105 0.348 0.053 0.362 0.852
naive_scan 0.296 0.061 0.071 0.063 0.074 0.030 0.028 0.032
sled_kv 1.50 0.017 0.079 0.121 0.159 0.017 0.830 0.208

N = 10 000

Motore costruzione Q1 Q2 Q3 Q4 Q5 Q7 Q8
onto 37.33 0.095 0.032 0.029 0.072 0.0005 0.113 0.066
sqlite 51.14 0.461 0.943 0.927 2.90 0.308 2.94 7.08
naive_scan 3.33 0.807 0.880 0.753 0.889 0.770 0.470 0.424
sled_kv 9.65 0.116 0.680 1.15 1.49 0.161 9.08 2.40

N = 100 000

Motore costruzione Q1 Q2 Q3 Q4 Q5 Q7 Q8
onto 350.3 0.800 0.275 0.229 0.671 0.0029 1.20 0.642
sqlite 623.8 4.30 9.91 10.20 33.03 2.85 29.89 81.03
naive_scan 36.54 10.65 10.15 8.76 10.39 9.15 6.32 5.73
sled_kv 90.38 1.17 9.29 13.97 20.05 1.70 137.4 51.47

N = 500 000

Motore costruzione Q1 Q2 Q3 Q4 Q5 Q7 Q8
onto 1789.2 3.87 1.40 1.10 3.39 0.017 8.36 3.76
sqlite 3507.5 22.36 57.27 55.86 188.4 15.79 155.9 408.6
naive_scan 192.5 48.39 50.57 43.87 56.79 55.44 37.91 38.42
sled_kv 484.3 8.17 69.32 88.73 157.1 11.02 1089.0 589.5

Memoria

Il motore non conserva una copia dei record: li ricostruisce dall'indice, che tiene tre bit per proprietà e per record, più due byte di peso epistemico. Con 8 proprietà trivalenti un record occupa circa 6 byte: 500 000 record stanno in meno di 3 MB (misura riproducibile con cargo run --release --example misura_memoria). Numeri e testi degli elementi, quando presenti, occupano memoria in più nel loro indice ordinato.

Tendenze

  • ONTO è il motore più veloce su tutte le query a ogni N testato. Il vantaggio cresce con N e, soprattutto, con il numero di proprietà congiunte: da ~10× (Q2, N=1 000) a 41-51× (Q2-Q3, N=500 000). Le congiunzioni su bitset sono O(n/64) a livello di parola intera, mentre SQLite deve incrociare più indici B-tree e gli altri motori scandiscono record per record.
  • Q5 (distribuzione) è il divario più ampio: ~950× a N=500 000. Il conteggio V/F/N è un singolo count_ones() sui bitset già in memoria; gli altri motori eseguono una scansione completa o una query aggregata su tabella.
  • Q7 (ranking) penalizza soprattutto sled_kv (~130× più lento): senza uno score posizionale nativo, ogni query deve materializzare gli insiemi V/N di tutte le 8 proprietà e ricalcolare il punteggio lato applicativo — lo stesso costo che avrebbe un client Redis con SINTER/SMEMBERS ripetuti.
  • Q1 è il caso dove il KV si avvicina di più (2.1×): un singolo lookup per chiave è quasi ottimale anche per un KV generico. Il vantaggio di ONTO emerge appena la query combina più proprietà o richiede ranking/soglie.
  • La costruzione di ONTO costa più di naive_scan e sled_kv (indicizzare 8 bitset per record), ma resta più veloce di SQLite (8 indici B-tree) e viene ripagata interamente dal costo delle query: a N=500 000 l'intero build è recuperato da poche decine di Q2-Q3.

Nota sulla sostituzione di Redis

Redis non è disponibile in questo ambiente (nessun redis-server nel PATH), quindi il confronto "NoSQL/KV" usa sled, un motore KV embedded puro Rust che non richiede alcun processo esterno. sled non è architetturalmente identico a Redis (nessuna rete, nessuna replica, nessun TTL), ma condivide il tratto rilevante per questo confronto: nessun bitset posizionale nativo, solo lookup per chiave — la stessa categoria di struttura dati che userebbe un client Redis con SADD/SINTER. Per un confronto con Redis reale in produzione, valutare che il costo di rete e di materializzazione degli insiemi lato client si sommerebbe a quello già misurato per sled_kv.

Micro-benchmark di regressione (cargo bench)

Al contrario dello sweep sopra (pensato per confrontare motori diversi su dataset grandi), benches/benchmark.rs misura le operazioni core in isolamento, per intercettare regressioni tra modifiche. Numeri da una sessione di misura su questo repository:

Operazione Tempo
TrivalentValue::and/or/not ~0.6-0.9 ns
TrivalentIndex::and (2 proprietà, N=1 000) ~1.67 µs
TrivalentIndex::and (2 proprietà, N=100 000) ~88.7 µs
OntoEngine inserimento (N=1 000, 3 proprietà) ~0.96 ms
OntoEngine inserimento (N=10 000, 3 proprietà) ~10.2 ms
search_profile (2 proprietà, N=100 000) ~226 µs
top_k(20) (N=100 000) ~260 µs

Report HTML completo in target/criterion/report/index.html dopo cargo bench.

Fonte: onto-rs/docs/BENCHMARK.md