字符串操作是任何一个C++开发程序无法绕过的点,很多时候针对字符串的操作需要进行优化,从而达到更优的使用效率和内存利用率。一般会采用标准的std::string替代C字符串,一方面是std::string为一个成熟的类对象,其成员操作基本能满足绝大多数的使用场景,另一个方面是因为string在标准库中已经实现了自动的扩容操作(capacity),不用使用者再为内存空间的分配而做过多的操作,从而减少对内存管理带来的性能开销。
针对string的使用,不同的使用方法会带来很大的性能差距。所以在日常的开发中,针对string使用方法的效率优化也是一个基础的问题。
1、传递方式优化
由于string是一个类,在函数调用传递和获取string对象时,如果是查询string对象的某个元素或者一些信息,或者操作修改该string时,采用引用传递替代值传递会减少临时对象的分配,并且在同时操作多个较大的string时,也会减少瞬时的内存占用突增问题。
测试代码如下:
void valTransferStr(string str)
{return;
}void refTransferStr(string& str)
{return;
}void testTranferStr()
{string str("test");auto current_time1 = std::chrono::system_clock::now();auto timestamp1 = std::chrono::duration_cast<std::chrono::milliseconds>(current_time1.time_since_epoch()).count();cout << "val begin timestamp: " << timestamp1 << endl;for (int i = 0; i < 10000000; ++i){valTransferStr(str);}auto current_time2 = std::chrono::system_clock::now();auto timestamp2 = std::chrono::duration_cast<std::chrono::milliseconds>(current_time2.time_since_epoch()).count();cout << "val end timestamp: " << timestamp2 << endl;cout << "val transfer string cost time: " << timestamp2 - timestamp1 << endl;auto current_time3 = std::chrono::system_clock::now();auto timestamp3 = std::chrono::duration_cast<std::chrono::milliseconds>(current_time3.time_since_epoch()).count();cout << "ref begin timestamp: " << timestamp3 << endl;for (int i = 0; i < 10000000; ++i){refTransferStr(str);}auto current_time4 = std::chrono::system_clock::now();auto timestamp4 = std::chrono::duration_cast<std::chrono::milliseconds>(current_time4.time_since_epoch()).count();cout << "ref end timestamp: " << timestamp4 << endl;cout << "ref transfer string cost time: " << timestamp4 - timestamp3 << endl;
}
可以看到,单纯进行1000W次值传递耗时是引用耗时的52倍左右,所以采用能在使用引用传递的情况尽量采用引用传递的方式。
2、扩容分配优化
针对string的扩容策略在另一篇文章中已经详细介绍过《std::string在 Windows MSVC和Linux Gcc 中capacity容量扩容策略的分析和对比》,这里就不再介绍。如果需要操作的string需要频繁的进行拼接操作,那么需要在代码工程定义该string的地方,预先调用reserver()进行容量的分配,避免频繁的capacity扩容,提高运行效率。
预分配方式耗时低于反复扩容方式。
测试代码如下:
void insertStr1()
{string str;for (int i = 0; i < 10000000; ++i){str += "1";}
}void insertStr2()
{string str;str.reserve(10000000);for (int i = 0; i < 10000000; ++i){str += "1";}
}void testInsertCostTime()
{auto current_time1 = std::chrono::system_clock::now();auto timestamp1 = std::chrono::duration_cast<std::chrono::milliseconds>(current_time1.time_since_epoch()).count();cout << "origin insert begin timestamp: " << timestamp1 << endl;insertStr1();auto current_time2 = std::chrono::system_clock::now();auto timestamp2 = std::chrono::duration_cast<std::chrono::milliseconds>(current_time2.time_since_epoch()).count();cout << "origin insert end timestamp: " << timestamp2 << endl;cout << "origin insert string cost time: " << timestamp2 - timestamp1 << endl;auto current_time3 = std::chrono::system_clock::now();auto timestamp3 = std::chrono::duration_cast<std::chrono::milliseconds>(current_time3.time_since_epoch()).count();cout << "reserver insert begin timestamp: " << timestamp3 << endl;insertStr2();auto current_time4 = std::chrono::system_clock::now();auto timestamp4 = std::chrono::duration_cast<std::chrono::milliseconds>(current_time4.time_since_epoch()).count();cout << "reserver insert end timestamp: " << timestamp4 << endl;cout << "reserver insert string cost time: " << timestamp4 - timestamp3 << endl;
}
3、字符拼接方式的调整
针对字符的拼接在另一篇文章中已经详细介绍过《std::string多个插入字符方式以及效率对比》。
4、遍历方式优化
直接贴上迭代器遍历以及标准的for遍历的结果
void iterLoop(string &str)
{auto current_time1 = std::chrono::system_clock::now();auto timestamp1 = std::chrono::duration_cast<std::chrono::milliseconds>(current_time1.time_since_epoch()).count();cout << "iter loop begin timestamp: " << timestamp1 << endl;for (auto it = str.begin(); it != str.end(); ++it){if (*it == 'a'){}}auto current_time2 = std::chrono::system_clock::now();auto timestamp2 = std::chrono::duration_cast<std::chrono::milliseconds>(current_time2.time_since_epoch()).count();cout << "iter loop end timestamp: " << timestamp2 << endl;cout << "iter loop string cost time: " << timestamp2 - timestamp1 << endl;
}void indexLoop(string& str)
{auto current_time3 = std::chrono::system_clock::now();auto timestamp3 = std::chrono::duration_cast<std::chrono::milliseconds>(current_time3.time_since_epoch()).count();cout << "index loop begin timestamp: " << timestamp3 << endl;auto len = str.size();for (int i = 0; i < str.size(); ++i){if (str[i] == 'a'){}}auto current_time4 = std::chrono::system_clock::now();auto timestamp4 = std::chrono::duration_cast<std::chrono::milliseconds>(current_time4.time_since_epoch()).count();cout << "index loop end timestamp: " << timestamp4 << endl;cout << "index loop string cost time: " << timestamp4 - timestamp3 << endl;
}void testLoopStr()
{string str;for (int i = 0; i < 10000000; ++i){str.push_back('1');}iterLoop(str);indexLoop(str);
}
发现迭代器遍历执行时间大于标准for遍历,但是迭代器操作不会产生临时对象,这一点优于下标方式,所以要看实际使用场景
5、复合赋值操作避免创建临时对象
在进行字符串拼接时,字符串的连接运算符+ 开销会很大,它会调用内存管理器构建一个临时的字符串对象来保存连接后的字符串,再将该字符串赋值给目的字符串。可以采用复合赋值操作符”xxx+=yyy”来替代”xxx= xxx + yyy”的方式
测试代码如下:
void testAddStr1(string &str)
{string res;auto len = str.size();auto current_time1 = std::chrono::system_clock::now();auto timestamp1 = std::chrono::duration_cast<std::chrono::milliseconds>(current_time1.time_since_epoch()).count();cout << "=+ begin timestamp: " << timestamp1 << endl;for (int i = 0; i < len; ++i){res = res + str[i];}auto current_time2 = std::chrono::system_clock::now();auto timestamp2 = std::chrono::duration_cast<std::chrono::milliseconds>(current_time2.time_since_epoch()).count();cout << "=+ end timestamp: " << timestamp2 << endl;cout << "=+ string cost time: " << timestamp2 - timestamp1 << endl;
}void testAddStr2(string& str)
{string res;auto len = str.size();auto current_time3 = std::chrono::system_clock::now();auto timestamp3 = std::chrono::duration_cast<std::chrono::milliseconds>(current_time3.time_since_epoch()).count();cout << "+= begin timestamp: " << timestamp3 << endl;for (int i = 0; i < len; ++i){res += str[i];}auto current_time4 = std::chrono::system_clock::now();auto timestamp4 = std::chrono::duration_cast<std::chrono::milliseconds>(current_time4.time_since_epoch()).count();cout << "+= end timestamp: " << timestamp4 << endl;cout << "+= string cost time: " << timestamp4 - timestamp3 << endl;
}void testAddStr()
{string str;for (int i = 0; i < 10000; ++i){str.push_back('1');}testAddStr1(str);testAddStr2(str);
}
6、减少频繁调用
举一个例子来说明当前优化点,比如size()成员函数的调用。字符串的遍历为一个常见操作,如果一个字符串较大,在for遍历时,如果每次都用当前的index与size()进行比较,则需要每次都调用size()获取字符串大小,调用次数为size次,如果用一个临时对象先记录size大小,在for遍历时就不需要进行每次的调用,减少时间开销。
测试代码如下:
void testCallStr1(string &str)
{string res;auto current_time1 = std::chrono::system_clock::now();auto timestamp1 = std::chrono::duration_cast<std::chrono::milliseconds>(current_time1.time_since_epoch()).count();cout << "origin call begin timestamp: " << timestamp1 << endl;for (int i = 0; i < str.size(); ++i){}auto current_time2 = std::chrono::system_clock::now();auto timestamp2 = std::chrono::duration_cast<std::chrono::milliseconds>(current_time2.time_since_epoch()).count();cout << "origin call end timestamp: " << timestamp2 << endl;cout << "origin call cost time: " << timestamp2 - timestamp1 << endl;
}void testCallStr2(string& str)
{string res;auto len = str.size();auto current_time3 = std::chrono::system_clock::now();auto timestamp3 = std::chrono::duration_cast<std::chrono::milliseconds>(current_time3.time_since_epoch()).count();cout << "opt call begin timestamp: " << timestamp3 << endl;for (int i = 0; i < len; ++i){}auto current_time4 = std::chrono::system_clock::now();auto timestamp4 = std::chrono::duration_cast<std::chrono::milliseconds>(current_time4.time_since_epoch()).count();cout << "opt call end timestamp: " << timestamp4 << endl;cout << "opt call cost time: " << timestamp4 - timestamp3 << endl;
}void testStr()
{string str;for (int i = 0; i < 10000000; ++i){str.push_back('1');}testCallStr1(str);testCallStr2(str);
}
可以看到优化后的效率有所提高