在a C++ question about optimization and code style中,在优化std::string
副本的背景下,有几个答案提到了“SSO”。在这种情况下,SSO意味着什么?
显然不是“单点登录”。 “共享字符串优化”,或许?
答案 0 :(得分:175)
对自动变量的操作(“来自堆栈”,即您创建的变量而不调用malloc
/ new
)通常比涉及免费存储(“堆”,这是使用new
)创建的变量。但是,自动数组的大小在编译时是固定的,但是来自免费存储的数组的大小不是。此外,堆栈大小有限(通常为几MiB),而免费存储仅受系统内存的限制。
SSO是短/小字符串优化。 std::string
通常将字符串存储为指向免费存储(“堆”)的指针,它提供了与调用new char [size]
时类似的性能特征。这可以防止非常大的字符串堆栈溢出,但它可能会更慢,尤其是对于复制操作。作为优化,std::string
的许多实现创建了一个小型自动数组,类似于char [20]
。如果您有一个20个字符或更小的字符串(给定此示例,实际大小不同),它会将其直接存储在该数组中。这样就完全无需调用new
,这会加快速度。
编辑:
我没想到这个答案会如此受欢迎,但是既然如此,让我给出一个更现实的实现,但需要注意的是,我从来没有真正阅读任何“疯狂”的SSO实现。 / p>
至少,std::string
需要存储以下信息:
大小可以存储为std::string::size_type
或指向结尾的指针。唯一的区别是当用户调用size
时是否需要减去两个指针,或者在用户调用size_type
时向指针添加end
。容量也可以以任何方式存储。
首先,根据我上面概述的内容考虑天真的实现:
class string {
public:
// all 83 member functions
private:
std::unique_ptr<char[]> m_data;
size_type m_size;
size_type m_capacity;
std::array<char, 16> m_sso;
};
对于64位系统,这通常意味着std::string
每个字符串有24个字节的“开销”,另外还有16个用于SSO缓冲区(由于填充要求,此处选择16个而不是20个)。如我的简化示例所示,存储这三个数据成员加上本地字符数组是没有意义的。如果m_size <= 16
,那么我将把所有数据放在m_sso
中,所以我已经知道了容量,我不需要指向数据的指针。如果m_size > 16
,那么我不需要m_sso
。在我需要所有这些的地方绝对没有重叠。一个不浪费空间的更智能的解决方案看起来更像这样(未经测试,仅用于示例目的):
class string {
public:
// all 83 member functions
private:
size_type m_size;
union {
class {
// This is probably better designed as an array-like class
std::unique_ptr<char[]> m_data;
size_type m_capacity;
} m_large;
std::array<char, sizeof(m_large)> m_small;
};
};
我认为大多数实现看起来都更像这样。
答案 1 :(得分:30)
SSO是“小字符串优化”的缩写,这是一种将小字符串嵌入到字符串类主体中而不是使用单独分配的缓冲区的技术。
答案 2 :(得分:7)
正如其他答案所解释的那样,SSO表示小型/短字符串优化。 这种优化背后的动机是无可否认的证据,即应用程序通常处理的短字符串要比长的字符串多。
正如David Stone in his answer above所解释的,std::string
类使用内部缓冲区来存储指定长度的内容,这消除了动态分配内存的需要。这使代码更有效和更快。
This other related answer清楚地表明,内部缓冲区的大小取决于std::string
的实现,具体情况因平台而异(请参见下面的基准测试结果)。
这是一个小程序,它对许多相同长度的字符串的复制操作进行基准测试。 它开始打印复制1000万个长度= 1的字符串的时间。 然后以长度= 2的字符串重复。直到长度为50。
#include <string>
#include <iostream>
#include <vector>
#include <chrono>
static const char CHARS[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
static const int ARRAY_SIZE = sizeof(CHARS) - 1;
static const int BENCHMARK_SIZE = 10000000;
static const int MAX_STRING_LENGTH = 50;
using time_point = std::chrono::high_resolution_clock::time_point;
void benchmark(std::vector<std::string>& list) {
std::chrono::high_resolution_clock::time_point t1 = std::chrono::high_resolution_clock::now();
// force a copy of each string in the loop iteration
for (const auto s : list) {
std::cout << s;
}
std::chrono::high_resolution_clock::time_point t2 = std::chrono::high_resolution_clock::now();
const auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count();
std::cerr << list[0].length() << ',' << duration << '\n';
}
void addRandomString(std::vector<std::string>& list, const int length) {
std::string s(length, 0);
for (int i = 0; i < length; ++i) {
s[i] = CHARS[rand() % ARRAY_SIZE];
}
list.push_back(s);
}
int main() {
std::cerr << "length,time\n";
for (int length = 1; length <= MAX_STRING_LENGTH; length++) {
std::vector<std::string> list;
for (int i = 0; i < BENCHMARK_SIZE; i++) {
addRandomString(list, length);
}
benchmark(list);
}
return 0;
}
如果要运行此程序,则应像./a.out > /dev/null
那样进行操作,以免计算字符串的打印时间。
重要的数字将打印到stderr
,因此它们将显示在控制台中。
我已经使用MacBook和Ubuntu计算机的输出创建了图表。 请注意,当长度达到给定点时,复制字符串的时间将大大增加。 那是字符串不再适合内部缓冲区且必须使用内存分配的时候了。
还要注意,在linux机器上,当字符串的长度达到16时发生跳转。 在Macbook上,当长度达到23时会发生跳转。这确认SSO取决于平台的实现。