Cette réponse donne une vue d’ensemble de Nice de l’optimisation des chaînes courtes (SSO). Cependant, j'aimerais savoir plus en détail comment cela fonctionne dans la pratique, en particulier dans l'implémentation de libc ++:
Combien de temps faut-il pour que la chaîne soit qualifiée pour le SSO? Cela dépend-il de l'architecture cible?
Comment l'implémentation fait-elle la distinction entre les chaînes courtes et les chaînes longues lors de l'accès aux données de chaîne? Est-ce aussi simple que m_size <= 16
ou s'agit-il d'un drapeau faisant partie d'une autre variable membre? (J'imagine que m_size
ou une partie de celui-ci peut également être utilisé pour stocker des données de chaîne).
J'ai posé cette question spécifiquement pour libc ++ parce que je sais qu'il utilise l'authentification unique, c'est même mentionné sur le page d'accueil de libc ++ .
Voici quelques observations après avoir regardé la source :
libc ++ peut être compilé avec deux dispositions de mémoire légèrement différentes pour la classe string, elle est régie par le _LIBCPP_ALTERNATE_STRING_LAYOUT
drapeau. Les deux configurations distinguent également les machines little-endian et big-endian, ce qui nous laisse au total 4 variantes. Je vais assumer la mise en page "normale" et le petit endian dans ce qui suit.
En supposant en outre que size_type
est 4 octets et que value_type
est 1 octet, voici à quoi ressemblent les 4 premiers octets d’une chaîne en mémoire:
// short string: (s)ize and 3 bytes of char (d)ata
sssssss0;dddddddd;dddddddd;dddddddd
^- is_long = 0
// long string: (c)apacity
ccccccc1;cccccccc;cccccccc;cccccccc
^- is_long = 1
Puisque la taille de la chaîne courte se trouve dans les 7 bits supérieurs, il faut la déplacer lors de l’accès:
size_type __get_short_size() const {
return __r_.first().__s.__size_ >> 1;
}
De même, le getter et le setter pour la capacité d'une longue chaîne utilisent __long_mask
pour contourner le is_long
peu.
Je cherche toujours une réponse à ma première question, c’est-à-dire quelle valeur aurait __min_cap
, la capacité des chaînes courtes, prendre pour différentes architectures?
Autres implémentations de bibliothèque standard
Cette réponse donne un bon aperçu de std::string
dispositions de mémoire dans d'autres implémentations de bibliothèque standard.
La libc ++ basic_string
Est conçue pour avoir un sizeof
3 mots sur toutes les architectures, où sizeof(Word) == sizeof(void*)
. Vous avez correctement disséqué l'indicateur long/short et le champ de taille dans le formulaire court.
quelle valeur __min_cap, la capacité des chaînes courtes, prend-elle pour différentes architectures?
Dans la forme abrégée, il y a 3 mots avec lesquels travailler:
char
, 1 octet va au null final (libc ++ stockera toujours un null final derrière les données).Cela laisse 3 mots moins 2 octets pour stocker une chaîne courte (c'est-à-dire le plus grand capacity()
sans allocation).
Sur une machine 32 bits, 10 caractères vont tenir dans la chaîne courte. sizeof (chaîne) est 12.
Sur une machine 64 bits, 22 caractères vont tenir dans la chaîne courte. sizeof (chaîne) est 24.
Un objectif majeur de la conception était de minimiser sizeof(string)
, tout en rendant le tampon interne aussi grand que possible. La raison en est d’accélérer la construction et l’affectation des déménagements. Plus le sizeof
est grand, plus vous devez déplacer de mots pendant une construction ou une affectation de déplacement.
La forme longue nécessite un minimum de 3 mots pour stocker le pointeur de données, sa taille et sa capacité. Par conséquent, j'ai limité la forme abrégée à ces 3 mêmes mots. Il a été suggéré qu'une taille de 4 mots pourrait avoir de meilleures performances. Je n'ai pas testé ce choix de design.
_ LIBCPP_ABI_ALTERNATE_STRING_LAYOUT
Il existe un indicateur de configuration appelé _LIBCPP_ABI_ALTERNATE_STRING_LAYOUT
Qui réorganise les membres de données de sorte que la "mise en page longue" passe de:
struct __long
{
size_type __cap_;
size_type __size_;
pointer __data_;
};
à:
struct __long
{
pointer __data_;
size_type __size_;
size_type __cap_;
};
La motivation de ce changement est la conviction que le fait de placer __data_
En premier aura certains avantages en termes de performances grâce à un meilleur alignement. Une tentative a été faite pour mesurer les avantages de performance, et c'était difficile à mesurer. Cela ne fera pas empirer les performances et pourrait l’améliorer légèrement.
Le drapeau doit être utilisé avec précaution. C'est une ABI différente, et si elle est accidentellement mélangée avec une libc ++ std::string
Compilée avec un paramètre différent de _LIBCPP_ABI_ALTERNATE_STRING_LAYOUT
, Cela créera des erreurs d'exécution.
Je recommande que cet indicateur ne soit modifié que par un fournisseur de libc ++.
Le implémentation de libc ++ est un peu compliqué, je vais ignorer sa conception alternative et supposer un petit ordinateur endian:
template <...>
class basic_string {
/* many many things */
struct __long
{
size_type __cap_;
size_type __size_;
pointer __data_;
};
enum {__short_mask = 0x01};
enum {__long_mask = 0x1ul};
enum {__min_cap = (sizeof(__long) - 1)/sizeof(value_type) > 2 ?
(sizeof(__long) - 1)/sizeof(value_type) : 2};
struct __short
{
union
{
unsigned char __size_;
value_type __lx;
};
value_type __data_[__min_cap];
};
union __ulx{__long __lx; __short __lxx;};
enum {__n_words = sizeof(__ulx) / sizeof(size_type)};
struct __raw
{
size_type __words[__n_words];
};
struct __rep
{
union
{
__long __l;
__short __s;
__raw __r;
};
};
__compressed_pair<__rep, allocator_type> __r_;
}; // basic_string
Remarque: __compressed_pair
Est essentiellement une paire optimisée pour le optimisation de la base vide , aussi appelé template <T1, T2> struct __compressed_pair: T1, T2 {};
; à toutes fins pratiques, vous pouvez le considérer comme une paire régulière. Son importance vient tout simplement parce que std::allocator
Est sans état et donc vide.
Ok, c'est plutôt brut, alors vérifions la mécanique! En interne, de nombreuses fonctions appellent __get_pointer()
qui appelle elle-même __is_long
Pour déterminer si la chaîne utilise la représentation __long
Ou __short
:
bool __is_long() const _NOEXCEPT
{ return bool(__r_.first().__s.__size_ & __short_mask); }
// __r_.first() -> __rep const&
// .__s -> __short const&
// .__size_ -> unsigned char
Pour être honnête, je ne suis pas trop sûr qu'il s'agisse de la norme C++ (je connais la provision initiale de sous-séquence dans union
, mais je ne sais pas comment elle se lie à une union anonyme et à un alias jeté ensemble), mais une bibliothèque standard est autorisée. de profiter de toute façon du comportement défini par la mise en œuvre.