- Combinaison de la programmation logique Datalog et de l’efficacité de Rust pour détailler le développement d’un moteur Datalog interactif à la fois simple, agréable à utiliser et pensé pour les performances
- Ajout et extension en temps réel de règles (
rule) et de faits (fact) dans un environnement CLI intuitif, avec chargement de gros volumes de données, saisie dynamique de règles et performances de requête rapides
- Parsing, représentation des données et évaluation des règles (planning/evaluation) expliqués étape par étape avec du code Rust pour apprendre une méthode d’implémentation concrète
- Démarrage depuis une implémentation simple non optimisée puis amélioration progressive des performances et de la structure, permettant aussi d’apprendre des logiques avancées comme le traitement parallèle des données et l’optimisation des jointures
- Exécution réelle de cas d’analyse de programmes sur de grands jeux de données (nullability, aliasing, etc.), avec partage de retours sur les problèmes de performance et de mémoire, l’optimisation des requêtes et l’amélioration des plans de jointure
Introduction : expérimenter la programmation logique Datalog en Rust
- Pendant la période du Memorial Day, divers ateliers pratiques et discussions autour de la programmation logique (dont Datalog) ont eu lieu à la conférence Minnowbrook
- Les outils Datalog existants (Soufflé, ctdal, etc.) ont montré des limites en usage réel, extensibilité et performances, ce qui a mis en évidence le besoin d’un outil plus pratique
- L’auteur a donc décidé d’implémenter lui-même en Rust un interpréteur Datalog (
datatoad) réunissant simplicité, ergonomie et performances
- Objectif du projet : charger rapidement de gros volumes de données depuis le CLI, ajouter ou modifier des règles dynamiquement, vérifier les résultats en temps réel et conserver de bonnes performances de requête
Concepts de base de Datalog
- Datalog repose sur des énoncés logiques sous forme de règles (
Head :- Body) et déduit automatiquement tous les faits dérivables à partir des fact et rule donnés
- Une règle (par ex.
tri(a, b, c) :- edge(a, b), edge(b, c), edge(a, c)) est composée de variables et de littéraux
- Un fact est une valeur vraie sans condition (par ex.
edge(1, 2) :- .)
- Points forts de Datalog : l’ajout de règles augmente l’ensemble des informations déductibles (monotonicité), et le résultat final est identique quel que soit l’ordre des règles et des faits (convergence)
- En Rust, les règles et les
fact sont représentés par des structures Atom / Rule / Term, et les ensembles de fact sont gérés par relation
Conception des structures clés
Représentation des données
- Les fact sont d’abord conçus comme
Vec<String>, et les ensembles de fact comme BTreeMap<String, Vec<Fact>>
- Pour optimiser les gros volumes de données, une structure columnar (orientée colonnes) est introduite afin de minimiser l’overhead d’allocation
- FactContainer : ensemble de
fact triés et dédupliqués, avec une structure append only
- FactLSM : gestion de plusieurs couches de
FactContainer selon une approche LSM (Log-structured merge-tree) pour améliorer l’efficacité de l’insertion, du tri et de la fusion
- FactSet : gestion du cycle de vie des
fact (nouvellement ajoutés, récemment déduits, stabilisés) pour éviter les calculs en double et le gaspillage mémoire inutile
Application des règles et inférence
- Chaque règle génère un
JoinPlan, puis l’inférence s’effectue par merge join selon un ordre de colonnes et une combinaison de clés appropriés
merge join : les colonnes clés de chaque atome du corps sont triées, puis de nouveaux fact ne sont déduits que lorsque les clés de jointure correspondent, ce qui maximise les performances
- La structure
stable/recent/to_add de FactSet permet de séparer les fact déjà déduits des nouveaux et d’éviter les recalculs inutiles (évaluation différentielle)
- Boucle
.update() : toutes les règles sont appliquées de manière répétée jusqu’à ce que la déduction de nouveaux fact s’arrête, c’est-à-dire jusqu’au point fixe
Implémentation du parseur
- Prise en charge d’une grammaire de style Soufflé (
?var, :-, ., ,, etc.), avec tokenizer et parseur écrits directement en Rust
- En cas d’erreur, retour sûr de
None, dans une conception volontairement simple adaptée à un environnement expérimental
Optimisation des performances et exemples d’analyse réels
Analyse de nullability (reachability)
- Définition de règles Datalog et mesure des performances sur de grands jeux de données (par ex.
httpd_df) pour suivre les chemins de copie et de déplacement de valeurs
- Les performances varient énormément selon la manière d’écrire les règles (liaison des variables, ordre des colonnes, génération de relations temporaires selon le plan de jointure, etc.)
- Selon la forme initiale des données et la stratégie de jointure, le temps d’exécution et l’usage mémoire peuvent varier de plusieurs dizaines de fois, montrant concrètement l’importance de l’optimisation des requêtes
- Après optimisation, des gains de performance de 10 à plus de 80 fois par rapport à des outils existants en C++ (comme Graspan) ont été observés
Analyse d’aliasing (points-to)
- Implémentation de motifs Datalog complexes pour l’analyse d’aliasing et le suivi de pointeurs, avec exécution des mêmes requêtes que dans la littérature (Graspan, Zheng-Rugina, etc.)
- Les opérations de répétition (
^*), d’option (^?) et de transposition (^T) dans les règles Datalog sont développées en récursion et en union explicites
- Selon la manière de nommer et de réutiliser les résultats intermédiaires (relation
alias, jointures temporaires, etc.), l’efficacité et la consommation de ressources du plan de requête global changent fortement
- Sans optimisation du plan de requête, la production de gros résultats intermédiaires entraîne une chute des performances et une explosion de la mémoire (par ex. relation
V)
- L’approche « demand-driven » qui ne produit que les résultats nécessaires (transformation magic sets) est discutée, avec des perspectives d’amélioration via la transformation concrète du plan de requête
Enseignements tirés des expérimentations directes en Rust
- Les performances d’un moteur Datalog reposent avant tout sur la représentation des données (columnar/LSM), l’inférence différentielle et l’optimisation du plan de jointure
- Écrire des règles de manière purement mécanique conduit facilement à générer des données intermédiaires inutiles et à gaspiller des ressources, d’où la nécessité de l’optimisation
- En expérimentant directement en Rust, il est possible de gérer efficacement des
fact représentant plusieurs millions ou dizaines de millions de lignes sur des jeux de données réels, tout en obtenant extensibilité et rapidité d’inférence
- L’environnement CLI permet d’expérimenter facilement le chargement massif de données, l’ajout de règles en temps réel et la vérification des résultats
- Le rôle du query optimizer, les bushy-tree join utilisant les résultats intermédiaires, et l’habitude d’éviter la création de relations inutiles offrent des enseignements concrets pour l’écriture et l’exploitation de Datalog
Pistes d’extension pour la suite
- Il reste des sujets de recherche comme le spill sur disque, l’extension distribuée sur plusieurs workers/processus, les streaming joins et les optimisations de compilation personnalisées
- Le Datalog en Rust présente un fort potentiel d’usage dans des domaines concrets comme l’analyse de programmes à grande échelle, l’inférence sur graphes et relations, l’analyse statique et le suivi de flux de données
1 commentaires
Commentaire Hacker News