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之后都采用的這種方式。

  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的內容,此時似乎ab是可以共享一塊內存的,然而由於stringoperator[]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
2
0136F7D0 ------- 0136F7D4
0136F7AC ------- 016F67F0

可以看到,a.c_str()的地址與ab本身的地址較為接近,他們都位於函數的棧空間中,而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++機制


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM