標籤:

考慮R值引用,c++下多字元串連接,如何寫更高效?

比如有以下的兩個std::string,

std::string s1 = "s1";

std::string s2 = "s2"

// 寫法1

s1 += s2 + " is " + s1 + ", string";

// 寫法2

s1.append(s2).append("is").append(s1).append(",string");

或者有更好的寫法?請給出合理解釋。謝謝


若你這樣寫的話,其實瓶頸不會是在你認為的地方,如是否有右值引用等。若你去profile,你會發現你的這兩種寫法的瓶頸在 += / append 上。而這個回答,我也想要凸顯profile的重要性,所以會有很多的圖,以及詳細的步驟。

我們舉一個簡單的例子:

#include &
#include &
#include &

#define VER 1

int main()
{
typedef std::chrono::high_resolution_clock clock;
typedef std::chrono::duration& mil;
std::string str;
std::string s1 = "s1";
std::string s2 = "s2";
auto t0 = clock::now();
#if VER == 1
for (int i = 0; i &< 20; ++i) { s1 += s2 + "is" + s1 + ",string"; } #elif VER == 2 for (int i = 0; i &< 20; ++i) { s1.append(s2).append("is").append(s1).append(",string"); } #endif auto t1 = clock::now(); std::cout &<&< mil(t1 - t0).count() &<&< "ms "; }

20次就可以反映問題了。

我們首先定義 VER 為 1,然後去用Profile. 由於Mac下我沒有找到gperf,所以我使用了XCode的Instruments來進行工作。

首先,我們來跑一下O3的情況下,其時間為多少:

平均38~39.

然後,我們直接跑一下第二種的情況:

平均在大約25上下。

乍看之下,似乎第二種比第一種好很多,所以是不是跟右值引用有很大的關係呢?其實這都只是表象,若我們還想更大的提高,需要更深次的看。若我們去profile一下,分別觀測一下這兩種情況:

在XCode中,我們可以選用Instruments來達到我們的目的,如下所示:

VER 1:

選擇Time Profiler.

勾選High Frequency,然後點擊左上角的紅色按鈕Record.

然後我們點擊出現的函數下拉小三角:

我們可以發現什麼呢?我們可以發現其實時間消耗最多的是在append上,而append上最多的是因為__grow_by_and_replace,即string容器的自動增長。

我們跟蹤進去,即可發現是因為發現如今的size加上即將要加的字元以後會超過capacity,就會調用。而__grow_by_and_replace會做什麼呢?

那就是會做copy動作,而copy是很費時間的。在接下分析VER 1前,我們先看看VER 2的情況:

其實,VER 2的消耗也是如此,只是VER 2相比VER 1,只有s1有這個消耗,而在s1中還有s2. 那麼,為了避免這樣的內存自動分配,我們可以怎麼做呢?那就是使用reserve,來避免容器的自動增長。

於是,我們在VER 1寫下這樣的一行代碼:

然後,我們編譯測試:

效果立竿見影。

然後,我們profile會發現依然有append:

那麼,為什麼會有呢?在string中,我們有operator +, 然後他會造成什麼情況呢?

若我現在返回的這個對象,然後再連續加上一個新的字元,則再次進入operator + ,那麼再次調用append,則再次掉入了capacity不足的問題,於是再次__grow_by_and_replace,從而copy,而這樣的返回對象會出現在哪裡呢?會出現s2 + "is"以後再次 + s1,然後完畢後再次 + ",string". 而由於這個返回對象我們很難控制,所以,我們會怎麼來進一步改善呢?那就是進一步控制,分開 +=.

再次運行:

平均約17 ~ 18.

我們再次profile:

可以看到還有__grow_by_and_replace,但是只有一個了。那麼,問題是為什麼還有呢?

其實是我設定的reserve參數不適合新的這種寫法了。那麼我怎麼知道設定多少呢?如我剛才這樣設置一樣?其實很簡單,直接std::cout &<&< s1.size() &<&< std::endl;就得到了。

我們設定好更好的參數:

然後再次運行:

差不多11 ~ 12左右吧。

然後我們再次profile:

如我們所願,沒有了__grow_by_and_replace。那麼,我們可以發現VER 1的寫法是不是變成了VER 2的寫法了?然後只是多了一個reserve,於是我們把reserve加到VER 2中:

我們會發現平均也是11 ~ 12ms,和 VER 1 持平了(畢竟一個寫法)。然後profile也是如此:

沒有了__grow_by_and_replace.於是,我們完成了從平均38 ~ 39 降到 11 ~ 12 的工作,寫法的改進都是自然而然的,一切的根據都是: Profile and Data.


c++的字元串操作,想提高效率,就兩點1)根據需要調整capacity;2)避免生成新的string obj


當然是用rope啦http://llvm.org/docs/doxygen/html/classllvm_1_1Twine.html


《imprefect C++》里有個高效拼接字元串的例子,基本就是是寫法2優化再加語法糖。

語法糖比較簡單,2B青年可以用逗號,正常青年用&<&<就行。

優化無非是少分配,少拷貝,能分配在棧上(如果有格式化數字之類需求要注意這一條),就別放堆上。

就題主的例子,無非在2的基礎上,先統計所有子串的長度,最後只分配一次,集中拷貝。大致做法就是先生成一個棧上臨時對象列表,表達式最後統一處理。

沒有語法糖的形式大概這樣:joinstr(s1,strref(s2)+strref(s3)+strref(s4)).當然分配一次這種優化,除非表達式特別長也沒啥顯著效果


推薦閱讀:

C#轉C++開發,該歷經怎樣的學習路線?
如何評價Qt Lite Project?
什麼時候用異常,什麼時候用斷言?
以後想做大型遊戲(至少是端游,不是手游),不知道是不是一定需要精通C++或者熟練?
為什麼老師不推薦用《C++ Primer》作為教材?

TAG:C |