C/C++字符串使用军规

C/C++字符串使用军规 

1. 概述

本文对常见的C++ string使用方式进行了测试,并结合源代码分析,总结出如何高效的使用C++ string对象。

2. 测试情况

2.1. 测试环境

测试环境信息如下:

配置项目

配置信息

备注

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)

 

2.2. 测试结果

测试结果如下:

操作(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++的“+”操作和Cstrcat操作性能都很差,但原因不一样

循环“+=”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的字符串比较

 

2.3. 数据分析

1)构造“test”串的时间是构造空串的时间的6

2)“=”和“+=”时间相近

3)“+”操作比“+=”操作性能要低30%

4循环“+”操作的性能极低,而循环“+=”好很多

5)传引用和传值的效率相差2.5倍,传值和返回对象的时间基本相同;

6size()操作是恒定时间,strlen()是和字符串长度线性相关的;

3. 源码分析

3.1. string的内存管理

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_capacity2倍,则按照当前空间的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)));

      }

 

3.2. 常见操作

 

3.2.1. “=”

代码如下(详见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)); }

 

 

3.2.2. +

代码如下(详见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

3.2.3. +=”

代码如下(详见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

 

3.2.4.  ==”操作

==“操作最终的实现代码如下:

      static int

      compare(const char_type* __s1, const char_type* __s2, size_t __n)

      { return memcmp(__s1, __s2, __n); }

 

通过代码可以看出,string==”操作最终使用的是memcmp函数实现。

 

3.2.5. size()

size()函数实现如下(详见gcc源码/libstdc++-v3/include/bits/ basic_string.h):

      size_type

      size() const

      { return _M_rep()->_M_length; }

 

通过代码可以看出,对于string对象来说,已经使用了一个成员变量来记录字符串长度,而不需要像C语言的strlen()函数那样采用遍历的方式来求长度,这也是C++的“+=”操作性能比Cstrlen+sprintf或者strcat操作高出几个数量级的原因。

4. 使用指南

从以下几方面来看,大型项目推荐使用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;                      //重置目标字符串的结尾指针

 

你可能感兴趣的:(String,测试,gcc,basic,语言,代码分析)