善用 swap 塞資料進 container
今天早上半夢半醒之間想到的技巧,不曉得是不是世所周知的技巧,至少這對最近比較少在用功的我來說,是新東西。
通常,我們要塞東西進 container,如讀取一個 CSV 檔,存進 vector< vector<string> > 裡,會這麼寫:
vector< vector<string> > csv_data;
ifstream ifs("data.csv");
while (ifs) {
string line;
getline(ifs, line);
vector<string> tokens;
// splits line string delimited by ",", and save to tokens.
split(line, ",", tokens);
csv_data.push_back(tokens);
}
問題出在 csv_data.push_back() 的那一行,tokens 會被「複製」一份進 csv_data,然後隨即被解構。這不禁讓我在想,若是 tokens 又肥又大,又不想要讓 csv_tokens 只裝 pointer to vector<string> 的話,那這個「複製」不就既耗時又耗記憶體?
例如,以上例來說,若這個 CSV 檔,每一行都各有 1000 個 entries,那 push_back() 總共要複製 1000 個 string,實在會是個效能殺手。
於是我就想到,其實 push_back() 那一行,我們可以這麼寫:
csv_data.push_back(vector<string>()); // push dummy entry first csv_data.at(csv_data.size() - 1).swap(tokens); // swap with the real one
我們先 push_back 一個「空的」物件進去,因為是空的,所以「複製」成本極低。然後,再用 at() 得到 reference to 剛剛那個空物件,利用 swap() 與 tokens 交換內容。交換之後,csv_data 的最後一個 entry,就有完整的資料,但 tokens 卻變成空物件。但因為 tokens 將被拋棄解構,所以沒有關係。
如此一來,我們就省去了 1000 個 string 的複製,只需要耗費空的 vector<string> 的複製成本,與 swap() 的成本就好。而通常來說,swap() 的成本極低,因此原本既耗時又耗記憶體的步驟,就被優化成既快速又省記憶體了。



8 Comments
csv_data.back().swap(tokens);
BTW, C++0x 的 sequence container 都有支援 rvalue reference. 以後就不需要這種 two stage push_back 了.
void push_back(T&& x);
用不著 sawp 吧:
csv_data.push_back(vector()); // 一樣
split(line, ",", csv_data.back()); // 直接 split 進 cvs_data 的最後一個元素.
不過你的 split 是哪裡的啊? 跟 boost 不一樣:
boost::split(csv_data.back(), line, is_any_of(","));
或許這樣寫也可以:
csv_data.push_back( vector() );
split( line, ",", csv_data.back() );
fr3@K,
謝謝,用 back() 確實比較簡潔易懂。不過若是其他的 container 如 set,那可能就要用 a_set.insert(vector<line>()).first->swap(tokens); 了。
av,
這篇的 split() 是我掰出來的假想函式,不是 boost::split()。
果然本篇引起熱烈迴響 XD
來提一下我在 IRC 裡跟 jeffhung 討論過的意見:
其實前陣子我就碰過 av 提到的寫法,但我最後決定改掉它,回歸原始操作方式。因為......
1. 沒有 profiling 無法證明這樣真能有幫助;事實上,在最近我碰到的另一個類似案例中,profiling 結果發現 bottleneck 總是在 I/O 處,而且遠大於 push_back() 及其相應的 copy constructor/assignment, 無法透過上述 reference 操作或是 reserve 來改善。
2. 在 1. 的前提下,可讀性及日後維護的其他考量遠比這類最佳化更重要。尤其是第五則 comment 也提到了,只要換個 container 事情就不同了。
3. 如果再多為將來可能接管的新手想一點,寧可先假裝所有的 container 和 copy constructor 都支援 reference 操作和 copy-on-write.
It has been mentioned in effective c++3rd item 25
Post a Comment