178

关于优化和代码风格的 C++ 问题中,有几个答案在优化std::string. 在这种情况下,SSO 是什么意思?

显然不是“单点登录”。也许是“共享字符串优化”?

4

3 回答 3

252

背景/概述

对自动变量的操作(“来自堆栈”,这是您在不调用malloc/的情况下创建的变量new)通常比那些涉及自由存储(“堆”,这是使用创建的变量)快得多new。但是,自动数组的大小在编译时是固定的,但来自自由存储的数组的大小不是。此外,堆栈大小是有限的(通常为几个 MiB),而免费存储仅受系统内存的限制。

SSO 是短/小字符串优化。Astd::string通常将字符串存储为指向空闲存储(“堆”)的指针,这提供了与调用new char [size]. 这可以防止非常大的字符串的堆栈溢出,但它可能会更慢,尤其是复制操作。作为一种优化,许多实现std::string创建一个小的自动数组,比如char [20]. 如果您有一个 20 个字符或更小的字符串(在此示例中,实际大小会有所不同),它将直接将其存储在该数组中。这完全避免了调用的需要new,从而加快了速度。

编辑:

我没想到这个答案会如此受欢迎,但既然如此,让我给出一个更现实的实现,但需要注意的是,我从未真正“在野外”阅读过任何 SSO 的实现。

实施细节

a 至少std::string需要存储以下信息:

  • 规模
  • 容量
  • 数据的位置

大小可以存储为 astd::string::size_type或指向末尾的指针。唯一的区别是您是否希望在用户调用时必须减去两个指针,或者在用户调用时向指针size添加 a 。容量也可以以任何一种方式存储。size_typeend

你不用为你不使用的东西付费。

首先,根据我上面概述的内容考虑简单的实现:

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;
    };
};

我假设大多数实现看起来更像这样。

于 2012-04-25T16:18:06.147 回答
33

SSO 是“Small String Optimization”的缩写,一种将小字符串嵌入到字符串类的主体中而不是使用单独分配的缓冲区的技术。

于 2012-04-25T16:15:57.390 回答
28

正如其他答案已经解释的那样,SSO 表示Small / Short String Optimization。这种优化背后的动机是不可否认的证据,即应用程序通常处理的短字符串比长字符串多得多。

正如 David Stone在上面的回答中所解释的那样,std::string该类使用内部缓冲区来存储最多给定长度的内容,这消除了动态分配内存的需要。这使得代码更加高效快速

这个其他相关答案清楚地表明内部缓冲区的大小取决于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 取决于平台实现。

Ubuntu Ubuntu 上的 SSO 基准测试

MacBook Pro Macbook Pro 上的 SSO 基准测试

于 2018-08-11T04:44:17.333 回答