在牛客网上看到一题字符串拷贝相关的题目,深入挖掘了下才发现原来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=
s3=helloworld
s4=helloworld
s1.refcount=
s3.address=0x16b9054
s4.address=0x16b9054 s5 = Xelloworldjeiqjeiqoej
>>delete heap data1 s5=helloworld
s3=helloworld
s4=helloworld
s3.refcount =
s5.address=0x16b9054
s3.address=0x16b9054
s4.address=0x16b9054 执行读操作
operator char()
s3[] = h
s5 = helloworld
s3 = helloworld
s4 = helloworld
s5.refcount =
s3.refcount =
s3.address=0x16b9054
s4.address=0x16b9054 const char operator[](size_t)const
h
s6'address=0x7ffffdcdce40
>>delete heap data1
>>delete heap data1
>>delete heap data1
cthon@zrw:~/c++/$ vim cowstring1.cc
cthon@zrw:~/c++/$ g++ cowstring1.cc
cthon@zrw:~/c++/$ ./a.out
s1=
s2=
s1.refcount=
s3=helloworld
s4=helloworld
s1.refcount=
s3.address=0xb18054
s4.address=0xb18054 s5 = Xelloworldjeiqjeiqoej
>>delete heap data1 s5=helloworld
s3=helloworld
s4=helloworld
s3.refcount =
s5.address=0xb18054
s3.address=0xb18054
s4.address=0xb18054//这里s3/s4/s5指向同一个内存空间,发现没,这就是cow的妙用 执行读操作
operator char()
s3[] = h
s5 = helloworld
s3 = helloworld
s4 = helloworld
s5.refcount =
s3.refcount =
s3.address=0xb18054
s4.address=0xb18054 const char 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
时,释放原内存。
不过,实际的string
COW实现中,对于什么是”写操作”的认定和我们的直觉是不同的,考虑以下代码:
string a = "Hello";
string b = a;
cout << b[] << 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.