一、为什么需要优化字符串的使用
当前C++编程常用的字符串是有两种:MFC的CString和模板库的std::string,在使用过程中因为字符串的一些特性会导致cpu消耗增加,所以根据下面介绍的字符串特性我们可以进行一些使用上面的优化,那么先讲讲字符串有哪些特性。
二、字符串的特性:
- 字符串是动态分配的,那在使用过程中就存在频繁的复制、内存申请与销毁操作
- 字符串的赋值操作是内存的重新分配
- 下面我们来看看测试代码,在特性1中打印出来的str2的值依然是”hello”,说明str1赋值给str2的时候str2是有自己的内存空间,所以修改str1的值并不会影响到str2,所以说明字符串的赋值操作是内存的重新分配。在特性2中, str1 = str2 + str3 + str4;这句代码str2+str3会构造一个临时对象,临时对象+str4又会构造一个临时对象,这个现象说明字符串在使用过程中存在频繁的对象生成和释放。
// string的特性1
void string_nature1()
{
string str1, str2;
str1 = "hello";
str2 = str1;
str1[0] = 'w';
cout << "str2:" << str2 << endl;
}
// string的特性2
void string_nature2()
{
string str1 = "1", str2 = "2", str3 = "3", str4 = "4";
// 会构造几个临时string对象,几次内存释放
str1 = str2 + str3 + str4;
cout << "str1:" << str1 << endl;
}
三、举例优化string的使用:
根据上面的特性,我们在使用过程中可以有针对的进行优化,下面我们从一个最原始的功能函数开始进行优化,优化的过程是根据字符串的特性循序进步的。(案例的运行环境是win10+vs2019)
- 原型函数,下面是一个移除字符串控制符的函数
std::string remove_ctrl_org(std::string s)
{
std::string result;
for (unsigned int i = 0; i < s.length(); ++i)
{
if (s[i] >= 0x20)
result = result + s[i];
}
return result;
}
- 上面的代码中result + s[i] 会构建一个临时string对象,那么我们使用复合赋值操作避免临时字符串。 从下图的运行结果可以看出,优化后的性能比没有优化的有20倍的效果提升。
std::string remove_ctrl_append(std::string s)
{
std::string result;
for (unsigned int i = 0; i < s.length(); ++i)
{
if (s[i] >= 0x20)
result += s[i];// 省略掉了临时对象的生成,此处查看string源码是调用了append
}
return result;
}
- 接下来我们继续优化,字符串使用过程中空间不够时会重新申请内存和拷贝内容,那么我们可以通过预留存储空间减少内存的重新分配。通过测试结果,相比上一次的优化有将近1倍的性能提升。
std::string remove_ctrl_reserve(std::string s)
{
std::string result;
result.reserve(s.length());// 提前申请内存
for (unsigned int i = 0; i < s.length(); ++i)
{
if (s[i] >= 0x20)
result += s[i];
}
return result;
}
- 我们继续优化,发现在传参过程中有字符串拷贝的存在,那么我改为传入引用。运行测试案例,对比前一次优化有50%的性能提升
std::string remove_ctrl_ref(const std::string& s)// 传参改为引用
{
std::string result;
result.reserve(s.length());
for (unsigned int i = 0; i < s.length(); ++i)
{
if (s[i] >= 0x20)
result += s[i];
}
return result;
}
- 继续优化, 消除对返回的字符串的复制。这个时候的测试结果我们发现性能提升不明显
void remove_ctrl_ref_result(const std::string& s, std::string& result)
{
result.reserve(s.length());
for (unsigned int i = 0; i < s.length(); ++i)
{
if (s[i] >= 0x20)
result += s[i];
}
}
- 还能怎么优化呢? 我们可以换个方式来优化,丢弃掉string库手动写一个函数来实现这个功能。测试发现自己手动写的函数在性能上面有了巨大的提升,这是因为我们舍弃了string类的使用,减少了对象的新建与释放的性能开销。 最后相比于最原始的方法,性能提升了将近200倍。
void remove_ctrl_c(const char* src, char* des, size_t size)
{
for (size_t i = 0; i < size; ++i)
{
if (src[i] >= 0x20)
{
*des++ = src[i];
}
}
*des = 0;
}
- 最后我们可以再换个思路,从算法层面进行优化。下面的代码不创建新的字符串,而是修改参数字符串的值作为结果返回,也不用一个一个字符进行拼接。 从测试结果可以看出,只是修改了一下算法,在性能上面就已经比上面用字符串的优化最好的结果都要更快(除了手动写的纯C函数外)。
std::string remove_ctrl_erase(std::string s)
{
for (size_t i = 0; i < s.length();)
{
if (s[i] < 0x20)
s.erase(i, 1);
else
++i;
}
return s;
}
四、优化string的使用的总
总结上面的测试案例,我们可以得出一些结论:
- 由于字符串是动态分配内存的,因此它们的性能开销非常大。它们在表达式中的行为与值类似,它们的实现方式中需要大量的复制。
- 将字符串作为对象而非值可以降低内存分配和复制的频率。
- 为字符串预留内存空间可以减少内存分配的开销。
- 将指向字符串的常量引用传递给函数与传递值的结果几乎一样,但是更加高效。
- 将函数的结果通过输出参数作为引用返回给调用方会复用实参的存储空间,这可能比分配新的存储空间更高效。
- 有时候,换一种不同的算法会更容易优化或是本身就更高效。
- 标准库中的类是为通用用途而实现的,它们很简单。它们并不需要特别高效,也没有为某些特殊用途而进行优化。
五、在使用过程中消除一些不必要的转换:
1.建议将函数返回值写成char*而非string,这样带来的好处
a.避免返回时需要构造string对象
b.假如其它调用的地方需要用到char* 还需要再转换一次
2.代码示例如下:
std::string GetName()
{
return "name";
}
// 这会将返回值的转换推迟至它真正被使用的时候
const char* GetName2()
{
return "name";
}
void string_convert_test()
{
char const* p = GetName2(); // 没有转换
std::string s = GetName2(); // 使用时转换为'std::string'
std::cout << GetName2(); // 没有转换
}
后面我会继续讲解C++性能优化其它方面的知识,希望喜欢的朋友可以关注收藏。
版权声明:本文为dm569263708原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。