在牛客網上看到一題字符串拷貝相關的題目,深入挖掘了下才發現原來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時,釋放原內存。
不過,實際的stringCOW實現中,對於什么是”寫操作”的認定和我們的直覺是不同的,考慮以下代碼:
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
