暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

C++性能优化实践(四)

purecpp 2021-09-02
2602

在谈优化运行期字符串之前,有必要先来看看std::string的一些特点,这些特点有助于我们优化性能。事实上std::string并没有想象中那么慢,我们来看一个测试:

const int SIZE = 1000000;
void test_stack() {
ScopedTimer timer("stack");
for (int i = 0; i < SIZE; i++) {
char buf[12];
char buf1[12];
}
}

void test_string() {
ScopedTimer timer("std string");
for (int i = 0; i < SIZE; i++) {
std::string str("hello ");
std::string str1("world");
}
}

在gcc9.2 -O3编译运行后的结果:

stack : 28 ns
std string : 25 ns
//https://godbolt.org/z/afq3edxdx

可以看到字符串的构造和栈内存的分配速度相当,可以说是相当快了。再看另外一个测试:

const int SIZE = 1000000;
void test_stack() {
ScopedTimer timer("stack");
for (int i = 0; i < SIZE; i++) {
char buf[12];
char buf1[12];
}
}

void test_string() {
ScopedTimer timer("std string");
for (int i = 0; i < SIZE; i++) {
std::string str("hello world, it is test string.");
}
}

在gcc9.2 -O3编译运行后的结果:

stack : 33 ns
std string : 14507394 ns

测试结果和之前相比,string的构造和栈内存分配性能相差了好几个数量级,测试结果有点让人吃惊。测试代码几乎没有任何变化,只是string的字符串长度变长了一些,为什么字符串长度变长性能下降这么多呢?这和std::string实现的特点有关。

std::string的特点

以libc++ string为例看看它的一些实现细节,string内部有一个union来存放字符:

  短字符串会放到栈上,长字符串会在堆上分配内存。由于栈上的内存大小为23,最后一个字符存放\0终止符,所以可以保存最长为22字节的短字符串,超过22字节的字符串会在对上分配内存,这就是之前测试例子中为什么几个字节的字符串和栈内存的效率是差不多的,而长字符串的效率却相差了这么多倍的原因。究其本质,是因为栈内存比堆内存的分配效率高得多。所以std::string做了一个小优化,将短字符串放到栈上,从而大幅提升性能。

这里也给了我们一个启示,如果我让这个字符串都在栈上分配内存不是可以获得更好的性能吗?

运行期字符串优化思路

让字符串都使用栈上的内存而不是堆内存,这就是运行期字符串优化的基本思路。C++有没有提供这样的机制去做这个优化呢?在C++17中新增加了一个新特性std::pmr,在头文件memory_resource中(https://en.cppreference.com/w/cpp/header/memory_resource)

std::pmr::string允许我们在栈上创建string,所以可以借助std::pmr去做性能优化。来看一个测试代码:

  char buf[1024];
std::pmr::monotonic_buffer_resource resource{ &buf, 1024 };
std::pmr::string s{ &resource };

s += "hello";
s += " world";

我们先用一块栈内存来初始化std::pmr::monotonic_buffer_resource,接着用它来构造一个std::pmr::string,后续字符串操作都是在栈上了。如果字符串长度超过了初始的栈内存1024字节之后会怎么样,不用担心,当栈内存用完之后std::pmr::string会在堆上分配内存。

接下来看看通过std::pmr优化之后的string性能怎么样。

性能对比

将std::string和std::pmr::string做一个性能对比:

const int SIZE = 1000000;

void test_pmr_string() {
ScopedTimer timer("pmr string");
for (int i = 0; i < SIZE; i++) {
char buf[1024];
std::pmr::monotonic_buffer_resource resource{ &buf, 1024 };
std::pmr::string s{ &resource };

s.append("it is a test");
s.append("it is a long string test;it is a long string test;it is a long string test;");
}
}

void test_std_string() {
ScopedTimer timer("std string");
for (int i = 0; i < SIZE; i++) {
std::string s;
s.reserve(1024);
s.append("it is a test");
s.append("it is a long string test;it is a long string test;it is a long string test;");
}
}

int main() {
std::cout << "==========small string testing\n";

for (int i = 0; i < 20; ++i) {
test_pmr_string();
test_std_string();
}
}

gcc 9.2 -O3 -std=c++17

pmr string : 21656894 ns
std string : 56772931 ns

可见std::pmr::string的性能比std::string提升了两倍,优化效果还是挺明显的。

虽然std::pmr::string对性能提升比较多,但也存在一些问题,还有没有其它办法继续提升运行期字符串的性能呢?

且听下回分解,点赞越多更新越快!


文章转载自purecpp,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论