Le stockage de données non liées dans le bit le moins significatif d’un pointeur peut-il fonctionner de manière fiable?

Laissez-moi juste dire d’emblée que ce que je suis conscient de ce que je vais proposer est un péché mortel, et que je vais probablement brûler dans Programming Hell pour y avoir même pensé.

Cela dit, je suis toujours intéressé de savoir s’il existe une quelconque raison pour laquelle cela ne fonctionnerait pas.

La situation est la suivante: j’ai une classe de pointeur intelligent de comptage de références que j’utilise partout. Il ressemble actuellement à ceci (note: psuedocode incomplet / simplifié):

class IRefCountable { public: IRefCountable() : _refCount(0) {} virtual ~IRefCountable() {} void Ref() {_refCount++;} bool Unref() {return (--_refCount==0);} private: unsigned int _refCount; }; class Ref { public: Ref(IRefCountable * ptr, bool isObjectOnHeap) : _ptr(ptr), _isObjectOnHeap(isObjectOnHeap) { _ptr->Ref(); } ~Ref() { if ((_ptr->Unref())&&(_isObjectOnHeap)) delete _ptr; } private: IRefCountable * _ptr; bool _isObjectOnHeap; }; 

Aujourd’hui, j’ai remarqué que sizeof (Réf) = 16. Cependant, si je supprime la variable membre booléenne _isObjectOnHeap, sizeof (Ref) est réduite à 8. Cela signifie que pour chaque référence de mon programme, il y a 7,875 octets de RAM perdus … et qu’il existe de nombreuses références dans mon programme. .

Eh bien, cela semble être une perte de RAM. Mais j’ai vraiment besoin de ce petit supplément d’information (d’accord, amusez-vous bien et supposez que c’est vraiment pour la discussion que je fais). Et je remarque que, puisque IRefCountable est une classe non-POD, elle sera (probablement) toujours allouée à une adresse mémoire alignée sur un mot. Par conséquent, le bit le moins significatif de (_ptr) doit toujours être zéro.

Ce qui me fait me demander… y a-t-il une raison pour laquelle je ne peux pas OU mon seul bit de données booléennes dans le bit le moins significatif du pointeur, et ainsi réduire de moitié la taille de (Ref) sans sacrifier aucune fonctionnalité? Bien sûr, je devrais faire attention à ce point ET avant de déréférencer le pointeur, ce qui rendrait les déréférencements de pointeur moins efficaces, mais cela pourrait être compensé par le fait que les arbitres sont maintenant plus petits et plus nombreux. peut entrer dans le cache du processeur à la fois, et ainsi de suite.

Est-ce une chose raisonnable à faire? Ou est-ce que je me prépare à un monde de blessures? Et si ces derniers, comment exactement cela me serait-il blessé? (Notez qu’il s’agit d’un code qui doit être exécuté correctement dans tous les environnements de bureau raisonnablement modernes, mais pas nécessairement dans des ordinateurs embarqués ni dans des superordinateurs ou quoi que ce soit d’exotique.)

Une raison? Sauf si les choses ont changé récemment dans la norme, la représentation de la valeur d’un pointeur est définie par la mise en oeuvre. Il est certainement possible que certaines implémentations, quelque part, fassent de même, en définissant ces bits bas inutilisés pour leurs propres besoins. Il est encore plus possible que certaines implémentations utilisent des pointeurs de mots plutôt que des pointeurs, donc au lieu que deux mots adjacents soient aux “adresses” 0x8640 et 0x8642, ils seraient aux “adresses” 0x4320 et 0x4321.

Une solution délicate au problème consiste à faire de Ref une classe abstraite (de facto). Toutes les instances seraient en fait des instances de RefOnHeap et RefNotOnHeap. S’il y a un nombre aussi important de références, l’espace supplémentaire utilisé pour stocker le code et les métadonnées de trois classes plutôt que d’une seule est constitué par les économies d’espace, chaque référence ayant la moitié de sa taille. (Cela ne fonctionnera pas trop bien, le compilateur peut omettre le pointeur vtable s’il n’y a pas de méthodes virtuelles et l’introduction de méthodes virtuelles appenda les 4 ou 8 octets à la classe).

Le problème ici est que cela dépend entièrement de la machine. Ce n’est pas quelque chose que l’on voit souvent dans le code C ou C ++, mais cela a certainement été fait à plusieurs resockets en assembleur. Les interprètes Old Lisp utilisaient presque toujours cette astuce pour stocker des informations de type dans le ou les bits faibles. (J’ai vu int en code C, mais dans des projets mis en œuvre pour une plate-forme cible spécifique.)

Personnellement, si j’essayais d’écrire du code portable, je ne le ferais probablement pas. Le fait est que cela fonctionnera presque certainement sur “tous les environnements de bureau raisonnablement modernes”. (Certainement, cela fonctionnera sur tous ceux auxquels je peux penser.)

Cela dépend beaucoup de la nature de votre code. Si vous le maintenez et que personne d’autre n’aura à gérer le “monde des blessures”, alors tout ira bien. Vous devrez append des ifdef pour toute architecture étrange que vous devrez peut-être prendre en charge ultérieurement. D’un autre côté, si vous le publiez sous forme de code “portable”, vous seriez inquiet.

Une autre façon de gérer cela consiste à écrire deux versions de votre pointeur intelligent, une pour les machines sur lesquelles cela fonctionnera et une autre pour les machines où cela ne fonctionnera pas. Ainsi, tant que vous conserverez les deux versions, changer un fichier de configuration pour utiliser la version 16 octets n’aura pas été un gros problème.

Il va sans dire que vous devrez éviter d’écrire tout autre code supposant que sizeof(Ref) vaut 8 plutôt que 16. Si vous utilisez des tests unitaires, exécutez-les avec les deux versions.

Si vous souhaitez utiliser uniquement les fonctionnalités standard et ne vous fier à aucune implémentation, C ++ 0x permet d’exprimer l’alignement (voici une question récente à laquelle j’ai répondu). Il existe également std::uintptr_t pour obtenir de manière fiable un type entier non signé suffisamment grand pour contenir un pointeur. Maintenant, la seule chose garantie, c’est qu’une conversion du type pointeur en std::[u]intptr_t puis en ce même type donne le pointeur original.

Je suppose que vous pourriez argumenter que si vous pouvez récupérer le std::intptr_t (avec masquage), vous pouvez alors obtenir le pointeur d’origine. Je ne sais pas à quel point ce raisonnement serait solide.

[edit: en y réfléchissant, rien ne garantit qu’un pointeur aligné prend une forme particulière lorsqu’il est converti en un type intégral, par exemple un type avec quelques bits non définis. probablement trop exagéré ici]

Il y aura toujours un sentiment d’ incertitude à l’esprit même si cette méthode fonctionne, car au final, vous jouez avec l’architecture interne, qu’elle soit portable ou non.

D’autre part, pour résoudre ce problème, si vous voulez éviter la variable bool , je suggérerais un constructeur simple comme,

 Ref(IRefCountable * ptr) : _ptr(ptr) { if(ptr != 0) _ptr->Ref(); } 

D’après le code, je sens que le comptage des références n’est nécessaire que lorsque l’object est sur le tas . Pour les objects automatiques, vous pouvez simplement passer 0 à la class Ref et mettre les contrôles NULL appropriés dans le constructeur / destructeur.

Avez-vous pensé à un stockage en dehors des cours?

Selon que vous ayez (ou non) à vous soucier du multi-threading et à contrôler l’implémentation de new / delete / malloc / free, cela vaut la peine d’essayer.

Le point serait qu’au lieu d’incrémenter un compteur local (local à l’object), vous maintiendriez une adresse de carte “compteur” -> nombre qui ignorerait de manière lugubre les adresses passées qui sont en dehors de la zone allouée (stack par exemple).

Cela peut sembler idiot (il y a de la place pour la contention dans MT), mais cela joue aussi plutôt bien avec la lecture seule puisque l’object n’est pas “modifié” uniquement pour le comptage.

Bien sûr, je n’ai aucune idée de la performance que vous pouvez espérer atteindre avec ceci: p