C/C++字符串使用军规
本文对常见的C++ string使用方式进行了测试,并结合源代码分析,总结出如何高效的使用C++ string对象。
测试环境信息如下:
配置项目 |
配置信息 |
备注 |
CPU |
8 * 2核 |
Intel(R) Xeon(R) CPU E5620 主频2.40GHz, 物理CPU 2个,逻辑CPU 16个 |
内存 |
24G |
6块 * 4G DDR3 1333 REG |
OS |
Redhat 5 |
Linux platform2 2.6.18-164.el5 #1 SMP Tue Aug 18 15:51:48 EDT 2009 x86_64 x86_64 x86_64 GNU/Linux |
编译器 |
gcc 4.1.2 |
gcc version 4.1.2 20080704 (Red Hat 4.1.2-48) |
测试结果如下:
操作(1M次) |
性能(ms) |
C语言函数 |
C语言性能(ms) |
备注 |
创建空串 |
13 |
NA |
NA |
NA |
创建一个“test”串 |
85 |
char[]=”test” |
5 |
NA |
“=”操作 |
95 |
strcpy() |
16 |
|
“+=”操作 |
95 |
strcat() |
25 |
两个字符串长度都是10 |
“+”操作 |
125 |
strcat() |
||
循环“+”10长度字符串 |
2852631 |
strcat() |
769268 |
C++的“+”操作和C的strcat操作性能都很差,但原因不一样 |
循环“+=”10长度字符串 |
43 |
strlen() + sprintf() |
3877099 |
C代码如下: sprintf(pos, "%s", part); len = strlen(buffer); pos = buffer + len; |
函数参数传引用 |
40 |
NA |
NA |
NA |
函数参数传值 |
100 |
NA |
NA |
NA |
返回string局部变量 |
110 |
NA |
NA |
NA |
size()操作 |
4 |
strlen() |
40 |
字符串长度为10 |
“==”操作 |
43 |
strcmp() |
22 |
两个长度为10的字符串比较 |
1)构造“test”串的时间是构造空串的时间的6倍
2)“=”和“+=”时间相近
3)“+”操作比“+=”操作性能要低30%
4)循环“+”操作的性能极低,而循环“+=”好很多
5)传引用和传值的效率相差2.5倍,传值和返回对象的时间基本相同;
6)size()操作是恒定时间,strlen()是和字符串长度线性相关的;
string的内存申请函数实现如下(为了阅读方便,去掉了注释和一些辅助代码,详见gcc源码/libstdc++-v3/include/bits/basic_string.tcc):
template<typename _CharT, typename _Traits, typename _Alloc> typename basic_string<_CharT, _Traits, _Alloc>::_Rep* basic_string<_CharT, _Traits, _Alloc>::_Rep:: _S_create(size_type __capacity, size_type __old_capacity, const _Alloc& __alloc) { // _GLIBCXX_RESOLVE_LIB_DEFECTS // 83. String::npos vs. string::max_size() if (__capacity > _S_max_size) __throw_length_error(__N("basic_string::_S_create"));
const size_type __pagesize = 4096; const size_type __malloc_header_size = 4 * sizeof(void*);
//如下代码进行空间大小计算,采用了指数增长的方式,即:如果要求的空间__capacity小于当前空间__old_capacity的2倍,则按照当前空间的2倍来申请。 if (__capacity > __old_capacity && __capacity < 2 * __old_capacity) __capacity = 2 * __old_capacity;
// NB: Need an array of char_type[__capacity], plus a terminating // null char_type() element, plus enough for the _Rep data structure. // Whew. Seemingly so needy, yet so elemental. size_type __size = (__capacity + 1) * sizeof(_CharT) + sizeof(_Rep);
const size_type __adj_size = __size + __malloc_header_size; if (__adj_size > __pagesize && __capacity > __old_capacity) { const size_type __extra = __pagesize - __adj_size % __pagesize; __capacity += __extra / sizeof(_CharT); // Never allocate a string bigger than _S_max_size. if (__capacity > _S_max_size) __capacity = _S_max_size; __size = (__capacity + 1) * sizeof(_CharT) + sizeof(_Rep); }
//此处开始分配空间,第一步使用allocate函数申请空间,第二步使用new (__place)的方式生成一个对象返回。此处分两步的主要原因应该是内存分配和释放是由allocator实现的,string对象只使用内存,所以使用定位new的方式返回对象给string,这样string本身无法delete内存。 void* __place = _Raw_bytes_alloc(__alloc).allocate(__size); _Rep *__p = new (__place) _Rep; __p->_M_capacity = __capacity; __p->_M_set_sharable(); return __p; } |
gcc中的allocator实现如下(详见gcc源码/libstdc++-v3/include/ext/new_allocator.h):
pointer allocate(size_type __n, const void* = 0) { if (__builtin_expect(__n > this->max_size(), false)) std::__throw_bad_alloc(); //如下代码使用new函数申请内存 return static_cast<_Tp*>(::operator new(__n * sizeof(_Tp))); } |
代码如下(详见gcc源码/libstdc++-v3/include/bits/ basic_string.h):
basic_string& operator=(const basic_string& __str) { return this->assign(__str); } |
其中assign实现如下(详见gcc源码/libstdc++-v3/include/bits/ basic_string.tcc):
template<typename _CharT, typename _Traits, typename _Alloc> basic_string<_CharT, _Traits, _Alloc>& basic_string<_CharT, _Traits, _Alloc>:: assign(const basic_string& __str) { if (_M_rep() != __str._M_rep()) { // XXX MT const allocator_type __a = this->get_allocator(); _CharT* __tmp = __str._M_rep()->_M_grab(__a, __str.get_allocator()); _M_rep()->_M_dispose(__a); _M_data(__tmp); } return *this; } |
_M_grab函数实现如下(详见gcc源码/libstdc++-v3/include/bits/ basic_string.h):
_CharT* _M_grab(const _Alloc& __alloc1, const _Alloc& __alloc2) { return (!_M_is_leaked() && __alloc1 == __alloc2) ? _M_refcopy() : _M_clone(__alloc1); } |
通过_M_grab函数可以看出,对于同一个_Alloc对象即同一块内存,使用引用记数,否则使用clone进行拷贝。
clone的操作最后调用如下代码(详见gcc源码/libstdc++-v3/include/bits/ char_traits.h):
static char_type* copy(char_type* __s1, const char_type* __s2, size_t __n) { return static_cast<char_type*>(memcpy(__s1, __s2, __n)); } |
代码如下(详见gcc源码/libstdc++-v3/include/bits/ basic_string.h)
template<typename _CharT, typename _Traits, typename _Alloc> basic_string<_CharT, _Traits, _Alloc> operator+(const basic_string<_CharT, _Traits, _Alloc>& __lhs, const basic_string<_CharT, _Traits, _Alloc>& __rhs) { //第一步:生成一个string对象__str包含左值 basic_string<_CharT, _Traits, _Alloc> __str(__lhs); //第二步:将右值append到__str __str.append(__rhs); //第三步:返回局部变量__str return __str; } |
通过以上代码可以看出,“+”操作耗费的性能是很大的:第一步创建一个对象,在函数结束时析构对象,第三步调用拷贝构造函数构造临时对象,然后在赋值结束后析构对象。
对于一个连加的表达式,这样的耗费更加可观,例如如下语句:
string str1 = str2 +str3 + str4 +str5;
则以上过程会执行3次,总共6次构造和析构操作,而且随着+次数越来越多,字符串越来越长,构造析构成本更高。测试数据显示连续操作100万次,耗费时间达到了惊人的2852631ms!
代码如下(详见gcc源码/libstdc++-v3/include/bits/ basic_string.h)
basic_string& operator+=(const basic_string& __str) { return this->append(__str); } |
通过以上代码可以看出,“+=”操作的代码很简单,只是简单的append,不需要额外的局部变量和临时变量,因此性能也会高得多。这也是测试数据两者相差巨大的原因。
append函数最终调用如下函数完成操作(详见gcc源码/libstdc++-v3/include/bits/ char_traits.h):
static char_type* copy(char_type* __s1, const char_type* __s2, size_t __n) { return static_cast<char_type*>(memcpy(__s1, __s2, __n)); } |
但我们还要继续深入思考以下:为什么“+”操作要这样做呢?我个人认为原因应该是“+”操作支持连加的原因,例如str1 = str2 +str3 + str4 +str5。
“==“操作最终的实现代码如下:
static int compare(const char_type* __s1, const char_type* __s2, size_t __n) { return memcmp(__s1, __s2, __n); } |
通过代码可以看出,string“==”操作最终使用的是memcmp函数实现。
size()函数实现如下(详见gcc源码/libstdc++-v3/include/bits/ basic_string.h):
size_type size() const { return _M_rep()->_M_length; } |
通过代码可以看出,对于string对象来说,已经使用了一个成员变量来记录字符串长度,而不需要像C语言的strlen()函数那样采用遍历的方式来求长度,这也是C++的“+=”操作性能比C的strlen+sprintf或者strcat操作高出几个数量级的原因。
从以下几方面来看,大型项目推荐使用C++的字符串:
1) 测试结果来看,除了+操作外,100万次操作的性能基本都在100ms以内;
2) 从源码分析来看,string的操作最终基本上都是调用mem*函数,这和C语言的字符串也是一致的;
3) string对象封装了内存管理,操作方便,使用安全简单,不会像C语言字符串那样容易导致内存问题(溢出、泄露、非法内存);
4) 使用“+=” 循环拼接字符串性能优势很明显;
但在使用过程中为了尽可能的提高性能,需要遵循以下原则:
l 函数调用时使用传引用,而不要使用传值,不需要改变的参数加上const修饰符
l 使用“+=”操作,而不要使用“+”操作,即使写多个“+=”也无所谓
例如将str1 = str2 +str3 + str4 +str5写成如下语句:
str1 += str2;
str1 += str3;
str1 += str4;
str1 += str5;
同样,C语言的字符串处理性能总体上要比C++ string性能高,但同样需要避免C语言的性能缺陷,即:
l 要尽量避免显示或者隐式(strcat)求字符串的长度,特别是对长字符串求长度。
例如,测试用例中C语言的循环字符串拼接操作sprintf + strlen并不是唯一的实现方式,参考C++ string的实现,C语言拼接字符串优化的方式如下(测试结果是78ms):
len = strlen(part); //计算需要拼接的字符串长度 memcpy(pos, part, len); //使用memcpy将字符串拼接到目标字符串末尾 pos += len; //重置目标字符串的结尾指针 |