C++ Short String Optimization stackoverflow回答集锦以及我的思考

简单说明:
C++ Short String Optimization 指C++针对短字符串的优化。

  1. 默认情况下,C++的std::string都是存储在heap中,导致访问std::string需要经过一次寻址过程,速度较慢,并且这种实现的空间局部性不好,对cache的利用较低。
  2. 很多string的字符串长度很小,这个时候,我们可以把字符串存储到栈上,从而不需要进行内存分配,优化创建速度,并且访问栈上数据的局部性很好,速度比较快。

std::string需要存储如下变量:字符串所在地址,字符串大小size,字符串已经申请的内存capacity,加上短字符串
类似如下struct声明:

struct String {
    char* addr;
    size_t size;
    size_t capacity;
    char[16] short_string;
}

在现在的C++编译器中,因为当小于一定大小时,我们没有使用堆,所以不需要存储addr指针,并且此时的容量就是短字符串容量,也不需要存储,所以短字符串缓存可以和这两者复用,样例如下:

struct String {
    size_t size;
    union {
        struct {
            size_t capacity;
            char* addr;
        }
        char[16] short_string;
    }
}

这时,string没有使用额外的空间,并且能够针对端字符串进行优化,是一个很好的优化方式。

我自己的测试代码:

#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include <mutex>
#include <array>

using namespace std;

int main(int argc, char const* argv[])
{
  int i;
  vector<char> test_vector;
  mutex g_mutex;
  array<int, 10> test_array;
  deque<char> test_deque;
  string short4("test");
  string long19("test test test test");
  string long29("test test test test test test");

  cout << "stack address: " << &i << endl;
  cout << "vector sizeof: " << sizeof(test_vector) << " vector address: " << &test_vector << endl;
  cout << "g_mutex sizeof: " << sizeof(g_mutex) << " g_mutex address: " << &g_mutex << endl;
  cout << "test_deque sizeof: " << sizeof(test_deque) << " test_deque address: " << &test_deque << endl;
  cout << "test_array sizeof: " << sizeof(test_array) << " test_array address: " << &test_array << endl;
  cout << "sizeof short4: " << sizeof(short4) << " address: " << &short4 << " char address: " << (void*)(&(short4[0])) << endl;
  cout << "sizeof long19: " << sizeof(long19)  << " address: " << &long19 << " char address: " << (void*)(&(long19[0])) << endl;
  cout << "sizeof long29: " << sizeof(long29) << " address: " << &long29 << " char address: " << (void*)(&(long29[0])) << endl;
  return 0;
}

输出:

stack address: 0x7fff5d7eeca4
vector sizeof: 24 vector address: 0x7fff5d7eec88
g_mutex sizeof: 64 g_mutex address: 0x7fff5d7ef210
test_deque sizeof: 48 test_deque address: 0x7fff5d7eec30
test_array sizeof: 40 test_array address: 0x7fff5d7eec60
sizeof short4: 24 address: 0x7fff5d7eec08 char address: 0x7fff5d7eec09 —— 在栈上分配
sizeof long19: 24 address: 0x7fff5d7eebf0 char address: 0x7fff5d7eebf1 —— 栈上
sizeof long29: 24 address: 0x7fff5d7eebd8 char address: 0x7fdf27402710 —— 堆上

工具提示:
在线在 Visual C++里执行C++代码: http://webcompiler.cloudapp.net/
在线在gcc,clang里面执行C++代码: https://wandbox.org/

参考:

[什么是SSO?] (https://stackoverflow.com/questions/10315041/meaning-of-acronym-sso-in-the-context-of-stdstring/10319672#10319672)

回答翻译:

在automatic variables(『栈上』,指没有通过malloc/new常见的变量)上的操作通常要比在free store(『堆上』,通过malloc/new创建的变量)。但是,栈的大小是在编译期确定的,而堆的大小不是的。并且,栈有大小限制(一般是几M),而堆的大小只受限于你的系统内存大小。

SSO是 Short/Small String Optimization. 一个std::string通常是存储指向堆空间的指针,这和调用new char[size]的性能类似。这样做能够防止在非常大的string时不会出现栈溢出,不过它会比较慢,尤其是在复制操作时。作为一个优化,需要std::string的实现创建一个小的栈上的数组,例如:char[20]. 如果你的string长度小于等于20(这是一个例子,与实际大小不同), 它直接把数据保存到这个数组中。这去除了new调用,从而提高了一些速度。

EDIT:
我没有想到这个回答现在这么火,但是因为它确实很火,我在下面给出一个更加实际的实现,但是请注意我没有度过真实世界的SSO实现。

实现详情:
std::string最少需要下述信息:

  • 大小 size
  • 容量 capacity
  • 数据的位置 the location of data

大小可以被存储为std::string::size_type 或者指向末尾的指针。唯一的区别是是在用户调用size时两个指针相减,还是在用户调用end时对开始指针加一个数。capacity也可以以这两种方式存储。

你不需要为你不会使用的东西付款

首先,考虑我在上面提到的最原始的实现方式:

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,我知道了capacity,并且我知道我不需要指向数据的指针。如果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;
    };
};

我认为大多数的实现和这个比较类似。

[Meaning of acronym SSO in the context of std::string 各个编译器实现使用的内存使用情况对比,包含在stack heap中分配的内存,以及可用的capacity Why does libc++'s implementation of std::string take up 3x memory as libstdc++?] (https://stackoverflow.com/questions/27631065/why-does-libcs-implementation-of-stdstring-take-up-3x-memory-as-libstdc/28003328#28003328)

回答翻译:
这辆车有一个简短的程序来帮助你探索std::string使用的不同类型的内存,包括stack和heap

#include <string>
#include <new>
#include <cstdio>
#include <cstdlib>

std::size_t allocated = 0;

void* operator new (size_t sz)
{
    void* p = std::malloc(sz);
    allocated += sz;
    return p;
}

void operator delete(void* p) noexcept
{
    return std::free(p);
}

int
main()
{
    allocated = 0;
    std::string s("hi");
    std::printf("stack space = %zu, heap space = %zu, capacity = %zu\n",
     sizeof(s), allocated, s.capacity());
}

使用 http://melpon.org/wandbox/ 可以很容易的得到不同编译器、库组合的结果,例如:

gcc 4.9.1

stack space = 8, heap space = 27, capacity = 2

gcc 5.0.0

stack space = 32, heap space = 0, capacity = 15

clang/libc++

stack space = 24, heap space = 0, capacity = 22

VS-2015: from( http://webcompiler.cloudapp.net)

stack space = 32, heap space = 0, capacity = 15

上述输出同样带有capacity,这表示度量string能够在堆中新分配一个更大的缓存前能够容纳多少个chars。在gcc5.0.0, libc++和VS2015的实现中,这表示的是短字符串缓存的大小。即:在栈上分配,用来存储短字符串缓存的大小,这时可以避免更代价高昂的堆内存分配。

这里发现libc++中的短字符串实现使用了最小的stack usage,并且包含了最大的短字符串缓存。并且如果你统计总共内存使用(stack + heap),在这四种2-character的string实现中,libC++使用了最小的总内存。

需要注意的是,这些测试都是在64位机器上运行的。在32位系统中,libc++的stack大小降到12,并且短字符串缓存大小降到10。我不知道其他实现在32位的表现,不过你可以使用上述代码来找到相应结果。

[What are the mechanics of short string optimization in libc++? Libc++ 实现] (https://stackoverflow.com/questions/21694302/what-are-the-mechanics-of-short-string-optimization-in-libc)
回答翻译:
libc++的basic_string在所有架构上被设计为sizeof大小为3个words,在这里sizeof(word) == sizeof(void*)。你已经正确分析出long/short标志,以及在short string类型时的size区域。

what value would __min_cap, the capacity of short strings, take for different architectures?

那么__min_cap,short string的容量,在各个架构上怎么计算的?

在short string的情况下,下面是string实现的3个words所做的工作

  • 1一个bit用来表示short/long标志
  • 7个bits表示字符串当前大小
  • 在char的情况下,一个字节用来表示字符串末尾的null(libc++会在数据后面一致保存一个结束的null)

这种情况下,剩下3 * words - 2 bytes的数据来存储短字符串(也就是短字符串不进行内存分配时最大的capacity())
在32位模式下,短字符串可以放置10个chars,sizeof(string) = 12 (3 * 4).
在64位模式下,短字符串可以放置22个chars,sizeof(string) = 24 (3 * 8).

这里的一个主要设计目标是:在最小化sizeof(string)的同时,让内部的buffer尽可能大。其中的根本原因是加速move语义构造和move语义赋值。当sizeof越大时,就需要在move语义构造和move语义赋值时移动更多的字节。

在长字符串的情况,至少需要三个words来分别存储 数据指针(pointer)、大小(size)、容量(capacity),因此我(作者)把短字符串格式限制到这三个words中。曾经有人提出4个words大小的sizeof可能会有更好的性能。但是我(作者)没有测试过这种设计选择。

[更精致的实现 SSO23,还分析了相关各个实现的细节] (https://github.com/elliotgoodrich/SSO-23)

写作方式参考,从string可能会占据多少字符串开始讲起

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容

  • 【转载】原文地址:std::string详解作者:kieven2008 之所以抛弃char*的字符串而选用C++标...
    VAYY阅读 641评论 0 2
  • c++的字符串类std::string能否存储二进制字符以及字符'\0'? 要解决这个问题,我们首先要了解c++的...
    CodingCode阅读 7,169评论 0 5
  • 一、字符串操作 strcpy(p, p1) 复制字符串 strncpy(p, p1, n) 复制指定长度字符串 s...
    JaiUnChat阅读 1,655评论 0 7
  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy阅读 9,513评论 1 51
  • 花瓣晒了一下午太阳, 落在地上, 捡起它们, 去听很多很多的音乐, 记得, 某时某刻, 某地 有一个姑娘 采集回来...
    关馨仁阅读 138评论 0 2