閱讀374 返回首頁    go 阿裏雲 go 技術社區[雲棲]


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;                      //重置目標字符串的結尾指針

 

最後更新:2017-04-02 06:51:39

  上一篇:go 六級學習筆記(1)
  下一篇:go 關於Myeclipse8.6和 Flex4 插件安裝,進度不走的問題解決辦法