c++ string remove_C++17,使用 string_view 来避免复制

当字符串数据的所有权已经确定(譬如由某个 string 对象持有),并且你只想访问(而不修改)他们时,使用 std::string_view 可以避免字符串数据的复制,从而提高程序效率,这(指程序效率)也是这篇文章的主要内容。

这次要介绍的 string_view 是 C++17 的一个主要特性。

9fb5e239ebea3f133998f808dedda40e.png

我假设你已经了解了一些 std::string_view 的知识,C++ 中的 string 类型在堆上存放自己的字符串数据,所以当你处理 string 类型的时候,很容易就会产生(堆)内存分配。

Small string optimisation

我们先看下以下的示例代码:

 1#include 
2#include 
3
4void* operator new(std::size_t count)  5{
6    std::cout <"   " <" bytes" <std::endl;
7    return malloc(count);
8}
9
10void getString(const std::string& str) {}
11
12int main()13{
14    std::cout <std::endl;
15
16    std::cout <"std::string" <std::endl;
17
18    std::string small = "0123456789";
19    std::string substr = small.substr(5);
20    std::cout <"   " <std::endl;
21
22    std::cout <std::endl;
23
24    std::cout <"getString" <std::endl;
25
26    getString(small);
27    getString("0123456789");
28    const char message[] = "0123456789";
29    getString(message);
30
31    std::cout <std::endl;
32
33    return 0;
34}

代码第 4 到第 8 行,我重载了全局的 new 操作符,这样我就能跟踪(堆)内存的分配了,而后,代码分别在第 18 行,第 19 行,第 27 行,第 29 行创建了 string 对象,所以这几处代码都会产生(堆)内存分配.相关的程序输出如下:

23f0eea6edd1b345e02cef82306da034.png

咦, 程序竟然没有产生内存分配?这是怎么回事?其实 string 类型只有在字符串超过指定大小(具体实现相关)时才会申请(堆)内存,对于 MSVC 来说,指定大小为 15,对于 GCC 和 Clang,这个值则为 23。

这也就意味着,较短的字符串数据是直接存储于 string 的对象内存中的,不需要分配(堆)内存。

从现在开始,示例代码中的字符串将拥有至少 30 个字符,这样我们就不需要关注短字符串优化了。好了,带着这个前提(字符串长度>= 30 个字符),让我们重新开始讲解。

No memory allocation required

现在, std::string_view 无需复制字符串数据的优点就更加明显了( std::string 不进行短字符串优化的情况下),下面的代码就是例证。

 1#include 
2#include 
3#include 
4#include 
5
6void* operator new(std::size_t count)  7{
8    std::cout <"   " <" bytes" <std::endl;
9    return malloc(count);
10}
11
12void getString(const std::string& str) {}
13
14void getStringView(std::string_view strView) {}
15
16int main()17{
18    std::cout <std::endl;
19
20    std::cout <"std::string" <std::endl;
21
22    std::string large = "0123456789-123456789-123456789-123456789";
23    std::string substr = large.substr(10);
24
25    std::cout <std::endl;
26
27    std::cout <"std::string_view" <std::endl;
28
29    std::string_view largeStringView{ large.c_str(), large.size() };
30    largeStringView.remove_prefix(10);
31
32    assert(substr == largeStringView);
33
34    std::cout <std::endl;
35
36    std::cout <"getString" <std::endl;
37
38    getString(large);
39    getString("0123456789-123456789-123456789-123456789");
40    const char message[] = "0123456789-123456789-123456789-123456789";
41    getString(message);
42
43    std::cout <std::endl;
44
45    std::cout <"getStringView" <std::endl;
46
47    getStringView(large);
48    getStringView("0123456789-123456789-123456789-123456789");
49    getStringView(message);
50
51    std::cout <std::endl;
52
53    return 0;
54}

代码 22 行,23 行,39 行,41 行因为创建了 string 对象 所以会分配(堆)内存,但是代码 29 行,30 行,47 行,48 行,49 行也相应的创建了 string_view 对象,但是并没有发生(堆)内存分配!

5f7cf4250abb3e2d76301d54adf9e6b7.png

这个结果令人印象深刻,(堆)内存分配是一个非常耗时的操作,尽量的避免(堆)内存分配会给程序带来很大的性能提升,使用 string_view 能提升程序效率的原因也正是在此,当你需要创建很多 string 的子字符串时, string_view 带来的效率提升将更加明显。

O(n) versus O(1)

std::string 和 std::string_view 都有 substr 方法, std::string 的 substr 方法返回的是字符串的子串,而 std::string_view 的 substr 返回的则是字符串子串的"视图".听上去似乎两个方法功能上比较相似,但他们之间有一个非常大的差别: std::string::substr 是线性复杂度, std::string_view::substr 则是常数复杂度.这意味着 std::string::substr 方法的性能取决于字符串的长度,而std::string_view::substr 的性能并不受字符串长度的影响。

让我们来做一个简单的性能对比

 1#include 
2#include 
3#include 
4#include 
5#include 
6#include 
7#include 
8
9#include 
10
11static const int count = 30;
12static const int access = 10000000;
13
14int main()
15{
16    std::cout <17
18    std::ifstream inFile("grimm.txt");
19
20    std::stringstream strStream;
21    strStream <22    std::string grimmsTales = strStream.str();
23
24    size_t size = grimmsTales.size();
25
26    std::cout <"Grimms' Fairy Tales size: " <27    std::cout <28
29    // random values
30    std::random_device seed;
31    std::mt19937 engine(seed());
32    std::uniform_int_distribution<> uniformDist(0, size - count - 2);
33    std::vector randValues;34    for (auto i = 0; i 3536    auto start = std::chrono::steady_clock::now();37    for (auto i = 0; i 38    {39        grimmsTales.substr(randValues[i], count);40    }41    std::chrono::duration durString = std::chrono::steady_clock::now() - start;42    std::cout <"std::string::substr:      " <" seconds" <4344    std::string_view grimmsTalesView{ grimmsTales.c_str(), size };45    start = std::chrono::steady_clock::now();46    for (auto i = 0; i 47    {48        grimmsTalesView.substr(randValues[i], count);49    }50    std::chrono::duration durStringView = std::chrono::steady_clock::now() - start;51    std::cout <"std::string_view::substr: " <" seconds" <5253    std::cout <5455    std::cout <"durString.count()/durStringView.count(): " <5657    std::cout <5859    return 0;60}

展示程序结果之前,让我先来简单描述一下:测试代码的主要思路就是读取一个大文件的内容并保存为一个 string ,然后分别使用 std::string 和 std::string_view 的 substr 方法创建很多子字符串。我很好奇这些子字符串的创建过程需要花费多少时间。

我使用了作为程序的读取文件.代码中的 grimmTales(第22行) 存储了文件的内容.代码 34 行中我向 std::vector 填充了 10000000 个范围为 [0, size - count - 2] 的随机数字。接着就开始了正式的性能测试.代码 37 行到 40 行我使用 std::string::substr 创建了很多长度为 30 的子字符串,之所以设置长度为 30,是为了规避 std::string 的短字符串优化.代码 46 行到 49 行使用std::string_view::substr 做了相同的工作(创建子字符串)。

程序的输出如下,结果中包含了文件的长度, std::string::substr 所花费的时间,std::string_view::substr 所花费的时间以及他们之间的比例.我使用的编译器是 GCC 6.3.0。

Size 30

没有开启编译器优化的结果:

0963755d38302a963b8b0393075e55ff.png

开启编译器优化的结果:

5a9469db8073e952633f74958ef8aded.png

编译器的优化对于 std::string::substr 的性能提升并没有多大作用,但是对于 std::string_view::substr 的性能提升则效果明显.而 std::string_view::substr 的效率几乎是 std::string::substr 的 45 倍!

Different sizes

那么如果我们改变子字符串的长度,上面的测试代码又会有怎样的表现呢?当然,相关测试我都开启了编译器优化,并且相关的数字我都做了 3 位小数的四舍五入。

01c5c84606107c4ad7257586b13613b8.png

对于上面的结果我并不感到惊讶,这些数字正好反应了 std::string::substr 和 std::string_view::substr 的算法复杂度. std::string::substr 是线性复杂度(依赖于字符串长度), std::string_view::substr 则是常数复杂度(不依赖于字符串长度)。最后的结论就是: std::string_view::substr 的性能要大幅优于 std::string::substr。

文章来源:

https://blog.csdn.net/tkokof1/article/details/82527370

欢迎关注公众号『easyserverdev』,同时,您也可以加入我的 QQ 群578019391。

818b9643dcbcf64a0da9eeecebb50618.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值