eMQTT5 analyse technique

Analyse technique pour la minimisation des ressources dans eMQTT5

eMQTT5 est un client MQTT v5.0 qui vise une faible utilisation des ressources pour les systèmes embarqués.

Afin d'atteindre cet objectif, nous avons utilisé de nombreuses astuces et techniques que nous présentons ci-dessous.

Pas d'allocation de tas

Sur un ordinateur de 4 Go, l'allocation de mémoire est très facile et peut se faire sans trop se soucier des conséquences. Sur un appareil de 64 ko, l'allocation est une chose qui, si elle n'est pas impossible, doit être très soigneusement réfléchie. L'allocation de la mémoire pose trois problèmes :

  1. L'espace limité et la gestion de l'épuisement de l'espace du tas
  2. Fragmentation
  3. Fuite de la mémoire

Le premier problème implique l'ajout de code pour gérer l'épuisement de l'espace de stockage, ce qui a un impact sur la taille binaire générée.

Le second problème survient lors de l'exécution des séquences habituelles "alloc A, alloc B, free A, alloc A, free B". Même si le logiciel est logiquement correct, il est très difficile pour un allocateur de récupérer la mémoire correctement dans ce cas, et cela finit par épuiser la mémoire, uniquement parce que l'allocateur ne peut pas fusionner l'espace libre correctement. Avoir un allocateur très intelligent prend aussi une taille binaire.

Le dernier problème est principalement dû à des bogues logiciels.

Afin d'éviter tous ces problèmes, eMQTT5 ne fait aucune allocation une fois créé. Il peut utiliser correctement les données allouées au tas, mais n'allouera rien sur le tas par lui-même.

Si des allocations sont absolument nécessaires, elles sont faites sur la pile et gérées en conséquence.

Pour allouer à partir de la pile (ce qui signifie que l'allocation sera libérée en quittant la fonction d'allocation), une classe appelée StackHeapBuffer est utilisée. Elle permet de suivre si un tampon a été alloué par le tas ou la pile. Dans le premier cas, le tampon est free lorsque l'instance est détruite (RAII). Habituellement, l'espace de la pile est limité, il n'est donc pas sûr d'allouer à partir de la pile sans faire très attention. Le StackHeapBuffer est associé à la macro DeclareStackHeapBuffer qui vérifie si la pile ne va pas déborder de l'allocation et allouée sur le tas si elle pense qu'elle va déborder.

Vues

Dans un système embarqué, où les cycles de mémoire et de CPU sont limités, la copie des données est un gaspillage de ressources. Pour éviter cela, eMQTT5 utilise les vues et le modèle de visiteur pour permettre de visiter le buffer du réseau reçu et de mapper les vues sur le buffer sans aucune copie. MQTT est principalement un protocole de sérialisation. Cela signifie qu'il décrit comment les valeurs et les données doivent être disposées dans la mémoire. En inversant le protocole, il est possible d'extraire les valeurs et les données d'un paquet sérialisé sans jamais le copier.

Cela se fait dans la classe VisitorVariant. Cette classe contient un buffer qui a la taille exacte des plus grandes données de base qui peuvent être lues dans MQTT v5.0. Ce buffer est ensuite utilisé comme une variante, c'est-à-dire un objet de type variable. Selon le type de variante, le buffer sera compris comme un flottant, un entier variable, une vue de chaîne de caractères, etc...

La sécurité du type se fait à l'exécution, lorsque la classe de la variante est instanciée, le type de buffer attendu est enregistré et tout accès effectué sur un autre type retournera une valeur vide ou nulle. Cependant, pour éviter de créer des instances inutiles, une variante peut muter vers un autre type pour réutiliser un seul buffer.

La variante peut contenir des vues. Les vues sont des objets spécifiques contenant une taille et un pointeur. Vous pouvez accéder à une vue comme l'objet visualisé (même interface) mais aucune allocation/désallocation n'est faite avec une vue. Cela signifie qu'une vue n'est valide que tant que le buffer vers lequel elle pointe est valide. Vous ne pouvez pas stocker une vue pour l'utiliser plus tard.

template et héritage

Lees template impliquent généralement un gonflement du code généré et s'appuie sur le compilateur pour optimiser tous les éléments communs des nombreuses instanciations/spécialisations. Dans notre cas, il serait trop optimiste d'attendre du compilateur qu'il trouve le moyen d'optimiser toute la hiérarchie complète des messages MQTT et des types de variable.

Nous avons donc utilisé un schéma que nous avons appelé "héritage statique mixte".

En C++, l'héritage à partir d'une classe de base implique généralement l'ajout d'une table virtuelle à votre objet. Cette table virtuelle implique à la fois un coût sur la taille binaire générée (vous devez stocker de nombreuses fonctions dupliquées, comme les destructeurs virtuels), sur la mémoire d'exécution (tous vos objets ont une table de pointeurs qui leur est attachée) et des performances d'exécution (l'appel d'une méthode implique au moins 2 lookup en mémoire, une pour récupérer l'adresse de la méthode à appeler et ensuite appeler la méthode).

Notre héritage statique mixte se fait donc de cette manière :

  1. Le code commun de toutes les instanciations/spécialisations du modèle est déplacé vers une classe de base
  2. Ne jamais utiliser directement la classe de base (ce n'est pas un véritable héritage polymorphique ici)
  3. Pas de destructeur virtuel pour la classe de base (non requis car il n'est jamais appelé polymorphiquement)
  4. Marquer le template enfant final (afin que le compilateur puisse optimiser la table virtuelle, puisqu'il peut calculer tous les appels de méthodes de manière statique)

Si l'usage exige un polymorphisme (par exemple, pour les "propriétés"), alors un destructeur virtuel est utilisé. Mais pour les autres classes, ne payez pas pour ce dont vous n'avez pas besoin.

Article précédent