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) trafebbreetosse, 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.
- Q1 — scan singolo:
- Motori:
onto— questo motore (indice bitset trivalente in memoria);sqlite—rusqlitecon featurebundled(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:
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 conSINTER/SMEMBERSripetuti. - 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_scanesled_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.