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 Julien Jorge (site web personnel) . É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 des.data()
sera fonction de l'alignement des
. L'allocateur ne sera pas utilisé et du coup la maîtrise de l'alignement est sans espoir :)[^] # Re: short strings
Posté par serge_sans_paille (site web personnel) . Évalué à 5.
Carrément ! Un clou de plus dans le cercueil de mes illusions.
[^] # Re: short strings
Posté par SChauveau . Évalué à 2. Dernière modification le 11 juillet 2021 à 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 marzoul . É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 ?
[^] # Re: Et LLVM dans tout ça ?
Posté par serge_sans_paille (site web personnel) . Évalué à 5.
L'histoire dont est tiré ce journal est relatée dans https://reviews.llvm.org/D104745. J'ai voulu isoler le problème pour que le journal soit plus accessible, mais tu devrais y trouver de quoi satisfaire ta légitime curiosité !
[^] # Re: Et LLVM dans tout ça ?
Posté par claudex . Évalué à 5.
Perso, j'avais lu LVM, j'avais beaucoup plus de mal à comprendre le rapport.
« Rappelez-vous toujours que si la Gestapo avait les moyens de vous faire parler, les politiciens ont, eux, les moyens de vous faire taire. » Coluche
# shared_ptr
Posté par Clément V . Évalué à 2.
Tu peux t'attendre au même genre de problème avec
std::shared_ptr
puisque s'il est créé avecstd::make_shared
oustd::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 desstd::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.