在牛客网上看到一题字符串拷贝相关的题目,深刻挖掘了下才发现原来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
对std::string的理性认识安全
Copy-On-Write必定使用了“引用计数”,必然有一个变量相似于RefCnt数据结构
当第一个string对象str1构造时,string的构造函数会根据传入的参数从堆上分配内存
当有其它string对象复制str1时,这个RefCnt会自动加1
当有对象析构时,这个计数会减1;直到最后一个对象析构时,RefCnt为0,此时,程序才会真正的释放这块从堆上分配的内存
Q1.1 RefCnt该存在在哪里呢?
若是存放在string类中,那么每一个string的实例都各自拥有本身的RefCnt,根本不能共有一个 RefCnt
若是是声明成全局变量,或是静态成员,那就是全部的string类共享一个了,这也不行
根据常理和逻辑,发生复制的时候
1)以一个对象构造本身(复制构造函数) 只须要在string类的拷贝构造函数中作点处理,让其引用计数累加
2)以一个对象赋值(重载赋值运算符)
在共享同一块内存的类发生内容改变时,才会发生Copy-On-Write
好比string类的 []、=、+=、+、操做符赋值,还有一些string类中诸如insert、replace、append等成员函数
if ( --RefCnt>0 ) { char* tmp = (char*) malloc(strlen(_Ptr)+1); strcpy(tmp, _Ptr); _Ptr = tmp; }
string h1 = “hello”; string h2= h1; string h3; h3 = h2; string w1 = “world”; string w2(“”); w2=w1;
copy-on-write的具体实现分析
解决方案分析
当为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就是指,复制的时候不当即申请新的空间,而是把这一过程延迟到写操做的时候,由于在这以前,两者的数据是彻底相同的,无需复制。这实际上是一种普遍采用的通用优化策略,它的核心思想是懒惰处理多个实体的资源请求,在多个实体之间共享某些资源,直到有实体须要对资源进行修改时,才真正为该实体分配私有的资源。
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[0] << endl;
以上代码显然没有修改string b
的内容,此时彷佛a
和b
是能够共享一块内存的,然而因为string
的operator[]
和at()
会返回某个字符的引用,此时没法准确的判断程序是否修改了string
的内容,为了保证COW实现的正确性,string
只得通通认定operator[]
和at()
具备修改的“语义”。
这就致使string
的COW实现存在诸多弊端(除了上述缘由外,还有线程安全的问题,可进一步阅读文末参考资料),所以只有老版本的GCC编译器和少数一些其余编译器使用了此方式,VS、Clang++、GCC 5.x等编译器均放弃了COW策略,转为使用SSO策略。
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在多线程环境下必然面临线程安全的问题。那么:
在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时负责销毁)
总结:
一、针对不一样的应用负载选用不一样的 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