在牛客網上看到一題字符串拷貝相關的題目,深刻挖掘了下才發現原來C++中string的實現仍是有好幾種優化方法的。html
原始題目是這樣的:linux
關於代碼輸出正確的結果是()(Linux g++ 環境下編譯運行)ios
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
的實現方法不一樣致使的。c++
簡而言之,目前各類STL實現中,對string
的實現有兩種不一樣的優化策略,即COW(Copy On Write)和SSO(Small String Optimization)。string
也是一個類,類的拷貝操做有兩種策略——深拷貝及淺拷貝。咱們本身寫的類默認狀況下都是淺拷貝的,能夠理解爲指針的複製,要實現深拷貝須要重載賦值操做符或拷貝構造函數。不過對於string
來講,大部分狀況下咱們用賦值操做是想實現深拷貝的,故全部實現中string
的拷貝均爲深拷貝。編程
最簡單的深拷貝就是直接new一個對象,而後把數據複製一遍,不過這樣作效率很低,STL中對此進行了優化,基本策略就是上面提到的COW和SSO。vim
咱們先以COW爲例分析一下std::string設計模式
對std::string的感性認識 promise
對std::string的理性認識安全
Copy-On-Write必定使用了「引用計數」,必然有一個變量相似於RefCnt數據結構
當第一個string對象str1構造時,string的構造函數會根據傳入的參數從堆上分配內存
當有其它string對象複製str1時,這個RefCnt會自動加1
當有對象析構時,這個計數會減1;直到最後一個對象析構時,RefCnt爲0,此時,程序纔會真正的釋放這塊從堆上分配的內存
Q1.1 RefCnt該存在在哪裏呢?
若是存放在string類中,那麼每一個string的實例都各自擁有本身的RefCnt,根本不能共有一個 RefCnt
若是是聲明成全局變量,或是靜態成員,那就是全部的string類共享一個了,這也不行
根據常理和邏輯,發生複製的時候
1)以一個對象構造本身(複製構造函數) 只須要在string類的拷貝構造函數中作點處理,讓其引用計數累加
2)以一個對象賦值(重載賦值運算符)
在共享同一塊內存的類發生內容改變時,纔會發生Copy-On-Write
好比string類的 []、=、+=、+、操做符賦值,還有一些string類中諸如insert、replace、append等成員函數
if ( --RefCnt>0 ) { char* tmp = (char*) malloc(strlen(_Ptr)+1); strcpy(tmp, _Ptr); _Ptr = tmp; }
string h1 = 「hello」; string h2= h1; string h3; h3 = h2; string w1 = 「world」; string w2(「」); w2=w1;
copy-on-write的具體實現分析
解決方案分析
當爲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; }
事實上,上面的代碼仍是由缺陷的,[ ]運算符不能區分讀操做或者寫操做。
爲了解決這個問題,可使用代理類來實現:
一、 重載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; }
二、代理模式:藉助自定義嵌套類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有不少實現方式,概括起來有三類,而每類又有多種變化。
一、無特殊處理(eager copy),採用相似std::vector的數據結構。如今不多采用這種方式。
二、Copy-on-write(COW),g++的std::string一直採用這種方式,不過慢慢被SSO取代。
三、短字符串優化(SSO),利用string對象自己的空間來存儲短字符串。VisualC++20十、clang libc、linux gnu5.x以後都採用的這種方式。
VC++的std::string的大小跟編譯模式有關,表中的小的數字時release編譯,大的數字是debug編譯。所以debug和release不能混用。除此之外,其餘庫的string大小是固定的。
這幾種實現方式都要保存三種數據:一、字符串自己(char*),二、字符串長度(size),三、字符串容量(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就是指,複製的時候不當即申請新的空間,而是把這一過程延遲到寫操做的時候,由於在這以前,兩者的數據是徹底相同的,無需複製。這實際上是一種普遍採用的通用優化策略,它的核心思想是懶惰處理多個實體的資源請求,在多個實體之間共享某些資源,直到有實體須要對資源進行修改時,才真正爲該實體分配私有的資源。
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策略。
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在多線程環境下必然面臨線程安全的問題。那麼:
在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時負責銷燬)
總結:
一、針對不一樣的應用負載選用不一樣的 string,對於短字符串,用 SSO string;對於中等長度的字符串,用 eager copy;對於長字符串,用 COW。具體分界點須要靠 profiling 來肯定,選用合適的字符串可能提升 10% 的整 體性能。 從實現的複雜度上看,eager copy 是最簡單的,SSO 稍微複雜一些,COW 最 難。
二、瞭解COW的缺陷依然可使咱們優化對string的使用:儘可能避免在多個線程間false sharing同一個「物理string「,儘可能避免在對string進行只讀訪問(如打印)時形成了沒必要要的內部拷貝。
說明:vs20十、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