在牛客网上看到一题字符串拷贝相关的题目,深入挖掘了下才发现原来C++中string的实现还是有好几种优化方法的,这里简单记录一下。
原始题目是这样的:
关于代码输出正确的结果是()(Linux g++ 环境下编译运行)
1 | int main(int argc, char *argv[]) |
这段程序的输出结果和编译器有关,在老版本(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
所谓COW就是指,复制的时候不立即申请新的空间,而是把这一过程延迟到写操作的时候,因为在这之前,二者的数据是完全相同的,无需复制。这其实是一种广泛采用的通用优化策略,它的核心思想是懒惰处理多个实体的资源请求,在多个实体之间共享某些资源,直到有实体需要对资源进行修改时,才真正为该实体分配私有的资源。
COW的实现依赖于引用计数(reference count, rc
),初始时rc=1
,每次赋值复制时rc++
,当修改时,如果rc>1
,需要申请新的空间并复制一份原来的数据,并且rc--
,当rc==0
时,释放原内存。
不过,实际的string
COW实现中,对于什么是”写操作”的认定和我们的直觉是不同的,考虑以下代码:
1 | string a = "Hello"; |
以上代码显然没有修改string b
的内容,此时似乎a
和b
是可以共享一块内存的,然而由于string
的operator[]
和at()
会返回某个字符的引用,此时无法准确的判断程序是否修改了string
的内容,为了保证COW实现的正确性,string
只得统统认定operator[]
和at()
具有修改的“语义”。
这就导致string
的COW实现存在诸多弊端(除了上述原因外,还有线程安全的问题,可进一步阅读文末参考资料),因此只有老版本的GCC编译器和少数一些其他编译器使用了此方式,VS、Clang++、GCC 5.x等编译器均放弃了COW策略,转为使用SSO策略。
Update 2022-12-03:
string
的 COW 实现带来的线程安全问题分析:
SSO
SSO策略中,拷贝均使用立即复制内存的方法,也就是深拷贝的基本定义,其优化在于,当字符串较短时,直接将其数据存在栈中,而不去堆中动态申请空间,这就避免了申请堆空间所需的开销。
使用以下代码来验证一下:
1 | int main() { |
某次运行的输出结果为:
1 | 0136F7D0 ------- 0136F7D4 |
可以看到,a.c_str()
的地址与a
、b
本身的地址较为接近,他们都位于函数的栈空间中,而b.c_str()
则离得较远,其位于堆中。
SSO是目前大部分主流STL库的实现方式,其优点就在于,对程序中经常用到的短字符串来说,运行效率较高。
参考资料:
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