前言:
std::string在历史上的底层常见实现有一下几种方式:
- 深拷贝(Eager Copy)
- 写时复制COW(Copy-On-Write)
- 短字符串优化SSO(Small String Optimization)
一、深拷贝
深拷贝(Eager Copy)是一种在计算机编程中用来创建一个对象的副本的机制,它不仅复制对象本身,还递归地复制对象所包含的所有子对象。这意味着原始对象和副本对象之间没有共享任何数据,对副本对象的任何修改都不会影响到原始对象。
比如以下MyClass类的拷贝构造函数和=运算符重载中进行深拷贝,直接在堆空间上申请一个一块空间来存放与拷贝对象数据成员值相同的数据,他的任何修改都不会影响到原始对象。
class MyClass {
public:
MyClass(const MyClass& other) {
data = new int(*other.data);
}
MyClass& operator=(const MyClass& other) {
if (this != &other) {
delete data;
data = new int(*other.data);
}
return *this;
}
private:
int* data;
};
浅拷贝示例对比:
只拷贝指针指向,之后两个指针指向同一片区域,相互影响。
class MyClass {
public:
MyClass(const MyClass& other) {
data = other.data;
}
MyClass& operator=(const MyClass& other) {
if (this != &other) {
data = other.data;
}
return *this;
}
private:
int* data;
};
深拷贝的缺点:
-
性能开销:深拷贝需要为副本分配新的内存,并将原始对象的数据逐个复制到新内存中。这个过程可能会非常耗时,特别是当对象包含大量数据或复杂数据结构时。
-
内存使用:深拷贝会分配额外的内存来存储副本的数据,这可能导致内存使用量的增加,特别是在处理大量数据或大量对象副本时。
二、写时复制COW
写时复制(Copy-on-Write,简称 COW)是一种优化策略,其核心思想是多个调用者共同获取相同的资源指针,直到某个调用者试图修改资源内容时,系统才会真正复制一份专用副本给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这种策略的优点在于可以减少不必要的数据复制,提高读取效率,尤其适用于读多写少的场景。
std::string的写时复制探究:
根据以上写时复制的原理,我们可以应用到std::string中,使用refcount进行引用计数,如果字符串对象进行复制操作,refcount+=1;,若该字符串对象销毁则,refcount-=1;,当refcount等于0时才真正释放此处空间。
refcount存放的位置:
- 数据成员(复制时二者对象的数据成员难以同步)
- 类静态成员(所有对象共有,但refcount需要多个)
- 使用堆空间(使用对象管理数组前四个字节保存refcount)
三、短字符串优化SSO
std::string
对象通常会包含一个内部缓冲区,这个缓冲区设计来存储短字符串。在 64 位 Linux 系统上,这个内部缓冲区通常足够大,可以容纳一定数量的字符,例如 15 个字符加上空终止符 。
- 小于等于15个字符-->存放到缓冲区buffer
- 大于15个字符-->缓冲区buffer存放指针指向对于堆空间
四、最佳策略
Facebook 提出的解决 std::string
存储的最佳策略是在其开源库 Folly 中实现的 fbstring
类。fbstring
是 std::string
的一个高性能替代品,它通过使用三种不同的存储策略来优化不同长度的字符串:
-
SSO(Short String Optimization):对于短字符串(长度小于或等于 23 个字符),
fbstring
直接使用栈内存进行存储,避免了动态内存分配的开销。这种优化使得短字符串的存储和操作非常高效,因为它们可以直接在栈上处理,而不需要进行堆内存分配。 -
Eager Copy:对于中等长度的字符串(大于 23 个字符且小于或等于 255 个字符),
fbstring
在堆上分配内存,并且总是进行拷贝。这种方式类似于std::string
的行为,但是经过优化以提高性能。 -
COW(Copy On Write):对于长字符串(大于 255 个字符),
fbstring
使用引用计数和 COW 优化来避免不必要的拷贝操作。这种策略在多线程环境中特别有用,因为它可以减少内存占用和复制操作。