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++的“+”操作和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的字符串比較 |
2.3. 數據分析
1)構造“test”串的時間是構造空串的時間的6倍
2)“=”和“+=”時間相近
3)“+”操作比“+=”操作性能要低30%
4)循環“+”操作的性能極低,而循環“+=”好很多
5)傳引用和傳值的效率相差2.5倍,傳值和返回對象的時間基本相同;
6)size()操作是恒定時間,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_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))); } |
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++的“+=”操作性能比C的strlen+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