libc ++での短い文字列最適化のメカニズムは何ですか?


102

この回答は、短い文字列の最適化(SSO)の概要を示しています。ただし、実際に、特にlibc ++実装でどのように機能するかを詳しく知りたいのですが。

  • SSOの対象となるには、文字列はどのくらい短い必要がありますか?これはターゲットアーキテクチャに依存しますか?

  • 文字列データにアクセスするとき、実装はどのように短い文字列と長い文字列を区別しますか?それは同じくらい簡単m_size <= 16ですか、それとも他のメンバー変数の一部であるフラグですか?(私はそれm_sizeまたはその一部が文字列データを格納するために使用されることもあると思います)

私がこの質問をlibc ++に対して特別に尋ねたのは、それがSSOを使用していることがわかっているためです。これは、libc ++ホームページにも記載されています

ソースを確認し後の観察結果を次に示します

libc ++は、文字列クラスの2つのわずかに異なるメモリレイアウトでコンパイルできます_LIBCPP_ALTERNATE_STRING_LAYOUT。これはフラグによって制御されます。どちらのレイアウトでも、リトルエンディアンマシンとビッグエンディアンマシンが区別され、合計4つの異なるバリアントが存在します。以下の説明では、「通常の」レイアウトとリトルエンディアンを想定します。

さらにそれsize_typeが4バイトでvalue_type1バイトであると仮定すると、これは文字列の最初の4バイトがメモリ内でどのように見えるかです。

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

短い文字列のサイズは上位7ビットであるため、アクセスするときにシフトする必要があります。

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

同様に、長い文字列の容量のゲッターとセッターは__long_maskis_longビットを回避するために使用します。

私はまだ私の最初の質問に対する答えを探しています。つまり__min_cap、異なる文字列に対して、どのような値、短い文字列の容量がとるのでしょうか?

その他の標準ライブラリの実装

この回答は、std::string他の標準ライブラリ実装におけるメモリレイアウトの概要を提供します。


libc ++はオープンソースであり、ここstringヘッダーを見つけることができます


@Matthieu M .:以前に見たことがありますが、残念ながら非常に大きなファイルです。チェックしてくれてありがとう。
ValarDohaeris 2014

@アリ:私はググリングでこれにつまずきました。ただし、このブログの投稿では、SSOの例示にすぎず、実際に使用される高度に最適化されたバリアントではないことが明確に述べられています。
ValarDohaeris 2014

回答:


120

libc ++ basic_stringsizeof、すべてのアーキテクチャで3ワードになるように設計されていsizeof(word) == sizeof(void*)ます。ロング/ショートフラグとショートフォームのサイズフィールドを正しく分析しました。

__min_cap、つまり短い文字列の容量は、異なるアーキテクチャに対してどのような値をとりますか?

短い形式では、処理する3つの単語があります。

  • 1ビットはロング/ショートフラグに行きます。
  • サイズは7ビットになります。
  • と仮定するとchar、1バイトは末尾のnullに移動します(libc ++は常にデータの後ろに末尾のnullを格納します)。

これにより、短い文字列(つまりcapacity()、割り当てなしで最大のもの)を格納するために、3ワードから2バイトが引かれます。

32ビットマシンでは、10文字が短い文字列に収まります。sizeof(string)は12です。

64ビットマシンでは、22文字が短い文字列に収まります。sizeof(string)は24です。

主な設計目標はsizeof(string)、内部バッファーをできるだけ大きくすると同時に、を最小化することでした。理論的根拠は、移動の構築と割り当ての移動を高速化することです。が大きいほど、sizeof移動構築または移動割り当て中に移動する必要がある単語が多くなります。

長い形式では、データポインタ、サイズ、容量を格納するために最低3ワードが必要です。したがって、短縮形を同じ3ワードに制限しました。4ワードのsizeofの方がパフォーマンスが高いことが示唆されています。私はそのデザインの選択をテストしていません。

_LIBCPP_ABI_ALTERNATE_STRING_LAYOUT

_LIBCPP_ABI_ALTERNATE_STRING_LAYOUT「長いレイアウト」が次のように変化するようにデータメンバーを再配置するという構成フラグがあります。

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

に:

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

この変更の動機は、__data_最初に配置することで、より適切な調整によりパフォーマンス上の利点が得られるという信念です。パフォーマンス上の利点を測定しようとしましたが、測定するのが困難でした。パフォーマンスが低下することはなく、わずかに改善される場合があります。

フラグは注意して使用する必要があります。これは異なるABIであり、誤っstd::stringて異なる設定でコンパイルされたlibc ++と混合すると、_LIBCPP_ABI_ALTERNATE_STRING_LAYOUTランタイムエラーが発生します。

このフラグはlibc ++のベンダーによってのみ変更されることをお勧めします。


17
libc ++とFacebook Follyの間にライセンスの互換性があるかどうかはわかりませんが、FBstringはサイズを残りの容量に変更することで余分な文字(つまり23)を格納し、23文字の短い文字列のnullターミネーターとしての役割を果たします。 。
TemplateRex

20
@TemplateRex:それは賢いです。ただし、libc ++を採用する場合は、libc ++がstd :: stringについて私が気に入っているもう1つの特性を放棄する必要がありstringます。構築されるデフォルトはすべて0ビットです。これにより、デフォルトの構築が非常に効率的になります。そして、あなたがルールを曲げても構わないと思っているなら、時には自由ですら。たとえば、callocメモリを作成して、デフォルトで構築された文字列でいっぱいになるように宣言することができます。
ハワードヒナント2014

6
ああ、0-initは確かにいいです!ところで、FBstringには2つのフラグビットがあり、短い、中間、大きい文字列を示します。それは23文字までの文字列にSSOを使用し、次に254文字までの文字列にmallocされたメモリ領域を使用し、それを超えてCOWを実行します(C ++ 11ではもはや合法ではないことを私は知っています)。
TemplateRex 14

int64ビットアーキテクチャでクラスを16バイトにのみパックできるように、サイズと容量をs に格納できないのはなぜですか?
phuclv 2016年

@LưuVĩnhPhúc:64ビットで2Gbを超える文字列を許可したかった。コストは明らかに大きいsizeofです。しかし同時に、内部バッファはchar14から22になり、これはかなり良い利点です。
Howard Hinnant、2016年

21

libcの++実装が少し複雑になって、私はその代替設計を無視し、リトルエンディアンのコンピュータを想定されます:

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

注:__compressed_pair基本的には、Empty Base Optimization(別名)用に最適化されたペアtemplate <T1, T2> struct __compressed_pair: T1, T2 {};です。すべての意図と目的のために、それを通常のペアと考えることができます。std::allocatorはステートレスであり、したがって空であるため、その重要性が浮上します。

さて、これはかなり生ですので、メカニズムをチェックしましょう!内部的には、多くの関数が__get_pointer()それ自体を呼び出し__is_longて、文字列が__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

正直言って、これが標準C ++であるunionかどうかはあまりわかりません(最初のサブシーケンスの規定は知っていますが、匿名ユニオンと一緒にスローされ、エイリアスがどのように組み合わされるかはわかりません)。標準ライブラリは、定義された実装を利用できます。とにかく行動。


この詳細な回答をありがとう!私が欠けている唯一の部分は__min_cap、さまざまなアーキテクチャで評価されるものです。何sizeof()が返され、エイリアスがどのように影響するかはわかりません。
ValarDohaeris 14

1
@ValarDohaeris実装が定義されています。通常、3 * the size of one pointerこの場合は、32ビットアーチでは12オクテット、64ビットアーチでは24オクテットになります。
ジャスティン2014
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.