Libc ++ 'da kısa dizgi optimizasyonunun mekaniği nelerdir?


104

Bu cevap , kısa dizgi optimizasyonuna (SSO) ilişkin güzel bir üst düzey genel bakış sunar. Bununla birlikte, pratikte, özellikle libc ++ uygulamasında nasıl çalıştığını daha ayrıntılı olarak bilmek isterim:

  • SSO'ya hak kazanmak için dizenin ne kadar kısa olması gerekir? Bu hedef mimariye bağlı mı?

  • Dize verilerine erişirken uygulama, kısa ve uzun dizeleri nasıl ayırt eder? Bu kadar basit mi m_size <= 16yoksa başka bir üye değişkeninin parçası olan bir bayrak mı? (Bunun m_sizeveya bir kısmının dize verilerini depolamak için de kullanılabileceğini düşünüyorum).

Bu soruyu özellikle libc ++ için sordum çünkü SSO kullandığını biliyorum, bu libc ++ ana sayfasında bile bahsediliyor .

İşte kaynağa baktıktan sonra bazı gözlemler :

libc ++, string sınıfı için iki farklı bellek düzeniyle derlenebilir, bu _LIBCPP_ALTERNATE_STRING_LAYOUTbayrak tarafından yönetilir . Her iki düzen de küçük endian ve big-endian makineleri arasında ayrım yapıyor ve bu da bize toplam 4 farklı varyant bırakıyor. Aşağıda "normal" düzeni ve küçük endianı varsayacağım.

Bunun size_type4 bayt ve value_type1 bayt olduğunu varsayarsak , bu, bir dizenin ilk 4 baytı bellekte nasıl görünürdü:

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

Kısa dizinin boyutu üstteki 7 bitte olduğundan, ona erişirken kaydırılması gerekir:

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

Benzer şekilde, uzun bir dizinin kapasitesi için alıcı ve ayarlayıcı __long_mask, is_longbit etrafında çalışmak için kullanır .

Hala ilk soruma bir cevap arıyorum, yani __min_capkısa dizelerin kapasitesi farklı mimariler için hangi değeri alır?

Diğer standart kitaplık uygulamaları

Bu cevap , std::stringdiğer standart kütüphane uygulamalarındaki bellek düzenlerine güzel bir genel bakış sağlar .


libc ++ açık kaynak olduğundan, stringbaşlığını burada bulabilirsiniz , şu anda kontrol ediyorum :)
Matthieu M.


@Matthieu M .: Bunu daha önce görmüştüm, maalesef çok büyük bir dosya, kontrol ederken yardım ettiğin için teşekkürler.
ValarDohaeris

@Ali: Google'da gezinirken bunun üzerine tökezledim. Bununla birlikte, bu blog yazısı açıkça bunun yalnızca bir SSO örneği olduğunu ve pratikte kullanılacak yüksek düzeyde optimize edilmiş bir varyant olmadığını söylüyor.
ValarDohaeris

Yanıtlar:


120

Libc ++ tüm mimarilerde 3 kelimeye basic_stringsahip olacak şekilde tasarlanmıştır . Uzun / kısa bayrağı ve kısa formdaki boyut alanını doğru bir şekilde incelediniz.sizeofsizeof(word) == sizeof(void*)

Kısa dizelerin kapasitesi olan __min_cap farklı mimariler için hangi değeri alır?

Kısaca, çalışmak için 3 kelime var:

  • 1 bit uzun / kısa bayrağa gider.
  • 7 bit boyuta gider.
  • Varsayalım ki char, 1 bayt sondaki boş değere gidiyor (libc ++ her zaman verilerin arkasında bir boş değer depolar).

Bu, kısa bir dizeyi depolamak için 3 kelime eksi 2 bayt bırakır (yani capacity(), tahsis olmadan en büyük ).

32 bitlik bir makinede, kısa diziye 10 karakter sığacaktır. sizeof (dize) 12'dir.

64 bitlik bir makinede, kısa diziye 22 karakter sığacaktır. sizeof (dize) 24'tür.

Ana tasarım hedefi, sizeof(string)dahili tamponu olabildiğince büyük hale getirirken en aza indirmekti . Mantık, inşaatı hızlandırmak ve atamayı taşımaktır. Ne kadar büyük olursa sizeof, bir hareket inşası veya taşıma ödevi sırasında o kadar fazla kelime taşımak zorunda kalırsınız.

Uzun form, veri işaretçisini, boyutunu ve kapasitesini depolamak için en az 3 kelimeye ihtiyaç duyar. Bu nedenle kısa formu aynı 3 kelimeyle sınırladım. 4 kelimelik bir boyutun daha iyi bir performansa sahip olabileceği öne sürülmüştür. Bu tasarım seçimini test etmedim.

_LIBCPP_ABI_ALTERNATE_STRING_LAYOUT

_LIBCPP_ABI_ALTERNATE_STRING_LAYOUTVeri üyelerini yeniden düzenleyen bir yapılandırma bayrağı vardır, böylece "uzun düzen" şuna göre değişir:

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

to:

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

Bu değişikliğin motivasyonu, __data_ilk sıraya koymanın daha iyi uyum sayesinde bazı performans avantajlarına sahip olacağına inanmaktır . Performans avantajlarını ölçmek için bir girişimde bulunuldu ve ölçülmesi zordu. Performansı daha da kötüleştirmez ve biraz daha iyi hale getirebilir.

Bayrak dikkatli kullanılmalıdır. Bu farklı bir ABI'dir ve yanlışlıkla std::stringfarklı bir ayar ile derlenmiş bir libc ++ ile karıştırılırsa _LIBCPP_ABI_ALTERNATE_STRING_LAYOUTçalışma zamanı hataları oluşturur.

Bu bayrağın yalnızca bir libc ++ satıcısı tarafından değiştirilmesini öneririm.


17
Libc ++ ve Facebook Folly arasında lisans uyumluluğu olup olmadığından emin değilim, ancak FBstring, boyutu kalan kapasiteye değiştirerek fazladan bir karakter (yani 23) depolamayı başarır , böylece 23 karakterlik kısa bir dizi için boş sonlandırıcı olarak çift görev yapabilir. .
TemplateRex

20
@TemplateRex: Bu akıllıca. Bununla birlikte, eğer libc ++ benimsenirse, libc ++ 'nın std :: string ile ilgili sevdiğim bir diğer özelliğinden vazgeçmesini gerektirir: Yapılandırılan öntanımlılar string0 bittir. Bu, varsayılan yapıyı süper verimli hale getirir. Ve eğer kuralları esnetmeye istekliysen, bazen bedava bile. Örneğin, callochafızaya alabilir ve sadece varsayılan olarak oluşturulmuş dizelerle dolu olduğunu bildirebilirsiniz.
Howard Hinnant

6
Ah, 0-init gerçekten güzel! BTW, FBstring, kısa, orta ve büyük dizeleri gösteren 2 bayrak bitine sahiptir. SSO'yu 23 karaktere kadar dizeler için kullanır ve ardından 254 karaktere kadar olan dizeler için malloc-ed bellek bölgesi kullanır ve bunun ötesinde COW yapar (artık C ++ 11'de yasal değil, biliyorum).
TemplateRex

intSınıfın 64 bit mimarilerde yalnızca 16 bayta paketlenebilmesi için boyut ve kapasite neden s ' de depolanamıyor ?
phuclv

@ LưuVĩnhPhúc: 64 bit üzerinde 2 Gb'den büyük dizelere izin vermek istedim. Maliyet kuşkusuz daha yüksek sizeof. Ancak aynı zamanda dahili tampon char14'ten 22'ye çıkıyor ki bu oldukça iyi bir avantaj.
Howard Hinnant

21

Libc ++ uygulaması biraz karmaşık, onun alternatif tasarım görmezden ve biraz endian bilgisayar varsayalım edeceğiz:

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

Not: __compressed_pairTemelde Boş Taban Optimizasyonu için optimize edilmiş bir çifttir , aka template <T1, T2> struct __compressed_pair: T1, T2 {};; tüm niyet ve amaçlar için bunu normal bir çift olarak kabul edebilirsiniz. std::allocatorDevletsiz ve dolayısıyla boş olduğu için önemi ortaya çıkıyor .

Tamam, bu oldukça çiğ, o yüzden mekaniği kontrol edelim! Dahili olarak, dizenin veya gösterimi kullanıp kullanmadığını belirlemek için __get_pointer()kendisi çağıran birçok işlev çağırır :__is_long__long__short

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

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

Dürüst olmak gerekirse, bunun Standart C ++ olduğundan pek emin değilim (içindeki ilk alt dizi hükmünü biliyorum, unionancak anonim bir birleşimle nasıl örtüştüğünü bilmiyorum ve bir araya getirilmiş takma ad), ancak bir Standart Kitaplığın tanımlanan uygulamadan yararlanmasına izin verilir yine de davranış.


Bu detaylı cevap için teşekkürler! Kaçırdığım tek parça __min_cap, farklı mimariler için neyin değerlendirileceğidir, neyin sizeof()geri döneceğinden ve takma addan nasıl etkileneceğinden emin değilim .
ValarDohaeris

1
@ValarDohaeris uygulaması tanımlanmıştır. tipik olarak, 3 * the size of one pointerbu durumda 32 bitlik bir yayda 12 sekizli ve 64 bitlik bir yayda 24 sekizli olmasını beklersiniz .
justin
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.