在牛客網上看到一題字符串拷貝相關的題目,深入挖掘了下才發現原來C++中string的實現還是有好幾種優化方法的。
原始題目是這樣的:
關於代碼輸出正確的結果是()(Linux g++ 環境下編譯運行)
int main(int argc, char *argv[]) { string a="hello world"; string b=a; if (a.c_str()==b.c_str()) { cout<<"true"<<endl; } else cout<<"false"<<endl; string c=b; c=""; if (a.c_str()==b.c_str()) { cout<<"true"<<endl; } else cout<<"false"<<endl; a=""; if (a.c_str()==b.c_str()) { cout<<"true"<<endl; } else cout<<"false"<<endl; return 0; }
這段程序的輸出結果和編譯器有關,在老版本(5.x之前)的GCC上,輸出是true true false
,而在VS上輸出是false false false
。這是由於不同STL標准庫對string
的實現方法不同導致的。
簡而言之,目前各種STL實現中,對string
的實現有兩種不同的優化策略,即COW(Copy On Write)和SSO(Small String Optimization)。string
也是一個類,類的拷貝操作有兩種策略——深拷貝及淺拷貝。我們自己寫的類默認情況下都是淺拷貝的,可以理解為指針的復制,要實現深拷貝需要重載賦值操作符或拷貝構造函數。不過對於string
來說,大部分情況下我們用賦值操作是想實現深拷貝的,故所有實現中string
的拷貝均為深拷貝。
最簡單的深拷貝就是直接new一個對象,然后把數據復制一遍,不過這樣做效率很低,STL中對此進行了優化,基本策略就是上面提到的COW和SSO。
我們先以COW為例分析一下std::string
對std::string的感性認識
- string類中有一個私有成員,其實是一個char*,記錄從堆上分 配內存的地址,其在構造時分配內存,在析構時釋放內存
- 因為是從堆上分配內存,所以string類在維護這塊內存上是格外小心的
- string類在返回這塊內存地址時,只返回const char*,也就是只讀的
- 成員函數:const char* c_str() const;
- 如果要寫,則只能通過string提供的方法進行數據的改寫。
對std::string的理性認識
- Q1. Copy-On-Write的原理是什么?
-
Copy-On-Write一定使用了“引用計數”,必然有一個變量類似於RefCnt
-
當第一個string對象str1構造時,string的構造函數會根據傳入的參數從堆上分配內存
-
當有其它string對象復制str1時,這個RefCnt會自動加1
-
當有對象析構時,這個計數會減1;直到最后一個對象析構時,RefCnt為0,此時,程序才會真正的釋放這塊從堆上分配的內存
-
Q1.1 RefCnt該存在在哪里呢?
-
如果存放在string類中,那么每個string的實例都各自擁有自己的RefCnt,根本不能共有一個 RefCnt
-
如果是聲明成全局變量,或是靜態成員,那就是所有的string類共享一個了,這也不行
-
-
-
- Q2. string類在什么情況下才共享內存的?
-
根據常理和邏輯,發生復制的時候
-
1)以一個對象構造自己(復制構造函數) 只需要在string類的拷貝構造函數中做點處理,讓其引用計數累加
-
2)以一個對象賦值(重載賦值運算符)
-
-
- Q3. string類在什么情況下觸發寫時才拷貝?
-
在共享同一塊內存的類發生內容改變時,才會發生Copy-On-Write
-
比如string類的 []、=、+=、+、操作符賦值,還有一些string類中諸如insert、replace、append等成員函數
-
- Q4. Copy-On-Write時,發生了什么?
- 引用計數RefCnt 大於1,表示這個內存是被共享的
-
if ( --RefCnt>0 ) { char* tmp = (char*) malloc(strlen(_Ptr)+1); strcpy(tmp, _Ptr); _Ptr = tmp; }
- Q5. Copy-On-Write的具體實現是怎么樣的?
- h1、h2、h3共享同一塊內存, w1、w2共享同一塊內存
- 如何產生這兩個引用計數呢?
-
string h1 = “hello”; string h2= h1; string h3; h3 = h2; string w1 = “world”; string w2(“”); w2=w1;
copy-on-write的具體實現分析
- String類創建的對象的內存是在堆上動態分配的,既然共享內存的各個對象指向的是同一個內存區,那我們就在這塊共享內存上多分配一點空間來存放這個引用計數RefCnt
- 這樣一來,所有共享一塊內存區的對象都有同樣的一個引用計數
解決方案分析
當為string對象分配內存時,我們要多分配一個空間用來存放這個引用計數的值,只要發生拷貝構造或賦值時,這個內存的值就會加1。而在內容修改時,string類為查看這個引用計數是否大於1,如果refcnt大於1,表示有人在共享這塊內存,那么自己需要先做一份拷貝,然后把引用計數減去1,再把數據拷貝過來。
根據以上分析,我們可以試着寫一下cow的代碼 :
class String { public: String() : _pstr(new char[5]()) { _pstr += 4; initRefcount(); } String(const char * pstr) : _pstr(new char[strlen(pstr) + 5]()) { _pstr += 4; initRefcount(); strcpy(_pstr, pstr); } String(const String & rhs) : _pstr(rhs._pstr) { increaseRefcount(); } String & operator=(const String & rhs) { if(this != & rhs) // 自復制 { release(); //回收左操作數的空間 _pstr = rhs._pstr; // 進行淺拷貝 increaseRefcount(); } return *this; } ~String() { release(); } size_t refcount() const { return *((int *)(_pstr - 4));} size_t size() const { return strlen(_pstr); } const char * c_str() const { return _pstr; } //問題: 下標訪問運算符不能區分讀操作和寫操作 char & operator[](size_t idx) { if(idx < size()) { if(refcount() > 1) {// 進行深拷貝 decreaseRefcount(); char * tmp = new char[size() + 5](); tmp += 4; strcpy(tmp, _pstr); _pstr = tmp; initRefcount(); } return _pstr[idx]; } else { static char nullchar = '\0'; return nullchar; } } const char & operator[](size_t idx) const { cout << "const char & operator[](size_t) const " << endl; return _pstr[idx]; } private: void initRefcount() { *((int*)(_pstr - 4)) = 1; } void increaseRefcount() { ++*((int *)(_pstr - 4)); } void decreaseRefcount() { --*((int *)(_pstr - 4)); } void release() { decreaseRefcount(); if(refcount() == 0) { delete [] (_pstr - 4); cout << ">> delete heap data!" << endl; } } friend std::ostream & operator<<(std::ostream & os, const String & rhs); private: char * _pstr; }; std::ostream & operator<<(std::ostream & os, const String & rhs) { os << rhs._pstr; return os; } int main(void) { String s1; String s2(s1); cout << "s1 = " << s1 << endl; cout << "s2 = " << s2 << endl; cout << "s1's refcount = " << s1.refcount() << endl; String s3 = "hello,world"; String s4(s3); cout << "s3 = " << s3 << endl; cout << "s4 = " << s4 << endl; cout << "s3's refcount = " << s3.refcount() << endl; printf("s3's address = %p\n", s3.c_str()); printf("s4's address = %p\n", s4.c_str()); cout << endl; String s5 = "hello,shenzheng"; cout << "s5 = " << s5 << endl; s5 = s4; cout << endl; cout << "s5 = " << s5 << endl; cout << "s3 = " << s3 << endl; cout << "s4 = " << s4 << endl; cout << "s3's refcount = " << s3.refcount() << endl; printf("s5's address = %p\n", s5.c_str()); printf("s3's address = %p\n", s3.c_str()); printf("s4's address = %p\n", s4.c_str()); cout << endl; cout << "執行寫操作之后:" << endl; s5[0] = 'X'; cout << "s5 = " << s5 << endl; cout << "s3 = " << s3 << endl; cout << "s4 = " << s4 << endl; cout << "s5's refcount = " << s5.refcount() << endl; cout << "s3's refcount = " << s3.refcount() << endl; printf("s5's address = %p\n", s5.c_str()); printf("s3's address = %p\n", s3.c_str()); printf("s4's address = %p\n", s4.c_str()); cout << endl; cout << "執行讀操作: " << endl; cout << "s3[0] = " << s3[0] << endl; cout << "s5 = " << s5 << endl; cout << "s3 = " << s3 << endl; cout << "s4 = " << s4 << endl; cout << "s5's refcount = " << s5.refcount() << endl; cout << "s3's refcount = " << s3.refcount() << endl; printf("s5's address = %p\n", s5.c_str()); printf("s3's address = %p\n", s3.c_str()); printf("s4's address = %p\n", s4.c_str()); cout << endl; const String s6("hello"); cout << s6[0] << endl; return 0; }
事實上,上面的代碼還是由缺陷的,[ ]運算符不能區分讀操作或者寫操作。
為了解決這個問題,可以使用代理類來實現:
1、 重載operator=和operator<<
#include <stdio.h> #include <string.h> #include <iostream> using std::cout; using std::endl; class String { class CharProxy { public: CharProxy(size_t idx, String & self) : _idx(idx) , _self(self) {} CharProxy & operator=(const char & ch); friend std::ostream & operator<<(std::ostream & os, const CharProxy & rhs); private: size_t _idx; String & _self; }; friend std::ostream & operator<<(std::ostream & os, const CharProxy & rhs); public: String() : _pstr(new char[5]()) { _pstr += 4; initRefcount(); } String(const char * pstr) : _pstr(new char[strlen(pstr) + 5]()) { _pstr += 4; initRefcount(); strcpy(_pstr, pstr); } //代碼本身就能解釋自己 --> 自解釋 String(const String & rhs) : _pstr(rhs._pstr) //淺拷貝 { increaseRefcount(); } String & operator=(const String & rhs) { if(this != & rhs) // 自復制 { release(); //回收左操作數的空間 _pstr = rhs._pstr; // 進行淺拷貝 increaseRefcount(); } return *this; } ~String() { release(); } size_t refcount() const { return *((int *)(_pstr - 4));} size_t size() const { return strlen(_pstr); } const char * c_str() const { return _pstr; } //自定義類型 CharProxy operator[](size_t idx) { return CharProxy(idx, *this); } #if 0 //問題: 下標訪問運算符不能區分讀操作和寫操作 char & operator[](size_t idx) { if(idx < size()) { if(refcount() > 1) {// 進行深拷貝 decreaseRefcount(); char * tmp = new char[size() + 5](); tmp += 4; strcpy(tmp, _pstr); _pstr = tmp; initRefcount(); } return _pstr[idx]; } else { static char nullchar = '\0'; return nullchar; } } #endif const char & operator[](size_t idx) const { cout << "const char & operator[](size_t) const " << endl; return _pstr[idx]; } private: void initRefcount() { *((int*)(_pstr - 4)) = 1; } void increaseRefcount() { ++*((int *)(_pstr - 4)); } void decreaseRefcount() { --*((int *)(_pstr - 4)); } void release() { decreaseRefcount(); if(refcount() == 0) { delete [] (_pstr - 4); cout << ">> delete heap data!" << endl; } } friend std::ostream & operator<<(std::ostream & os, const String & rhs); private: char * _pstr; }; //執行寫(修改)操作 String::CharProxy & String::CharProxy::operator=(const char & ch) { if(_idx < _self.size()) { if(_self.refcount() > 1) { char * tmp = new char[_self.size() + 5](); tmp += 4; strcpy(tmp, _self._pstr); _self.decreaseRefcount(); _self._pstr = tmp; _self.initRefcount(); } _self._pstr[_idx] = ch;//執行修改 } return *this; } //執行讀操作 std::ostream & operator<<(std::ostream & os, const String::CharProxy & rhs) { os << rhs._self._pstr[rhs._idx]; return os; } std::ostream & operator<<(std::ostream & os, const String & rhs) { os << rhs._pstr; return os; } int main(void) { String s1; String s2(s1); cout << "s1 = " << s1 << endl; cout << "s2 = " << s2 << endl; cout << "s1's refcount = " << s1.refcount() << endl; String s3 = "hello,world"; String s4(s3); cout << "s3 = " << s3 << endl; cout << "s4 = " << s4 << endl; cout << "s3's refcount = " << s3.refcount() << endl; printf("s3's address = %p\n", s3.c_str()); printf("s4's address = %p\n", s4.c_str()); cout << endl; String s5 = "hello,shenzheng"; cout << "s5 = " << s5 << endl; s5 = s4; cout << endl; cout << "s5 = " << s5 << endl; cout << "s3 = " << s3 << endl; cout << "s4 = " << s4 << endl; cout << "s3's refcount = " << s3.refcount() << endl; printf("s5's address = %p\n", s5.c_str()); printf("s3's address = %p\n", s3.c_str()); printf("s4's address = %p\n", s4.c_str()); cout << endl; cout << "執行寫操作之后:" << endl; s5[0] = 'X';//char& --> 內置類型 //CharProxy cp = ch; cout << "s5 = " << s5 << endl; cout << "s3 = " << s3 << endl; cout << "s4 = " << s4 << endl; cout << "s5's refcount = " << s5.refcount() << endl; cout << "s3's refcount = " << s3.refcount() << endl; printf("s5's address = %p\n", s5.c_str()); printf("s3's address = %p\n", s3.c_str()); printf("s4's address = %p\n", s4.c_str()); cout << endl; cout << "執行讀操作: " << endl; cout << "s3[0] = " << s3[0] << endl; cout << "s5 = " << s5 << endl; cout << "s3 = " << s3 << endl; cout << "s4 = " << s4 << endl; cout << "s5's refcount = " << s5.refcount() << endl; cout << "s3's refcount = " << s3.refcount() << endl; printf("s5's address = %p\n", s5.c_str()); printf("s3's address = %p\n", s3.c_str()); printf("s4's address = %p\n", s4.c_str()); cout << endl; const String s6("hello"); cout << s6[0] << endl; return 0; }
2、代理模式:借助自定義嵌套類Char,可以不用重載operator<<和operator=
#include <stdio.h> #include <string.h> #include <iostream> using std::cout; using std::endl; class String { //設計模式之代理模式 class CharProxy { public: CharProxy(size_t idx, String & self) : _idx(idx) , _self(self) {} CharProxy & operator=(const char & ch); //執行讀操作 operator char() { cout << "operator char()" << endl; return _self._pstr[_idx]; } private: size_t _idx; String & _self; }; public: String() : _pstr(new char[5]()) { _pstr += 4; initRefcount(); } String(const char * pstr) : _pstr(new char[strlen(pstr) + 5]()) { _pstr += 4; initRefcount(); strcpy(_pstr, pstr); } //代碼本身就能解釋自己 --> 自解釋 String(const String & rhs) : _pstr(rhs._pstr) //淺拷貝 { increaseRefcount(); } String & operator=(const String & rhs) { if(this != & rhs) // 自復制 { release(); //回收左操作數的空間 _pstr = rhs._pstr; // 進行淺拷貝 increaseRefcount(); } return *this; } ~String() { release(); } size_t refcount() const { return *((int *)(_pstr - 4));} size_t size() const { return strlen(_pstr); } const char * c_str() const { return _pstr; } //自定義類型 CharProxy operator[](size_t idx) { return CharProxy(idx, *this); } #if 0 //問題: 下標訪問運算符不能區分讀操作和寫操作 char & operator[](size_t idx) { if(idx < size()) { if(refcount() > 1) {// 進行深拷貝 decreaseRefcount(); char * tmp = new char[size() + 5](); tmp += 4; strcpy(tmp, _pstr); _pstr = tmp; initRefcount(); } return _pstr[idx]; } else { static char nullchar = '\0'; return nullchar; } } #endif const char & operator[](size_t idx) const { cout << "const char & operator[](size_t) const " << endl; return _pstr[idx]; } private: void initRefcount() { *((int*)(_pstr - 4)) = 1; } void increaseRefcount() { ++*((int *)(_pstr - 4)); } void decreaseRefcount() { --*((int *)(_pstr - 4)); } void release() { decreaseRefcount(); if(refcount() == 0) { delete [] (_pstr - 4); cout << ">> delete heap data!" << endl; } } friend std::ostream & operator<<(std::ostream & os, const String & rhs); private: char * _pstr; }; //執行寫(修改)操作 String::CharProxy & String::CharProxy::operator=(const char & ch) { if(_idx < _self.size()) { if(_self.refcount() > 1) { char * tmp = new char[_self.size() + 5](); tmp += 4; strcpy(tmp, _self._pstr); _self.decreaseRefcount(); _self._pstr = tmp; _self.initRefcount(); } _self._pstr[_idx] = ch;//執行修改 } return *this; } std::ostream & operator<<(std::ostream & os, const String & rhs) { os << rhs._pstr; return os; } int main(void) { String s1; String s2(s1); cout << "s1 = " << s1 << endl; cout << "s2 = " << s2 << endl; cout << "s1's refcount = " << s1.refcount() << endl; String s3 = "hello,world"; String s4(s3); cout << "s3 = " << s3 << endl; cout << "s4 = " << s4 << endl; cout << "s3's refcount = " << s3.refcount() << endl; printf("s3's address = %p\n", s3.c_str()); printf("s4's address = %p\n", s4.c_str()); cout << endl; String s5 = "hello,shenzheng"; cout << "s5 = " << s5 << endl; s5 = s4; cout << endl; cout << "s5 = " << s5 << endl; cout << "s3 = " << s3 << endl; cout << "s4 = " << s4 << endl; cout << "s3's refcount = " << s3.refcount() << endl; printf("s5's address = %p\n", s5.c_str()); printf("s3's address = %p\n", s3.c_str()); printf("s4's address = %p\n", s4.c_str()); cout << endl; cout << "執行寫操作之后:" << endl; s5[0] = 'X';//char& --> 內置類型 //CharProxy cp = ch; cout << "s5 = " << s5 << endl; cout << "s3 = " << s3 << endl; cout << "s4 = " << s4 << endl; cout << "s5's refcount = " << s5.refcount() << endl; cout << "s3's refcount = " << s3.refcount() << endl; printf("s5's address = %p\n", s5.c_str()); printf("s3's address = %p\n", s3.c_str()); printf("s4's address = %p\n", s4.c_str()); cout << endl; cout << "執行讀操作: " << endl; cout << "s3[0] = " << s3[0] << endl; cout << "s5 = " << s5 << endl; cout << "s3 = " << s3 << endl; cout << "s4 = " << s4 << endl; cout << "s5's refcount = " << s5.refcount() << endl; cout << "s3's refcount = " << s3.refcount() << endl; printf("s5's address = %p\n", s5.c_str()); printf("s3's address = %p\n", s3.c_str()); printf("s4's address = %p\n", s4.c_str()); cout << endl; const String s6("hello"); cout << s6[0] << endl; return 0; }
運行結果:
s1= s2= s1.refcount=2 s3=helloworld s4=helloworld s1.refcount=2 s3.address=0x16b9054 s4.address=0x16b9054 s5 = Xelloworldjeiqjeiqoej >>delete heap data1 s5=helloworld s3=helloworld s4=helloworld s3.refcount = 1 s5.address=0x16b9054 s3.address=0x16b9054 s4.address=0x16b9054 執行讀操作 operator char() s3[0] = h s5 = helloworld s3 = helloworld s4 = helloworld s5.refcount = 1 s3.refcount = 1 s3.address=0x16b9054 s4.address=0x16b9054 const char 7 operator[](size_t)const h s6'address=0x7ffffdcdce40 >>delete heap data1 >>delete heap data1 >>delete heap data1 cthon@zrw:~/c++/20180614$ vim cowstring1.cc cthon@zrw:~/c++/20180614$ g++ cowstring1.cc cthon@zrw:~/c++/20180614$ ./a.out s1= s2= s1.refcount=2 s3=helloworld s4=helloworld s1.refcount=2 s3.address=0xb18054 s4.address=0xb18054 s5 = Xelloworldjeiqjeiqoej >>delete heap data1 s5=helloworld s3=helloworld s4=helloworld s3.refcount = 1 s5.address=0xb18054 s3.address=0xb18054 s4.address=0xb18054//這里s3/s4/s5指向同一個內存空間,發現沒,這就是cow的妙用 執行讀操作 operator char() s3[0] = h s5 = helloworld s3 = helloworld s4 = helloworld s5.refcount = 1 s3.refcount = 1 s3.address=0xb18054 s4.address=0xb18054 const char 7 operator[](size_t)const h s6'address=0x7ffe8bbeadc0 >>delete heap data1 >>delete heap data1 >>delete heap data1
那么實際的COW時怎么實現的呢,帶着疑問,我們看下面:
Scott Meyers在《Effective STL》[3]第15條提到std::string有很多實現方式,歸納起來有三類,而每類又有多種變化。
1、無特殊處理(eager copy),采用類似std::vector的數據結構。現在很少采用這種方式。
2、Copy-on-write(COW),g++的std::string一直采用這種方式,不過慢慢被SSO取代。
3、短字符串優化(SSO),利用string對象本身的空間來存儲短字符串。VisualC++2010、clang libc、linux gnu5.x之后都采用的這種方式。
VC++的std::string的大小跟編譯模式有關,表中的小的數字時release編譯,大的數字是debug編譯。因此debug和release不能混用。除此以外,其他庫的string大小是固定的。
這幾種實現方式都要保存三種數據:1、字符串本身(char*),2、字符串長度(size),3、字符串容量(capacity).
直接拷貝(eager copy)
類似std::vector的“三指針結構”:
class string { public : const _pointer data() const{ return start; } iterator begin(){ return start; } iterator end(){ return finish; } size_type size() const{ return finish - start; } size_type capacity()const{ return end_of_storage -start; } private: char* start; char* finish; char* end_of_storage; }
對象的大小是3個指針,在32位系統中是12字節,64位系統中是24字節。
Eager copy string 的另一種實現方式是把后兩個成員變量替換成整數,表示字符串的長度和容量:
class string { public : const _pointer data() const{ return start; } iterator begin(){ return start; } iterator end(){ return finish; } size_type size() const{ return size_; } size_type capacity()const{ return capacity; } private: char* start; size_t size_; size_t capacity; }
這種做法並沒有多大改變,因為size_t和char*是一樣大的。但是我們通常用不到單個幾百兆字節的字符串,那么可以在改變以下長度和容量的類型(從64bit整數改成32bit整數)。
class string { private: char* start; size_t size_; size_t capacity; }
新的string結構在64位系統中是16字節。
COW寫時復制(copy-on-write)
所謂COW就是指,復制的時候不立即申請新的空間,而是把這一過程延遲到寫操作的時候,因為在這之前,二者的數據是完全相同的,無需復制。這其實是一種廣泛采用的通用優化策略,它的核心思想是懶惰處理多個實體的資源請求,在多個實體之間共享某些資源,直到有實體需要對資源進行修改時,才真正為該實體分配私有的資源。
string對象里只放一個指針:
class string { sturuct { size_t size_; size_t capacity; size_t refcount; char* data[1];//變量長度 } char* start; } ;
COW的操作復雜度,卡被字符串是O(1),但拷貝之后第一次operator[]有可能是O(N)。
優點
1. 一方面減少了分配(和復制)大量資源帶來的瞬間延遲(注意僅僅是latency,但實際上該延遲被分攤到后續的操作中,其累積耗時很可能比一次統一處理的延遲要高,造成throughput下降是有可能的)
2. 另一方面減少不必要的資源分配。(例如在fork的例子中,並不是所有的頁面都需要復制,比如父進程的代碼段(.code)和只讀數據(.rodata)段,由於不允許修改,根本就無需復制。而如果fork后面緊跟exec的話,之前的地址空間都會廢棄,花大力氣的分配和復制只是徒勞無功。)
實現機制
COW的實現依賴於引用計數(reference count, rc
),初始時rc=1
,每次賦值復制時rc++
,當修改時,如果rc>1
,需要申請新的空間並復制一份原來的數據,並且rc--
,當rc==0
時,釋放原內存。
不過,實際的string
COW實現中,對於什么是”寫操作”的認定和我們的直覺是不同的,考慮以下代碼:
string a = "Hello"; string b = a; cout << b[0] << endl;
以上代碼顯然沒有修改string b
的內容,此時似乎a
和b
是可以共享一塊內存的,然而由於string
的operator[]
和at()
會返回某個字符的引用,此時無法准確的判斷程序是否修改了string
的內容,為了保證COW實現的正確性,string
只得統統認定operator[]
和at()
具有修改的“語義”。
這就導致string
的COW實現存在諸多弊端(除了上述原因外,還有線程安全的問題,可進一步閱讀文末參考資料),因此只有老版本的GCC編譯器和少數一些其他編譯器使用了此方式,VS、Clang++、GCC 5.x等編譯器均放棄了COW策略,轉為使用SSO策略。
SSO 短字符串優化(short-string-optimization)
string對象比前兩個都打,因為有本地緩沖區。
class string { char* start; size_t size; static const int KlocalSize = 15; union { char buf[klocalSize+1]; size_t capacity; }data; };
如果字符串比較短(通常設為15個字節以內),那么直接存放在對象的buf里。start指向data.buf。
如果字符串超過15個字節,那么就編程eager copy 2的結構,start指向堆上分配的空間。
短字符串優化的實現方式不止一種,主要區別是把那三個指針/整數中的哪一 個與本地緩沖重合。例如《Effective STL》[3] 第 15 條展現的“實現 D” 是將 buffer 與 start 指針重合,這正是 Visual C++ 的做法。而 STLPort 的 string 是將 buffer 與 end_of_storage 指針重合。
SSO string 在 64-bit 中有一個小小的優化空間:如果允許字符串 max_size() 不大 於 4G 的話,我們可以用 32-bit 整數來表示長度和容量,這樣同樣是 32 字節的 string 對象,local buffer 可以增大至 19 字節。
class sso_string // optimized for 64-bit { char* start; uint32_t size; static const int kLocalSize = sizeof(void*) == 8 ? 19 : 15; union { char buffer[kLocalSize+1]; uint32_t capacity; } data; };
llvm/clang/libc++ 采用了與眾不同的 SSO 實現,空間利用率最高,local buffer 幾乎與三個指針/整數完全重合,在 64-bit 上對象大小是 24 字節,本地緩沖區可達 22 字節。
它用一個 bit 來區分是長字符還是短字符,然后用位操作和掩碼 (mask) 來取重 疊部分的數據,因此實現是 SSO 里最復雜的。
實現機制
SSO策略中,拷貝均使用立即復制內存的方法,也就是深拷貝的基本定義,其優化在於,當字符串較短時,直接將其數據存在棧中,而不去堆中動態申請空間,這就避免了申請堆空間所需的開銷。
使用以下代碼來驗證一下:
int main() { string a = "aaaa"; string b = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; printf("%p ------- %p\n", &a, a.c_str()); printf("%p ------- %p\n", &b, b.c_str()); return 0; }
某次運行的輸出結果為:
1 |
0136F7D0 ------- 0136F7D4 |
可以看到,a.c_str()
的地址與a
、b
本身的地址較為接近,他們都位於函數的棧空間中,而b.c_str()
則離得較遠,其位於堆中。
SSO是目前大部分主流STL庫的實現方式,其優點就在於,對程序中經常用到的短字符串來說,運行效率較高。
-----------------------------------------------------------------------------------------------------------------------------------------------------
基於”共享“和”引用“計數的COW在多線程環境下必然面臨線程安全的問題。那么:
std::string是線程安全的嗎?
在stackoverflow上對這個問題的一個很好的回答:是又不是。
從在多線程環境下對共享的string對象進行並發操作的角度來看,std::string不是線程安全的,也不可能是線程安全的,像其他STL容器一樣。
c++11之前的標准對STL容器和string的線程安全屬性不做任何要求,甚至根本沒有線程相關的內容。即使是引入了多線程編程模型的C++11,也不可能要求STL容器的線程安全:線程安全意味着同步,同步意味着性能損失,貿然地保證線程安全必然違背了C++的哲學:
Don't pay for things you don't use. |
但從不同線程中操作”獨立“的string對象來看,std::string必須是線程安全的。咋一看這似乎不是要求,但COW的實現使兩個邏輯上獨立的string對象在物理上共享同一片內存,因此必須實現邏輯層面的隔離。C++0x草案(N2960)中就有這么一段:
The C++0x draft (N2960) contains the section "data race avoidance" which basically says that library |
簡單說來就是:你瞞着用戶使用共享內存是可以的(比如用COW實現string),但你必須負責處理可能的競態條件。
而COW實現中避免競態條件的關鍵在於:
1. 只對引用計數進行原子增減
2. 需要修改時,先分配和復制,后將引用計數-1(當引用計數為0時負責銷毀)
總結:
1、針對不同的應用負載選用不同的 string,對於短字符串,用 SSO string;對於中等長度的字符串,用 eager copy;對於長字符串,用 COW。具體分界點需要靠 profiling 來確定,選用合適的字符串可能提高 10% 的整 體性能。 從實現的復雜度上看,eager copy 是最簡單的,SSO 稍微復雜一些,COW 最 難。
2、了解COW的缺陷依然可以使我們優化對string的使用:盡量避免在多個線程間false sharing同一個“物理string“,盡量避免在對string進行只讀訪問(如打印)時造成了不必要的內部拷貝。
說明:vs2010、clang libc++、linux gnu5都已經拋棄了COW,擁抱了SSO,facebook更是開發了自己fbstring。
fbstring簡單說明:
> 很短的用SSO(0-22), 23字節表示字符串(包括’\0′), 1字節表示長度.
> 中等長度的(23-255)用eager copy, 8字節字符串指針, 8字節size, 8字節capacity.
> 很長的(>255)用COW. 8字節指針(指向的內存包括字符串和引用計數), 8字節size, 8字節capacity.
參考資料:
std::string的Copy-on-Write:不如想象中美好
C++ 工程實踐(10):再探std::string
Why is COW std::string optimization still enabled in GCC 5.1?
C++ string的COW和SSO