string 常見的三種實現方式

eager copy

COW

SSO

Fbstring 介紹

Storage strategies

Implementation highlights

Benchmark

主要類

字串儲存資料結構

small strings

medium strings

large strings

如何區分字串型別 category

small strings

medium strings

large strings

category()

小端

大端

capacity()

size()

字串初始化

initSmall

initMedium

initLarge

特殊的建構函式 —— 不複製使用者傳入的字串

字串複製

copySmall

copyMedium

copyLarge

析構

COW

non-const operator

non-const 與 const

Realloc

其他

__builtin_expect

CMOV 指令

builtin_unreachable && assume(0)

jemalloc

find 演算法

判斷大小端

參考資料

在引入

fbstring

之前,我們首先再回顧一下 string 常見的三種實現方式。

string 常見的三種實現方式

string 中比較重要的 3 個欄位:

char *data。 指向存放字串的首地址(在 SSO 的某些實現方案中可能沒有此欄位)。

size_t size。 字串長度。

size_t capacity。 字串容量。capacity >= size。 在字串相加、reserve 等場景下會用到此欄位。

eager copy

這個是最簡單、最好理解的一種,在每次複製時將原 string 對應的記憶體以及所持有的動態資源完整地複製一份,即沒有任何特殊處理。

C++ folly庫解讀(一) Fbstring —— 一個完美替代std::string的庫

優點:

實現簡單。

每個物件互相獨立,不用考慮那麼多亂七八糟的場景。

缺點:

字串較大時,複製浪費空間。

COW

這個也算是計算機裡的基本思想了。不同於 eager copy 的每次複製都會複製,此種實現方式為寫時複製,即 copy-on-write。只有在某個 string 要對共享物件進行修改時,才會真正執行複製。

由於存在共享機制,所以需要一個

std::atomic

,代表被多少物件共享。

C++ folly庫解讀(一) Fbstring —— 一個完美替代std::string的庫

寫時複製:

C++ folly庫解讀(一) Fbstring —— 一個完美替代std::string的庫

優點:

字串空間較大時,減少了分配、複製字串的時間。

缺點:

refcount 需要原子操作,效能有損耗。

某些情況下會帶來意外的開銷。比如非 const 成員使用[],這會觸發 COW,因為無法知曉應用程式是否會對返回的字元做修改。典型的如

Legality of COW std::string implementation in C++11

中舉的例子:

std

::

string

s

“str”

);

const

char

*

p

=

s

data

();

{

std

::

string

s2

s

);

void

s

0

];

// 觸發COW

}

std

::

cout

<<

*

p

<<

‘\n’

// p指向的原有空間已經無效

其他更細節的缺點可以參考:

std::string 的 Copy-on-Write:不如想象中美好

SSO

Small String Optimization。

基於字串大多數比較短的特點

,利用 string 物件本身的棧空間來儲存短字串。而當字串長度大於某個臨界值時,則使用 eager copy 的方式。

SSO 下,string 的資料結構會稍微複雜點,使用 union 來區分短字串和長字串的場景:

class

string

{

char

*

start

size_t

size

static

const

int

kLocalSize

=

15

union

{

char

buffer

kLocalSize

+

1

];

// 滿足條件時,用來存放短字串

size_t

capacity

}

data

};

短字串,SSO:

C++ folly庫解讀(一) Fbstring —— 一個完美替代std::string的庫

長字串,eager copy:

C++ folly庫解讀(一) Fbstring —— 一個完美替代std::string的庫

這種資料結構的實現下,SSO 的閾值一般是 15 位元組。folly 的 fbstring 在 SSO 場景下,資料結構做了一些最佳化,可以儲存 23 個位元組,後面會提到。

優點:

短字串時,無動態記憶體分配。

缺點:

string 物件佔用空間比 eager copy 和 cow 要大。

Fbstring 介紹

fbstring 可以 100%相容 std::string。配合三種儲存策略和

jemalloc

,可以顯著的提高 string 的效能。

fbstring 支援 32-bit、64-bit、little-endian、big-endian。

Storage strategies

Small Strings (<= 23 chars) ,使用 SSO。

Medium strings (24 - 255 chars),使用 eager copy。

Large strings (> 255 chars),使用 COW。

Implementation highlights

與 std::string 100%相容。

COW 儲存時對於引用計數執行緒安全。

對 Jemalloc 友好。如果檢測到使用 jemalloc,那麼將使用 jemalloc 的一些非標準擴充套件介面來提高效能。

find()

使用簡化版的

Boyer-Moore algorithm

。在查詢成功的情況下,相對於

string::find()

有 30 倍的效能提升。在查詢失敗的情況下也有 1。5 倍的效能提升。

可以與 std::string 互相轉換。

Benchmark

FBStringBenchmark.cpp

中。

主要類

C++ folly庫解讀(一) Fbstring —— 一個完美替代std::string的庫

::folly::fbstring str(“abc”)

中的 fbstring 為 basic_fbstring的別名 :

typedef basic_fbstring fbstring;

basic_fbstring 在 fbstring_core 提供的介面之上,實現了 std::string 定義的所有介面。裡面有一個私有變數 store,預設值即為 fbstring_core。basic_fbstring 的定義如下,比 std::basic_string 只多了一個預設的模板引數 Storage:

template

<

typename

E

class

T

=

std

::

char_traits

<

E

>

class

A

=

std

::

allocator

<

E

>

class

Storage

=

fbstring_core

<

E

>>

class

basic_fbstring

fbstring_core 負責字串的儲存及字串相關的操作,例如 init、copy、reserve、shrink 等等。

字串儲存資料結構

最重要的 3 個數據結構 union{Char small*, MediumLarge ml*}、MediumLarge、RefCounted,定義在 fbstring_core 中,基本上所有的字串操作都離不開這三個資料結構。

struct

RefCounted

{

std

::

atomic

<

size_t

>

refCount_

Char

data_

1

];

static

RefCounted

*

create

size_t

*

size

);

// 建立一個RefCounted

static

RefCounted

*

create

const

Char

*

data

size_t

*

size

);

// ditto

static

void

incrementRefs

Char

*

p

);

// 增加一個引用

static

void

decrementRefs

Char

*

p

);

// 減少一個引用

// 其他函式定義

};

struct

MediumLarge

{

Char

*

data_

size_t

size_

size_t

capacity_

size_t

capacity

()

const

{

return

kIsLittleEndian

capacity_

&

capacityExtractMask

capacity_

>>

2

}

void

setCapacity

size_t

cap

Category

cat

{

capacity_

=

kIsLittleEndian

cap

|

static_cast

<

size_t

>

cat

<<

kCategoryShift

cap

<<

2

|

static_cast

<

size_t

>

cat

);

}

};

union

{

uint8_t

bytes_

sizeof

MediumLarge

)];

// For accessing the last byte。

Char

small_

sizeof

MediumLarge

/

sizeof

Char

)];

MediumLarge

ml_

};

small strings(SSO)時,使用 union 中的 Char small_儲存字串,即物件本身的棧空間。

medium strings(eager copy)時,使用 union 中的

MediumLarge ml_

Char* data_ : 指向分配在堆上的字串。

size_t size:字串長度。

size_t capacity :字串容量。

large strings(cow)時, 使用

MediumLarge ml_

和 RefCounted:

RefCounted。refCount_ :共享字串的引用計數。

RefCounted。data_[1] :

flexible array.

存放字串。

ml*。data_指向 RefCounted。data,ml*。size_與 ml。capacity_的含義不變。

但是這裡有一個問題是:SSO 情況下的 size 和 capacity 存在哪裡了?

capacity : 首先 SSO 的場景並不需要 capacity,因為此時利用的是棧空間,或者理解此種情況下的 capacity=maxSmallSize。

size : 利用 small_的一個位元組來儲存 size,

而且具體儲存的不是 size,而是maxSmallSize - s

(maxSmallSize=23,再轉成 char 型別),因為這樣可以 SSO 多儲存一個位元組,具體原因後面詳細講。

small strings :

C++ folly庫解讀(一) Fbstring —— 一個完美替代std::string的庫

medium strings :

C++ folly庫解讀(一) Fbstring —— 一個完美替代std::string的庫

large strings :

C++ folly庫解讀(一) Fbstring —— 一個完美替代std::string的庫

如何區分字串型別 category

字串的 small/medium/large 型別對外部透明,而且針對字串的各種操作例如 copy、shrink、reserve、賦值等等,三種類型的處理方式都不一樣,

所以,我們需要在上面的資料結構中做些“手腳”,來區分不同的字串型別。

因為只有三種類型,所以只需要 2 個 bit 就能夠區分。相關的資料結構為:

typedef

uint8_t

category_type

enum

class

Category

category_type

{

isSmall

=

0

isMedium

=

kIsLittleEndian

0x80

0x2

// 10000000 , 00000010

isLarge

=

kIsLittleEndian

0x40

0x1

// 01000000 , 00000001

};

kIsLittleEndian 為判斷當前平臺的大小端,大端和小端的儲存方式不同。

small strings

category 與 size 共同存放在 small_的最後一個位元組中(size 最大為 23,所以可以存下),考慮到大小端,所以有移位操作,這主要是為了讓 category()的判斷更簡單,後面再詳細分析。具體程式碼在 setSmallSize 中:

void

setSmallSize

size_t

s

{

……

constexpr

auto

shift

=

kIsLittleEndian

0

2

small_

maxSmallSize

=

char

((

maxSmallSize

-

s

<<

shift

);

……

}

C++ folly庫解讀(一) Fbstring —— 一個完美替代std::string的庫

medium strings

可能有人注意到了,在 MediumLarge 結構體中定義了兩個方法,

capacity()

setCapacity(size_t cap, Category cat)

,其中 setCapacity 即同時設定 capacity 和 category :

constexpr

static

size_t

kCategoryShift

=

sizeof

size_t

-

1

*

8

void

setCapacity

size_t

cap

Category

cat

{

capacity_

=

kIsLittleEndian

cap

|

static_cast

<

size_t

>

cat

<<

kCategoryShift

cap

<<

2

|

static_cast

<

size_t

>

cat

);

}

小端時,將 category = isMedium = 0x80 向左移動

(sizeof(size_t) - 1) * 8

位,即移到最高位的位元組中,再與 capacity 做或運算。

大端時,將 category = isMedium = 0x2 與 cap << 2 做或運算即可,左移 2 位的目的是給 category 留空間。

舉個例子,假設 64 位機器,capacity = 100 :

C++ folly庫解讀(一) Fbstring —— 一個完美替代std::string的庫

large strings

同樣使用 MediumLarge 的 setCapacity,演算法相同,只是 category 的值不同。

假設 64 位機器,capacity = 1000 :

C++ folly庫解讀(一) Fbstring —— 一個完美替代std::string的庫

category()

category()

為最重要的函式之一,作用是獲取字串的型別:

constexpr

static

uint8_t

categoryExtractMask

=

kIsLittleEndian

0xC0

0x3

// 11000000 , 00000011

constexpr

static

size_t

lastChar

=

sizeof

MediumLarge

-

1

union

{

uint8_t

bytes_

sizeof

MediumLarge

)];

// For accessing the last byte。

Char

small_

sizeof

MediumLarge

/

sizeof

Char

)];

MediumLarge

ml_

};

Category

category

()

const

{

// works for both big-endian and little-endian

return

static_cast

<

Category

>

bytes_

lastChar

&

categoryExtractMask

);

}

bytes_定義在 union 中,從註釋可以看出來,是為了配合 lastChar 更加方便的取該結構最後一個位元組。

配合上面三種類型字串的儲存,可以很容易理解這一行程式碼。

小端

C++ folly庫解讀(一) Fbstring —— 一個完美替代std::string的庫

大端

C++ folly庫解讀(一) Fbstring —— 一個完美替代std::string的庫

capacity()

獲取字串的 capaticy,因為 capacity 與 category 儲存都在一起,所以一起看比較好。

同樣分三種情況。

size_t

capacity

()

const

{

switch

category

())

{

case

Category

::

isSmall

return

maxSmallSize

case

Category

::

isLarge

// For large-sized strings, a multi-referenced chunk has no

// available capacity。 This is because any attempt to append

// data would trigger a new allocation。

if

RefCounted

::

refs

ml_

data_

>

1

{

return

ml_

size_

}

break

case

Category

::

isMedium

default

break

}

return

ml_

capacity

();

}

small strings : 直接返回 maxSmallSize,前面有分析過。

medium strings : 返回 ml_。capacity()。

large strings :

當字串引用大於 1 時,直接返回 size。因為此時的 capacity 是沒有意義的,任何 append data 操作都會觸發一次 cow

否則,返回 ml_。capacity()。

看下 ml。capacity() :

constexpr

static

uint8_t

categoryExtractMask

=

kIsLittleEndian

0xC0

0x3

constexpr

static

size_t

kCategoryShift

=

sizeof

size_t

-

1

*

8

constexpr

static

size_t

capacityExtractMask

=

kIsLittleEndian

~

size_t

categoryExtractMask

<<

kCategoryShift

0x0

/* unused */

size_t

capacity

()

const

{

return

kIsLittleEndian

capacity_

&

capacityExtractMask

capacity_

>>

2

}

categoryExtractMask 和 kCategoryShift 之前遇到過,分別用來計算 category 和小端情況下將 category 左移 kCategoryShift 位。capacityExtractMask 的目的就是消掉 category,讓 capacity_中只有 capacity。

對著上面的每種情況下字串的儲存的圖,應該很好理解,這裡不細說了。

size()

size_t

size

()

const

{

size_t

ret

=

ml_

size_

if

/* constexpr */

kIsLittleEndian

{

// We can save a couple instructions, because the category is

// small iff the last char, as unsigned, is <= maxSmallSize。

typedef

typename

std

::

make_unsigned

<

Char

>::

type

UChar

auto

maybeSmallSize

=

size_t

maxSmallSize

-

size_t

static_cast

<

UChar

>

small_

maxSmallSize

]));

// With this syntax, GCC and Clang generate a CMOV instead of a branch。

ret

=

static_cast

<

ssize_t

>

maybeSmallSize

>=

0

maybeSmallSize

ret

}

else

{

ret

=

category

()

==

Category

::

isSmall

smallSize

()

ret

}

return

ret

}

小端的情況下,medium strings 和 large strings 對應的 ml_的高位元組儲存的是 category(0x80、0x40),而 small strings 儲存的是 size,所以正如註釋說的,可以先判斷 kIsLittleEndian && maybeSmall,會快一些,不需要呼叫 smallSize()。而且現在絕大多數平臺都是小端。

如果是大端,那麼如果是 small,呼叫 smallSize(),否則返回 ml。size_;

size_t

smallSize

()

const

{

assert

category

()

==

Category

::

isSmall

);

constexpr

auto

shift

=

kIsLittleEndian

0

2

auto

smallShifted

=

static_cast

<

size_t

>

small_

maxSmallSize

])

>>

shift

assert

static_cast

<

size_t

>

maxSmallSize

>=

smallShifted

);

return

static_cast

<

size_t

>

maxSmallSize

-

smallShifted

}

比較簡單,不說了。

字串初始化

首先 fbstring_core 的建構函式中,根據字串的長度,呼叫 3 種不同型別的初始化函式:

fbstring_core

const

Char

*

const

data

const

size_t

size

bool

disableSSO

=

FBSTRING_DISABLE_SSO

{

if

disableSSO

&&

size

<=

maxSmallSize

{

initSmall

data

size

);

}

else

if

size

<=

maxMediumSize

{

initMedium

data

size

);

}

else

{

initLarge

data

size

);

}

}

initSmall

template

<

class

Char

>

inline

void

fbstring_core

<

Char

>::

initSmall

const

Char

*

const

data

const

size_t

size

{

// If data is aligned, use fast word-wise copying。 Otherwise,

// use conservative memcpy。

// The word-wise path reads bytes which are outside the range of

// the string, and makes ASan unhappy, so we disable it when

// compiling with ASan。

#ifndef FOLLY_SANITIZE_ADDRESS

if

((

reinterpret_cast

<

size_t

>

data

&

sizeof

size_t

-

1

))

==

0

{

const

size_t

byteSize

=

size

*

sizeof

Char

);

constexpr

size_t

wordWidth

=

sizeof

size_t

);

switch

((

byteSize

+

wordWidth

-

1

/

wordWidth

{

// Number of words。

case

3

ml_

capacity_

=

reinterpret_cast

<

const

size_t

*>

data

)[

2

];

FOLLY_FALLTHROUGH

case

2

ml_

size_

=

reinterpret_cast

<

const

size_t

*>

data

)[

1

];

FOLLY_FALLTHROUGH

case

1

ml_

data_

=

*

reinterpret_cast

<

Char

**>

const_cast

<

Char

*>

data

));

FOLLY_FALLTHROUGH

case

0

break

}

}

else

#endif

{

if

size

!=

0

{

fbstring_detail

::

podCopy

data

data

+

size

small_

);

}

}

setSmallSize

size

);

}

首先,如果傳入的字串地址是記憶體對齊的,

則配合 reinterpret_cast 進行 word-wise copy,提高效率。

否則,呼叫 podCopy 進行 memcpy。

最後,透過 setSmallSize 設定 small string 的 size。

setSmallSize :

void

setSmallSize

size_t

s

{

// Warning: this should work with uninitialized strings too,

// so don‘t assume anything about the previous value of

// small_[maxSmallSize]。

assert

s

<=

maxSmallSize

);

constexpr

auto

shift

=

kIsLittleEndian

0

2

small_

maxSmallSize

=

char

((

maxSmallSize

-

s

<<

shift

);

small_

s

=

’\0‘

assert

category

()

==

Category

::

isSmall

&&

size

()

==

s

);

}

之前提到過,small strings 存放的 size 不是真正的 size,是

maxSmallSize - size

,這樣做的原因是可以 small strings 可以多儲存一個位元組 。

因為假如儲存 size 的話,small中最後兩個位元組就得是\0 和 size,但是儲存maxSmallSize - size,當 size == maxSmallSize 時,small的最後一個位元組恰好也是\0。

initMedium

template

<

class

Char

>

FOLLY_NOINLINE

inline

void

fbstring_core

<

Char

>::

initMedium

const

Char

*

const

data

const

size_t

size

{

// Medium strings are allocated normally。 Don’t forget to

// allocate one extra Char for the terminating null。

auto

const

allocSize

=

goodMallocSize

((

1

+

size

*

sizeof

Char

));

ml_

data_

=

static_cast

<

Char

*>

checkedMalloc

allocSize

));

if

FOLLY_LIKELY

size

>

0

))

{

fbstring_detail

::

podCopy

data

data

+

size

ml_

data_

);

}

ml_

size_

=

size

ml_

setCapacity

allocSize

/

sizeof

Char

-

1

Category

::

isMedium

);

ml_

data_

size

=

‘\0’

}

folly 會透過 canNallocx 函式檢測是否使用 jemalloc,如果是,會使用 jemalloc 來提高記憶體分配的效能。關於 jemalloc 我也不是很熟悉,感興趣的可以查查,有很多資料。

所有的動態記憶體分配都會呼叫 goodMallocSize,獲取一個對 jemalloc 友好的值。

再透過 checkedMalloc 真正申請記憶體,存放字串。

呼叫 podCopy 進行 memcpy,與 initSmall 的 podCopy 一樣。

最後再設定 size、capacity、category 和\0。

initLarge

template

<

class

Char

>

FOLLY_NOINLINE

inline

void

fbstring_core

<

Char

>::

initLarge

const

Char

*

const

data

const

size_t

size

{

// Large strings are allocated differently

size_t

effectiveCapacity

=

size

auto

const

newRC

=

RefCounted

::

create

data

&

effectiveCapacity

);

ml_

data_

=

newRC

->

data_

ml_

size_

=

size

ml_

setCapacity

effectiveCapacity

Category

::

isLarge

);

ml_

data_

size

=

‘\0’

}

與 medium strings 最大的不同是會透過 RefCounted::create 建立 RefCounted 用於共享字串:

struct

RefCounted

{

std

::

atomic

<

size_t

>

refCount_

Char

data_

1

];

constexpr

static

size_t

getDataOffset

()

{

return

offsetof

RefCounted

data_

);

}

static

RefCounted

*

create

size_t

*

size

{

const

size_t

allocSize

=

goodMallocSize

getDataOffset

()

+

*

size

+

1

*

sizeof

Char

));

auto

result

=

static_cast

<

RefCounted

*>

checkedMalloc

allocSize

));

result

->

refCount_

store

1

std

::

memory_order_release

);

*

size

=

allocSize

-

getDataOffset

())

/

sizeof

Char

-

1

return

result

}

static

RefCounted

*

create

const

Char

*

data

size_t

*

size

{

const

size_t

effectiveSize

=

*

size

auto

result

=

create

size

);

if

FOLLY_LIKELY

effectiveSize

>

0

))

{

fbstring_detail

::

podCopy

data

data

+

effectiveSize

result

->

data_

);

}

return

result

}

};

需要注意的是:

ml*。data*指向的是 RefCounted。data_。

getDataOffset()用 offsetof 函式獲取 data*在 RefCounted 結構體內的偏移,

Char data*[1]

為 flexible array,存放字串。

注意對

std::atomic refCount_

進行原子操作的 c++ memory model :

store,設定引用數為 1 : std::memory_order_release

load,獲取當前共享字串的引用數: std::memory_order_acquire

add/sub。增加/減少一個引用 : std::memory_order_acq_rel

c++ memory model 是另外一個比較大的話題,可以參考:

incubator-brpc

Memory model synchronization modes

一文帶你看懂 C++11 的記憶體模型

C++11 中的記憶體模型下篇 - C++11 支援的幾種記憶體模型

特殊的建構函式 —— 不複製使用者傳入的字串

上面的三種構造,都是將應用程式傳入的字串,不管使用 word-wise copy 還是 memcpy,複製到 fbstring_core 中,且在 medium 和 large 的情況下,需要動態分配記憶體。

fbstring 提供了一個特殊的建構函式,讓 fbstring_core 接管應用程式自己分配的記憶體。

basic_fbstring 的建構函式,並呼叫 fbstring_core 相應的建構函式。

注意這裡 AcquireMallocatedString 為 enum class,比使用 int 和 bool 更可讀。

/**

* Defines a special acquisition method for constructing fbstring

* objects。 AcquireMallocatedString means that the user passes a

* pointer to a malloc-allocated string that the fbstring object will

* take into custody。

*/

enum

class

AcquireMallocatedString

{};

// Nonstandard constructor

basic_fbstring

value_type

*

s

size_type

n

size_type

c

AcquireMallocatedString

a

store_

s

n

c

a

{

}

basic_fbstring 呼叫相應的 fbstring_core 建構函式:

// Snatches a previously mallocated string。 The parameter “size”

// is the size of the string, and the parameter “allocatedSize”

// is the size of the mallocated block。 The string must be

// \0-terminated, so allocatedSize >= size + 1 and data[size] == ‘\0’。

//

// So if you want a 2-character string, pass malloc(3) as “data”,

// pass 2 as “size”, and pass 3 as “allocatedSize”。

fbstring_core

Char

*

const

data

const

size_t

size

const

size_t

allocatedSize

AcquireMallocatedString

{

if

size

>

0

{

FBSTRING_ASSERT

allocatedSize

>=

size

+

1

);

FBSTRING_ASSERT

data

size

==

‘\0’

);

// Use the medium string storage

ml_

data_

=

data

ml_

size_

=

size

// Don‘t forget about null terminator

ml_

setCapacity

allocatedSize

-

1

Category

::

isMedium

);

}

else

{

// No need for the memory

free

data

);

reset

();

}

}

可以看出這裡沒有複製字串的過程,而是直接接管了上游傳遞過來的指標指向的記憶體。但是,正如註釋說的,這裡直接使用了 medium strings 的儲存方式。

比如 folly/io/IOBuf。cpp 中的呼叫:

// Ensure NUL terminated

*

writableTail

()

=

0

fbstring

str

reinterpret_cast

<

char

*>

writableData

()),

length

(),

capacity

(),

AcquireMallocatedString

());

字串複製

同初始化,也是根據不同的字串型別,呼叫不同的函式:

fbstring_core

const

fbstring_core

&

rhs

{

assert

&

rhs

!=

this

);

switch

rhs

category

())

{

case

Category

::

isSmall

copySmall

rhs

);

break

case

Category

::

isMedium

copyMedium

rhs

);

break

case

Category

::

isLarge

copyLarge

rhs

);

break

default

folly

::

assume_unreachable

();

}

}

copySmall

template

<

class

Char

>

inline

void

fbstring_core

<

Char

>::

copySmall

const

fbstring_core

&

rhs

{

// Just write the whole thing, don’t look at details。 In

// particular we need to copy capacity anyway because we want

// to set the size (don‘t forget that the last character,

// which stores a short string’s length, is shared with the

// ml_。capacity field)。

ml_

=

rhs

ml_

}

正如註釋中所說,雖然 small strings 的情況下,字串儲存在 small中,但是我們只需要把 ml直接賦值即可,因為在一個 union 中。

copyMedium

template

<

class

Char

>

FOLLY_NOINLINE

inline

void

fbstring_core

<

Char

>::

copyMedium

const

fbstring_core

&

rhs

{

// Medium strings are copied eagerly。 Don‘t forget to allocate

// one extra Char for the null terminator。

auto

const

allocSize

=

goodMallocSize

((

1

+

rhs

ml_

size_

*

sizeof

Char

));

ml_

data_

=

static_cast

<

Char

*>

checkedMalloc

allocSize

));

// Also copies terminator。

fbstring_detail

::

podCopy

rhs

ml_

data_

rhs

ml_

data_

+

rhs

ml_

size_

+

1

ml_

data_

);

ml_

size_

=

rhs

ml_

size_

ml_

setCapacity

allocSize

/

sizeof

Char

-

1

Category

::

isMedium

);

}

medium strings 是 eager copy,所以就是“深複製”:

為字串分配空間、複製

賦值 size、capacity、category。

copyLarge

template

<

class

Char

>

FOLLY_NOINLINE

inline

void

fbstring_core

<

Char

>::

copyLarge

const

fbstring_core

&

rhs

{

// Large strings are just refcounted

ml_

=

rhs

ml_

RefCounted

::

incrementRefs

ml_

data_

);

}

large strings 的 copy 過程很直觀,因為是 COW 方式:

直接賦值 ml,內含指向共享字串的指標。

共享字串的引用計數加 1。

incrementRefs 和內部呼叫 fromData 這兩個個函式值得看一下:

static

RefCounted

*

fromData

Char

*

p

{

return

static_cast

<

RefCounted

*>

static_cast

<

void

*>

static_cast

<

unsigned

char

*>

static_cast

<

void

*>

p

))

-

getDataOffset

()));

}

static

void

incrementRefs

Char

*

p

{

fromData

p

->

refCount_

fetch_add

1

std

::

memory_order_acq_rel

);

}

因為 ml中指向的是 RefCounted 的 data[1],所以我們需要透過 fromData 來找到 data_所屬的 RefCounted 的地址。我把 fromData 函式內的運算拆開:

static

RefCounted

*

fromData

Char

*

p

{

// 轉換data_[1]的地址

void

*

voidDataAddr

=

static_cast

<

void

*>

p

);

unsigned

char

*

unsignedDataAddr

=

static_cast

<

unsigned

char

*>

voidDataAddr

);

// 獲取data_[1]在結構體的偏移量再相減,得到的就是所屬RefCounted的地址

unsigned

char

*

unsignedRefAddr

=

unsignedDataAddr

-

getDataOffset

();

void

*

voidRefAddr

=

static_cast

<

void

*>

unsignedRefAddr

);

RefCounted

*

refCountAddr

=

static_cast

<

RefCounted

*>

voidRefAddr

);

return

refCountAddr

}

C++ folly庫解讀(一) Fbstring —— 一個完美替代std::string的庫

值得關注的是如何轉換不同型別結構體的指標並做運算,這裡的做法是

Char* -> void* -> unsigned char* -> 與size_t做減法 -> void * -> RefCounted*

析構

~

fbstring_core

()

noexcept

{

if

category

()

==

Category

::

isSmall

{

return

}

destroyMediumLarge

();

}

如果是 small 型別,直接返回,因為利用的是棧空間。

否則,針對 medium 和 large,呼叫 destroyMediumLarge。

FOLLY_MALLOC_NOINLINE

void

destroyMediumLarge

()

noexcept

{

auto

const

c

=

category

();

FBSTRING_ASSERT

c

!=

Category

::

isSmall

);

if

c

==

Category

::

isMedium

{

free

ml_

data_

);

}

else

{

RefCounted

::

decrementRefs

ml_

data_

);

}

}

medium : free 動態分配的字串記憶體即可。

large : 呼叫 decrementRefs,針對共享字串進行操作。

static

void

decrementRefs

Char

*

p

{

auto

const

dis

=

fromData

p

);

size_t

oldcnt

=

dis

->

refCount_

fetch_sub

1

std

::

memory_order_acq_rel

);

FBSTRING_ASSERT

oldcnt

>

0

);

if

oldcnt

==

1

{

free

dis

);

}

}

邏輯也很清晰:先對引用計數減 1,如果本身就只有 1 個引用,那麼直接 free 掉整個 RefCounted。

COW

最重要的一點,也是 large strings 獨有的,就是 COW。 任何針對字串寫的操作,都會觸發 COW,包括前面舉過的[]操作,例如:

non-const

at(size_n)

non-const

operator[](size_type pos)

operator+

append

……

我們舉個例子,比如

non-const

operator[](size_type pos)

non-const operator[](size_type pos)

reference

operator

[](

size_type

pos

{

return

*

begin

()

+

pos

);

}

iterator

begin

()

{

return

store_

mutableData

();

}

來重點看下 mutableData() :

Char

*

mutableData

()

{

switch

category

())

{

case

Category

::

isSmall

return

small_

case

Category

::

isMedium

return

ml_

data_

case

Category

::

isLarge

return

mutableDataLarge

();

}

fbstring_detail

::

assume_unreachable

();

}

template

<

class

Char

>

inline

Char

*

fbstring_core

<

Char

>::

mutableDataLarge

()

{

FBSTRING_ASSERT

category

()

==

Category

::

isLarge

);

if

RefCounted

::

refs

ml_

data_

>

1

{

// Ensure unique。

unshare

();

}

return

ml_

data_

}

同樣是分三種情況。small 和 medium 直接返回字串的地址,large 會呼叫 mutableDataLarge(),

可以看出,如果引用數大於 1,會進行 unshare 操作 :

void

unshare

size_t

minCapacity

=

0

);

template

<

class

Char

>

FOLLY_MALLOC_NOINLINE

inline

void

fbstring_core

<

Char

>::

unshare

size_t

minCapacity

{

FBSTRING_ASSERT

category

()

==

Category

::

isLarge

);

size_t

effectiveCapacity

=

std

::

max

minCapacity

ml_

capacity

());

auto

const

newRC

=

RefCounted

::

create

&

effectiveCapacity

);

// If this fails, someone placed the wrong capacity in an

// fbstring。

FBSTRING_ASSERT

effectiveCapacity

>=

ml_

capacity

());

// Also copies terminator。

fbstring_detail

::

podCopy

ml_

data_

ml_

data_

+

ml_

size_

+

1

newRC

->

data_

);

RefCounted

::

decrementRefs

ml_

data_

);

ml_

data_

=

newRC

->

data_

ml_

setCapacity

effectiveCapacity

Category

::

isLarge

);

// size_ remains unchanged。

}

基本思路:

建立新的 RefCounted。

複製字串。

對原有的共享字串減少一個引用 decrementRefs,這個函式在上面的析構小節裡分析過。

設定 ml_的 data、capacity、category。

注意此時還不會設定 size,因為還不知道應用程式對字串進行什麼修改。

non-const 與 const

大家可能注意到了,上面的 at 和[]強調了 non-const,這是因為 const-qualifer 針對這兩個呼叫不會觸發 COW ,還以[]為例:

// C++11 21。4。5 element access:

const_reference

operator

[](

size_type

pos

const

{

return

*

begin

()

+

pos

);

}

const_iterator

begin

()

const

{

return

store_

data

();

}

// In C++11 data() and c_str() are 100% equivalent。

const

Char

*

data

()

const

{

return

c_str

();

}

const

Char

*

c_str

()

const

{

const

Char

*

ptr

=

ml_

data_

// With this syntax, GCC and Clang generate a CMOV instead of a branch。

ptr

=

category

()

==

Category

::

isSmall

small_

ptr

return

ptr

}

可以看出區別,non-const 版本的 begin()中呼叫的是 mutableData(),而 const-qualifer 版本呼叫的是 data() -> c_str(),而 c_str()直接返回的字串地址。(插一句,貌似之前fbstring的c_str()的實現是lazy null terminator的,可以看下這位大佬寫的文章:)

所以,當字串用到[]、at 且不需要寫操作時,最好用 const-qualifer。

我們拿 folly 自帶的

benchmark 工具

測試一下:

#include

“folly/Benchmark。h”

#include

“folly/FBString。h”

#include

“folly/container/Foreach。h”

using

namespace

std

using

namespace

folly

BENCHMARK

nonConstFbstringAt

n

{

::

folly

::

fbstring

str

“fbstring is a drop-in replacement for std::string。 The main benefit of fbstring is significantly increased ”

“performance on virtually all important primitives。 This is achieved by using a three-tiered storage strategy ”

“and by cooperating with the memory allocator。 In particular, fbstring is designed to detect use of jemalloc and ”

“cooperate with it to achieve significant improvements in speed and memory usage。”

);

FOR_EACH_RANGE

i

0

n

{

char

&

s

=

str

2

];

doNotOptimizeAway

s

);

}

}

BENCHMARK_DRAW_LINE

();

BENCHMARK_RELATIVE

constFbstringAt

n

{

const

::

folly

::

fbstring

str

“fbstring is a drop-in replacement for std::string。 The main benefit of fbstring is significantly increased ”

“performance on virtually all important primitives。 This is achieved by using a three-tiered storage strategy ”

“and by cooperating with the memory allocator。 In particular, fbstring is designed to detect use of jemalloc and ”

“cooperate with it to achieve significant improvements in speed and memory usage。”

);

FOR_EACH_RANGE

i

0

n

{

const

char

&

s

=

str

2

];

doNotOptimizeAway

s

);

}

}

int

main

()

{

runBenchmarks

();

}

結果是 constFbstringAt 比 nonConstFbstringAt 快了 175%

============================================================================

delve_folly

/

main

cc

relative

time

/

iter

iters

/

s

============================================================================

nonConstFbstringAt

39。85

ns

25。10

M

——————————————————————————————————————

constFbstringAt

175。57

%

22。70

ns

44。06

M

============================================================================

Realloc

reserve、operator+等操作,可能會涉及到記憶體重新分配,最終呼叫的都是 memory/Malloc。h 中的 smartRealloc:

inline

void

*

checkedRealloc

void

*

ptr

size_t

size

{

void

*

p

=

realloc

ptr

size

);

if

p

{

throw_exception

<

std

::

bad_alloc

>

();

}

return

p

}

/**

* This function tries to reallocate a buffer of which only the first

* currentSize bytes are used。 The problem with using realloc is that

* if currentSize is relatively small _and_ if realloc decides it

* needs to move the memory chunk to a new buffer, then realloc ends

* up copying data that is not used。 It’s generally not a win to try

* to hook in to realloc() behavior to avoid copies - at least in

* jemalloc, realloc() almost always ends up doing a copy, because

* there is little fragmentation / slack space to take advantage of。

*/

FOLLY_MALLOC_CHECKED_MALLOC

FOLLY_NOINLINE

inline

void

*

smartRealloc

void

*

p

const

size_t

currentSize

const

size_t

currentCapacity

const

size_t

newCapacity

{

assert

p

);

assert

currentSize

<=

currentCapacity

&&

currentCapacity

<

newCapacity

);

auto

const

slack

=

currentCapacity

-

currentSize

if

slack

*

2

>

currentSize

{

// Too much slack, malloc-copy-free cycle:

auto

const

result

=

checkedMalloc

newCapacity

);

std

::

memcpy

result

p

currentSize

);

free

p

);

return

result

}

// If there‘s not too much slack, we realloc in hope of coalescing

return

checkedRealloc

p

newCapacity

);

}

從註釋和程式碼看為什麼函式起名叫

smart

Realloc :

如果(the currentCapacity - currentSize) _ 2 > currentSize,即 currentSize < 2/3 _ capacity,說明當前分配的記憶體利用率較低,此時認為如果使用 realloc 並且 realloc 決定拷貝當前記憶體到新記憶體,成本會高於直接 malloc(newCapacity) + memcpy + free(old_memory)。

否則直接 realloc。

其他

__builtin_expect

給編譯器提供分支預測資訊。原型為:

long

__builtin_expect

long

exp

long

c

表示式的返回值為 exp 的值,跟 c 無關。

我們預期 exp 的值是 c。例如下面的例子,我們預期 x 的值是 0,所以這裡提示編譯器,只有很小的機率會呼叫到 foo()

if

__builtin_expect

x

0

))

foo

();

再比如判斷指標是否為空:

if

__builtin_expect

ptr

!=

NULL

1

))

foo

*

ptr

);

在 fbstring 中也用到了

builtin_expect,例如建立 RefCounted 的函式 (FOLLY_LIKELY 包裝了一下

builtin_expect):

#if __GNUC__

#define FOLLY_DETAIL_BUILTIN_EXPECT(b, t) (__builtin_expect(b, t))

#else

#define FOLLY_DETAIL_BUILTIN_EXPECT(b, t) b

#endif

// Likeliness annotations

//

// Useful when the author has better knowledge than the compiler of whether

// the branch condition is overwhelmingly likely to take a specific value。

//

// Useful when the author has better knowledge than the compiler of which code

// paths are designed as the fast path and which are designed as the slow path,

// and to force the compiler to optimize for the fast path, even when it is not

// overwhelmingly likely。

#define FOLLY_LIKELY(x) FOLLY_DETAIL_BUILTIN_EXPECT((x), 1)

#define FOLLY_UNLIKELY(x) FOLLY_DETAIL_BUILTIN_EXPECT((x), 0)

static

RefCounted

*

create

const

Char

*

data

size_t

*

size

{

const

size_t

effectiveSize

=

*

size

auto

result

=

create

size

);

if

FOLLY_LIKELY

effectiveSize

>

0

))

{

// __builtin_expect

fbstring_detail

::

podCopy

data

data

+

effectiveSize

result

->

data_

);

}

return

result

}

從彙編角度來說,會將可能性更大的彙編緊跟著前面的彙編,防止無效指令的載入。可以參考:

likely() and unlikely()

What is the advantage of GCC's __builtin_expect in if else statements?

CMOV 指令

conditional move,條件傳送。類似於 MOV 指令,但是依賴於 RFLAGS 暫存器內的狀態。如果條件沒有滿足,該指令不會有任何效果。

CMOV 的優點是可以避免分支預測,避免分支預測錯誤對 CPU 流水線的影響

。詳細可以看這篇文件:

amd-cmovcc.pdf

fbstring 在一些場景會提示編譯器生成 CMOV 指令,例如:

const

Char

*

c_str

()

const

{

const

Char

*

ptr

=

ml_

data_

// With this syntax, GCC and Clang generate a CMOV instead of a branch。

ptr

=

category

()

==

Category

::

isSmall

small_

ptr

return

ptr

}

builtin_unreachable && assume(0)

如果程式執行到了

builtin_unreachable 和

assume(0) ,那麼會出現未定義的行為。例如**builtin_unreachable 出現在一個不會返回的函式後面,而且這個函式沒有宣告為

**attribute\_\_((noreturn))

。例如

6.59 Other Built-in Functions Provided by GCC

給出的例子 :

void

function_that_never_returns

void

);

int

g

int

c

{

if

c

{

return

1

}

else

{

function_that_never_returns

();

__builtin_unreachable

();

}

}

如果不加

__builtin_unreachable ();

,會報

error: control reaches end of non-void function [-Werror=return-type]

folly 將

builtin_unreachable 和

assume(0) 封裝成了

assume_unreachable

[[noreturn]]

FOLLY_ALWAYS_INLINE

void

assume_unreachable

()

{

assume

false

);

// Do a bit more to get the compiler to understand

// that this function really will never return。

#if defined(__GNUC__)

__builtin_unreachable

();

#elif defined(_MSC_VER)

__assume

0

);

#else

// Well, it’s better than nothing。

std

::

abort

();

#endif

}

在 fbstring 的一些特性場景,比如 switch 判斷 category 中用到。這是上面提到過的 mutableData() :

Char

*

mutableData

()

{

switch

category

())

{

case

Category

::

isSmall

return

small_

case

Category

::

isMedium

return

ml_

data_

case

Category

::

isLarge

return

mutableDataLarge

();

}

folly

::

assume_unreachable

();

}

jemalloc

官網

API 文件

大致的演算法和原理可以參考 facebook 的這篇部落格 :

Scalable memory allocation using jemalloc

find 演算法

使用的簡化的 Boyer-Moore 演算法,文件說明是在查詢成功的情況下比 std::string 的 find 快 30 倍。benchmark 程式碼在

FBStringBenchmark.cpp

我自己測試的情況貌似是搜尋長字串的情況會更好些。

判斷大小端

// It‘s MSVC, so we just have to guess 。。。 and allow an override

#ifdef _MSC_VER

# ifdef FOLLY_ENDIAN_BE

static

constexpr

auto

kIsLittleEndian

=

false

# else

static

constexpr

auto

kIsLittleEndian

=

true

# endif

#else

static

constexpr

auto

kIsLittleEndian

=

__BYTE_ORDER__

==

__ORDER_LITTLE_ENDIAN__

#endif

BYTE_ORDER為預定義宏:

,值是

ORDER_LITTLE_ENDIAN

ORDER_BIG_ENDIAN

ORDER_PDP_ENDIAN

中的一個。

一般會這麼使用:

/* Test for a little-endian machine */

#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__

c++20 引入了std::endian,判斷會更加方便。

參考資料:

《Linux 多執行緒服務端程式設計:使用 muduo C++ 網路庫》 by 陳碩

漫步Facebook開源C++庫folly(1):string類的設計

(完)