Quali sono i meccanismi dell'ottimizzazione delle stringhe brevi in ​​libc ++?


102

Questa risposta offre una bella panoramica di alto livello sull'ottimizzazione delle stringhe brevi (SSO). Tuttavia, vorrei sapere più in dettaglio come funziona in pratica, in particolare nell'implementazione di libc ++:

  • Quanto deve essere breve la stringa per qualificarsi per SSO? Dipende dall'architettura di destinazione?

  • In che modo l'implementazione distingue tra stringhe brevi e lunghe quando si accede ai dati della stringa? È semplice come m_size <= 16o è un flag che fa parte di qualche altra variabile membro? (Immagino che m_sizeo parte di esso potrebbe essere utilizzato anche per memorizzare dati di stringa).

Ho posto questa domanda specificamente per libc ++ perché so che usa SSO, questo è anche menzionato nella home page di libc ++ .

Ecco alcune osservazioni dopo aver esaminato la fonte :

libc ++ può essere compilato con due layout di memoria leggermente diversi per la classe string, questo è governato dal _LIBCPP_ALTERNATE_STRING_LAYOUTflag. Entrambi i layout distinguono anche tra macchine little-endian e big-endian che ci lascia con un totale di 4 diverse varianti. Assumerò il layout "normale" e il little endian in quanto segue.

Supponendo inoltre che size_typesia 4 byte e che value_typesia 1 byte, questo è come apparirebbero i primi 4 byte di una stringa in memoria:

// 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

Poiché la dimensione della stringa breve è nei 7 bit superiori, deve essere spostata quando si accede:

size_type __get_short_size() const {
    return __r_.first().__s.__size_ >> 1;
}

Allo stesso modo, il getter e il setter per la capacità di una lunga stringa utilizza __long_maskper aggirare il is_longbit.

Sto ancora cercando una risposta alla mia prima domanda, ovvero quale valore __min_capassumerebbe la capacità delle stringhe corte per architetture diverse?

Altre implementazioni di librerie standard

Questa risposta offre una bella panoramica dei std::stringlayout di memoria in altre implementazioni di librerie standard.


libc ++ essendo open-source, puoi trovare la sua stringintestazione qui , lo sto controllando al momento :)
Matthieu M.


@ Matthieu M .: L'avevo già visto, purtroppo è un file molto grande, grazie per l'aiuto nel controllarlo.
ValarDohaeris

@ Ali: mi sono imbattuto in questo aspetto su Google. Tuttavia, questo post del blog afferma esplicitamente che è solo un'illustrazione di SSO e non una variante altamente ottimizzata che verrebbe utilizzata nella pratica.
ValarDohaeris

Risposte:


120

La libc ++ basic_stringè progettata per avere sizeof3 parole su tutte le architetture, dove sizeof(word) == sizeof(void*). Hai sezionato correttamente il flag lungo / corto e il campo delle dimensioni nella forma breve.

quale valore avrebbe __min_cap, la capacità di stringhe brevi, per architetture diverse?

Nella forma breve, ci sono 3 parole con cui lavorare:

  • 1 bit va al flag lungo / corto.
  • 7 bit corrispondono alla dimensione.
  • Supponendo che char1 byte vada al valore nullo finale (libc ++ memorizzerà sempre un nullo finale dietro i dati).

Ciò lascia 3 parole meno 2 byte per memorizzare una stringa breve (cioè più grande capacity()senza un'allocazione).

Su una macchina a 32 bit, 10 caratteri si adatteranno alla stringa breve. sizeof (stringa) è 12.

Su una macchina a 64 bit, 22 caratteri si adatteranno alla stringa breve. sizeof (stringa) è 24.

Uno dei principali obiettivi di progettazione era ridurre al minimo sizeof(string), rendendo il buffer interno il più ampio possibile. La logica è accelerare la costruzione del movimento e spostare l'assegnazione. Più grande è sizeof, più parole devi spostare durante una costruzione di mosse o un'assegnazione di mosse.

Il formato lungo richiede un minimo di 3 parole per memorizzare il puntatore di dati, le dimensioni e la capacità. Pertanto ho limitato la forma abbreviata a quelle stesse 3 parole. È stato suggerito che una dimensione di 4 parole potrebbe avere prestazioni migliori. Non ho testato quella scelta di design.

_LIBCPP_ABI_ALTERNATE_STRING_LAYOUT

C'è un flag di configurazione chiamato _LIBCPP_ABI_ALTERNATE_STRING_LAYOUTche riorganizza i membri dei dati in modo tale che il "layout lungo" cambi da:

struct __long
{
    size_type __cap_;
    size_type __size_;
    pointer   __data_;
};

per:

struct __long
{
    pointer   __data_;
    size_type __size_;
    size_type __cap_;
};

La motivazione di questo cambiamento è la convinzione che mettere al __data_primo posto avrà alcuni vantaggi in termini di prestazioni grazie a un migliore allineamento. È stato fatto un tentativo per misurare i vantaggi in termini di prestazioni ed è stato difficile misurarli. Non peggiorerà le prestazioni e potrebbe renderle leggermente migliori.

La bandiera dovrebbe essere usata con cura. È un ABI diverso, e se accidentalmente mescolato con una libc ++ std::stringcompilata con un'impostazione diversa di _LIBCPP_ABI_ALTERNATE_STRING_LAYOUTcreerà errori di runtime.

Raccomando che questo flag venga modificato solo da un fornitore di libc ++.


17
Non sono sicuro che ci sia compatibilità di licenza tra libc ++ e Facebook Folly, ma FBstring riesce a memorizzare un carattere extra (cioè 23) cambiando la dimensione alla capacità rimanente , in modo che possa fare il doppio dovere come terminatore nullo per una breve stringa di 23 caratteri .
TemplateRex

20
@TemplateRex: è intelligente. Tuttavia, se libc ++ adotta, richiederebbe che libc ++ rinunci a un'altra caratteristica che mi piace della sua std :: string: un valore predefinito costruito stringè tutto 0 bit. Ciò rende la costruzione predefinita super efficiente. E se sei disposto a infrangere le regole, a volte anche gratuitamente. Ad esempio, è possibile callocmemorizzare e dichiarare semplicemente che è pieno di stringhe costruite di default.
Howard Hinnant

6
Ah, 0-init è davvero carino! BTW, FBstring ha 2 bit flag, che indicano stringhe corte, intermedie e grandi. Utilizza l'SSO per stringhe fino a 23 caratteri, quindi utilizza una regione di memoria mallocata per stringhe fino a 254 caratteri e oltre a ciò fanno COW (non più legale in C ++ 11, lo so).
TemplateRex

Perché le dimensioni e la capacità non possono essere memorizzate in ints in modo che la classe possa essere compressa a soli 16 byte su architetture a 64 bit?
phuclv

@ LưuVĩnhPhúc: volevo consentire stringhe maggiori di 2 GB su 64 bit. Il costo è certamente maggiore sizeof. Ma allo stesso tempo il buffer interno per charva da 14 a 22, il che è un bel vantaggio.
Howard Hinnant

21

L' implementazione di libc ++ è un po 'complicata, ignorerò il suo design alternativo e suppongo che un piccolo computer 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

Nota: __compressed_pairè essenzialmente una coppia ottimizzata per l'ottimizzazione della base vuota , alias template <T1, T2> struct __compressed_pair: T1, T2 {};; a tutti gli effetti si può considerare una coppia regolare. La sua importanza emerge solo perché std::allocatorè apolide e quindi vuoto.

Ok, questo è piuttosto grezzo, quindi controlliamo la meccanica! Internamente, molte funzioni chiameranno la __get_pointer()quale stessa chiama __is_longper determinare se la stringa sta usando la rappresentazione __longor __short:

bool __is_long() const _NOEXCEPT
    { return bool(__r_.first().__s.__size_ & __short_mask); }

// __r_.first() -> __rep const&
//     .__s     -> __short const&
//     .__size_ -> unsigned char

Ad essere onesti, non sono troppo sicuro che questo sia lo standard C ++ (conosco la disposizione iniziale della sottosequenza in union ma non so come si combina con un'unione anonima e un aliasing messi insieme), ma una libreria standard può trarre vantaggio dall'implementazione definita comportamento comunque.


Grazie per questa risposta dettagliata! L'unico pezzo che mi manca è quello __min_capche valuterei per diverse architetture, non sono sicuro di cosa sizeof()tornerà e di come sia influenzato dall'aliasing.
ValarDohaeris

1
@ValarDohaeris è l'implementazione definita. in genere, ti aspetteresti 3 * the size of one pointerin questo caso, che sarebbero 12 ottetti su un arco a 32 bit e 24 su un arco a 64 bit.
justin
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.