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

  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 basic_string::_Rep* basic_string::_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 __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): 代码如下(详见gcc源码\libstdc++-v3\include\bits\ basic_string.h): 其中assign实现如下(详见gcc源码\libstdc++-v3\include\bits\ basic_string.tcc): template basic_string& basic_string:: 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): 通过_M_grab函数可以看出,对于同一个_Alloc对象即同一块内存,使用引用记数,否则使用clone进行拷贝。
  clone的操作最后调用如下代码(详见gcc源码\libstdc++-v3\include\bits\ char_traits.h): 代码如下(详见gcc源码\libstdc++-v3\include\bits\ basic_string.h) template basic_string operator+(const basic_string& __lhs, const basic_string& __rhs) { //第一步:生成一个string对象__str包含左值
  basic_string __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) 通过以上代码可以看出,"+="操作的代码很简单,只是简单的append,不需要额外的局部变量和临时变量,因此性能也会高得多。这也是测试数据两者相差巨大的原因。
  append函数最终调用如下函数完成操作(详见gcc源码\libstdc++-v3\include\bits\ char_traits.h): 但我们还要继续深入思考以下:为什么"+"操作要这样做呢?我个人认为原因应该是"+"操作支持连加的原因,例如str1 = str2 +str3 + str4 +str5。
  "=="操作最终的实现代码如下: 通过代码可以看出,string"=="操作最终使用的是memcmp函数实现。
  size()函数实现如下(详见gcc源码\libstdc++-v3\include\bits\ basic_string.h): 通过代码可以看出,对于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; //重置目标字符串的结尾指针

你可能感兴趣的:(技术杂绘)