Journal Alignement chaotic neutre

Posté par  (site Web personnel) . Licence CC By‑SA.
Étiquettes :
21
10
juil.
2021

Demat' iNal,

J'ai récemment eu l'ineffable [1] plaisir de corriger un bug dans LLVM qui m'a causé quelques mauvaises soirées. Afin que l'histoire devienne légende et que la légende fasse mythe, je me décide à vous en raconter quelques détails amusants.

Considérons le bout de code suivant :

#include <string>
#include <boost/align/aligned_allocator.hpp> 

constexpr std::size_t align = 32;
template<class T>
using aligned_allocator = boost::alignment::aligned_allocator<T, align>;

using aligned_string = std::basic_string<char, std::char_traits<char>, aligned_allocator<char>>;

aligned_string make_string(int num) {
    return aligned_string(num, '\0');

}

#include <iostream>
int main(int argc, char**argv) {
    auto s = make_string(argc);
    std::cout<< reinterpret_cast<std::uintptr_t>(s.data()) % align << std::endl;
    return 0;
}

Rien de bien fantastique : on a besoin d'une chaîne alignée sur 32, on utilise un allocateur spécialisé pour ça, et tout va pour le mieux. Ce code produit le résultat escompté (afficher 0) avec gcc5, gcc6 etc. Mais, et c'est là que l'horreur commence, pas avec gcc 4.9, où il affiche 24.

Mais pourquoi donc? (pas pourquoi donc utiliser gcc 4.9, hein, même si la question se pose). Une première piste : sur une architecture 32 bit, il va revoyer 12 et pas 24…

L'erreur du code présenté est de considérer que la pointeur renvoyé par std::basic_string<...>::data() est directement issu de l'allocateur mémoire. Rien n'oblige cela, et avec gcc 4.9, enfin avec la lib standard associée, l'organisation d'une string, c'est un header, puis le pointeur vers les données. Et le aligned_allocator est utilisé pour allouer toute la mémoire d'un coup, puis on fait quelques reinterpret_cast pour remplir le header correctement.

La structure d'une string ressemble à ça basic_string.h

                                        [_Rep]
                                        _M_length
   [basic_string<char_type>]            _M_capacity
   _M_dataplus                          _M_refcount
   _M_p ---------------->               unnamed array of char_type

l'allocation de l'objet complet: basic_string.tcc

size_type __size = (__capacity + 1) * sizeof(_CharT) + sizeof(_Rep);
/* ... */
void* __place = _Raw_bytes_alloc(__alloc).allocate(__size);
_Rep *__p = new (__place) _Rep;

Suivant la taille du header (sizeof(_Rep) donc), on a un alignement qui diffère… enfer !
Avec l'arrivée de C++11, le copy-on-write n'est plus une optimisation acceptée par le standard (je crois) et l'organisation des string se simplifie, le comportement espéré réapparait. Je ne suis pas pour autant persuadé que l'on puisse reposer sur une quelconque garantie entre l'alignement de l'allocateur mémoire et l'alignement des données de notre string. Mais peut-être que la sapience collective saura m'éclaircir ?

Conclusion de l'été : garder un string bien aligné, c'est compliqué.

[1] pas tant que ça vu que je vous en narre les péripéties ici même

  • # short strings

    Posté par  . Évalué à 4.

    Avec une implémentation de basic_string utilisant short string optimization (il me semble que c'est l'implémentation actuellement en place dans libc++, libstdc++, et la stl de msvc) je suppose que si le texte est court alors il n'y aura pas d'allocation et l'alignement de s.data() sera fonction de l'alignement de s. L'allocateur ne sera pas utilisé et du coup la maîtrise de l'alignement est sans espoir :)

    • [^] # Re: short strings

      Posté par  (site Web personnel) . Évalué à 5.

      Carrément ! Un clou de plus dans le cercueil de mes illusions.

    • [^] # Re: short strings

      Posté par  . Évalué à 2. Dernière modification le 11/07/21 à 16:53.

      Le problème des petits chaînes est la première chose qui été m'est venu à l'esprit en lisant le journal. On peut aussi imaginer que certaines implémentations puissent «optimiser» en ajustant l'adresse de début pour certaines opérations ; par exemple s.erase(0,1). En fait, je doute qu'il y ai quoi que ce soit dans le standard qui permette de faire la moindre hypothèse sur l'alignement des caractères.

      Si ton code est en C++17, le plus simple est probablement de créer une nouvelle classe basée sur std::string_view.

  • # Et LLVM dans tout ça ?

    Posté par  . Évalué à 10.

    Merci pour la narration sur ce croustillant problème :-)
    Je reste un peu perplexifié, parce que si on revient à l'introduction, on nous lance à la criée du problème LLVM, qui aurait été corrigé de surcoît.
    Et cependant, le contenu décrit sur une situation courante de déficience cognitive momentanée suite à l'utilisation de GCC, alors même que ce dernier n'y est pour rien !
    Est-ce un fourchage de clavier, ou un degré supplémentaire d'exercice de compréhension laissé au lecteur ?

  • # shared_ptr

    Posté par  . Évalué à 2.

    Tu peux t'attendre au même genre de problème avec std::shared_ptr puisque s'il est créé avec std::make_shared ou std::allocate_shared, la mémoire pour les données propres au pointeur partagé et celle pour l'objet lui-même sont généralement allouées en un seul bloc. C'est en fait un fonctionnement assez proches des std::string copy-on-write puisque la mémoire était aussi partagée (jusqu'à la première écriture).

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.