天天看點

c++再探string之eager-copy、COW和SSO方案

在牛客網上看到一題字元串拷貝相關的題目,深入挖掘了下才發現原來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之後都采用的這種方式。

c++再探string之eager-copy、COW和SSO方案

  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;
}        
c++再探string之eager-copy、COW和SSO方案

  對象的大小是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;
}        
c++再探string之eager-copy、COW和SSO方案

  這種做法并沒有多大改變,因為size_t和char*是一樣大的。但是我們通常用不到單個幾百兆位元組的字元串,那麼可以在改變以下長度和容量的類型(從64bit整數改成32bit整數)。

class string
{
private:
     char* start;
     size_t size_;
     size_t capacity;
}        
c++再探string之eager-copy、COW和SSO方案

  新的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;     
}  ;
      
c++再探string之eager-copy、COW和SSO方案

  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;
};      
c++再探string之eager-copy、COW和SSO方案

  如果字元串比較短(通常設為15個位元組以内),那麼直接存放在對象的buf裡。start指向data.buf。

c++再探string之eager-copy、COW和SSO方案

  如果字元串超過15個位元組,那麼就程式設計eager copy 2的結構,start指向堆上配置設定的空間。

c++再探string之eager-copy、COW和SSO方案

  短字元串優化的實作方式不止一種,主要差別是把那三個指針/整數中的哪一 個與本地緩沖重合。例如《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;
};
    
      
c++再探string之eager-copy、COW和SSO方案

  llvm/clang/libc++ 采用了與衆不同的 SSO 實作,空間使用率最高,local buffer 幾乎與三個指針/整數完全重合,在 64-bit 上對象大小是 24 位元組,本地緩沖區可達 22 位元組。

c++再探string之eager-copy、COW和SSO方案

  它用一個 bit 來區分是長字元還是短字元,然後用位操作和掩碼 (mask) 來取重 疊部分的資料,是以實作是 SSO 裡最複雜的。

c++再探string之eager-copy、COW和SSO方案
c++再探string之eager-copy、COW和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
2
      
0136F7D0 ------- 0136F7D4
0136F7AC ------- 016F67F0
      

可以看到,

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 
components may access shared data that is hidden from the user if and only if it activly avoids 
possible data races.       

簡單說來就是:你瞞着使用者使用共享記憶體是可以的(比如用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

短字元串優化的libc++機制