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

在牛客網上看到一題字符串拷貝相關的題目,深刻挖掘了下才發現原來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

  • 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的具體實現是怎麼樣的?
    • h一、h二、h3共享同一塊內存, w一、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;
}

 

事實上,上面的代碼仍是由缺陷的,[ ]運算符不能區分讀操做或者寫操做。 

爲了解決這個問題,可使用代理類來實現:

一、 重載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寫時複製(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時負責銷燬)

 

總結:

一、針對不一樣的應用負載選用不一樣的 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

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

相關文章
相關標籤/搜索