[译]C++17,使用 string_view 来避免复制
看到一個介紹 C++17 的系列博文(原文),有十來篇的樣子,覺得挺好,看看有時間能不能都簡單翻譯一下,這是第五篇~
當(dāng)字符串?dāng)?shù)據(jù)的所有權(quán)已經(jīng)確定(譬如由某個string對象持有),并且你只想訪問(而不修改)他們時,使用 std::string_view 可以避免字符串?dāng)?shù)據(jù)的復(fù)制,從而提高程序效率,這(指程序效率)也是這篇文章的主要內(nèi)容.
這次要介紹的 string_view 是 C++17 的一個主要特性.
我假設(shè)你已經(jīng)了解了一些 std::string_view 的知識,如果沒有,可以看看我之前的這篇文章.C++ 中的 string 類型在堆上存放自己的字符串?dāng)?shù)據(jù),所以當(dāng)你處理 string 類型的時候,很容易就會產(chǎn)生(堆)內(nèi)存分配.
Small string optimisation
我們先看下以下的示例代碼:
#include <iostream> #include <string>void* operator new(std::size_t count) {std::cout << " " << count << " bytes" << std::endl;return malloc(count); }void getString(const std::string& str) {}int main() {std::cout << std::endl;std::cout << "std::string" << std::endl;std::string small = "0123456789";std::string substr = small.substr(5);std::cout << " " << substr << std::endl;std::cout << std::endl;std::cout << "getString" << std::endl;getString(small);getString("0123456789");const char message[] = "0123456789";getString(message);std::cout << std::endl;return 0; }代碼第4到第8行,我重載了全局的 new 操作符,這樣我就能跟蹤(堆)內(nèi)存的分配了,而后,代碼分別在第18行,第19行,第27行,第29行創(chuàng)建了string對象,所以這幾處代碼都會產(chǎn)生(堆)內(nèi)存分配.相關(guān)的程序輸出如下:
咦, 程序竟然沒有產(chǎn)生內(nèi)存分配?這是怎么回事?其實 string 類型只有在字符串超過指定大小(具體實現(xiàn)相關(guān))時才會申請(堆)內(nèi)存,對于 MSVC 來說,指定大小為 15, 對于 GCC 和 Clang,這個值則為 23.
這也就意味著,較短的字符串?dāng)?shù)據(jù)是直接存儲于 string 的對象內(nèi)存中的,不需要分配(堆)內(nèi)存.
從現(xiàn)在開始,示例代碼中的字符串將擁有至少30個字符,這樣我們就不需要關(guān)注短字符串優(yōu)化了.好了,帶著這個前提(字符串長度>=30個字符),讓我們重新開始講解.
No memory allocation required
現(xiàn)在, std::string_view 無需復(fù)制字符串?dāng)?shù)據(jù)的優(yōu)點就更加明顯了(std::string不進(jìn)行短字符串優(yōu)化的情況下),下面的代碼就是例證.
#include <cassert> #include <iostream> #include <string> #include <string_view>void* operator new(std::size_t count) {std::cout << " " << count << " bytes" << std::endl;return malloc(count); }void getString(const std::string& str) {}void getStringView(std::string_view strView) {}int main() {std::cout << std::endl;std::cout << "std::string" << std::endl;std::string large = "0123456789-123456789-123456789-123456789";std::string substr = large.substr(10);std::cout << std::endl;std::cout << "std::string_view" << std::endl;std::string_view largeStringView{ large.c_str(), large.size() };largeStringView.remove_prefix(10);assert(substr == largeStringView);std::cout << std::endl;std::cout << "getString" << std::endl;getString(large);getString("0123456789-123456789-123456789-123456789");const char message[] = "0123456789-123456789-123456789-123456789";getString(message);std::cout << std::endl;std::cout << "getStringView" << std::endl;getStringView(large);getStringView("0123456789-123456789-123456789-123456789");getStringView(message);std::cout << std::endl;return 0; }代碼22行,23行,39行,41行因為創(chuàng)建了 string 對象 所以會分配(堆)內(nèi)存,但是代碼29行,30行,47行,48行,49行也相應(yīng)的創(chuàng)建了 string_view 對象,但是并沒有發(fā)生(堆)內(nèi)存分配!
這個結(jié)果令人印象深刻,(堆)內(nèi)存分配是一個非常耗時的操作,盡量的避免(堆)內(nèi)存分配會給程序帶來很大的性能提升,使用 string_view 能提升程序效率的原因也正是在此,當(dāng)你需要創(chuàng)建很多 string 的子字符串時, string_view 帶來的效率提升將更加明顯.
O(n) versus O(1)
std::string 和 std::string_view 都有 substr 方法, std::string 的 substr 方法返回的是字符串的子串,而 std::string_view 的 substr 返回的則是字符串子串的"視圖".聽上去似乎兩個方法功能上比較相似,但他們之間有一個非常大的差別: std::string::substr 是線性復(fù)雜度, std::string_view::substr 則是常數(shù)復(fù)雜度.這意味著 std::string::substr 方法的性能取決于字符串的長度,而std::string_view::substr 的性能并不受字符串長度的影響.
讓我們來做一個簡單的性能對比:
#include <chrono> #include <fstream> #include <iostream> #include <random> #include <sstream> #include <string> #include <vector>#include <string_view>static const int count = 30; static const int access = 10000000;int main() {std::cout << std::endl;std::ifstream inFile("grimm.txt");std::stringstream strStream;strStream << inFile.rdbuf();std::string grimmsTales = strStream.str();size_t size = grimmsTales.size();std::cout << "Grimms' Fairy Tales size: " << size << std::endl;std::cout << std::endl;// random valuesstd::random_device seed;std::mt19937 engine(seed());std::uniform_int_distribution<> uniformDist(0, size - count - 2);std::vector<int> randValues;for (auto i = 0; i < access; ++i) randValues.push_back(uniformDist(engine));auto start = std::chrono::steady_clock::now();for (auto i = 0; i < access; ++i) {grimmsTales.substr(randValues[i], count);}std::chrono::duration<double> durString = std::chrono::steady_clock::now() - start;std::cout << "std::string::substr: " << durString.count() << " seconds" << std::endl;std::string_view grimmsTalesView{ grimmsTales.c_str(), size };start = std::chrono::steady_clock::now();for (auto i = 0; i < access; ++i) {grimmsTalesView.substr(randValues[i], count);}std::chrono::duration<double> durStringView = std::chrono::steady_clock::now() - start;std::cout << "std::string_view::substr: " << durStringView.count() << " seconds" << std::endl;std::cout << std::endl;std::cout << "durString.count()/durStringView.count(): " << durString.count() / durStringView.count() << std::endl;std::cout << std::endl;return 0; }展示程序結(jié)果之前,讓我先來簡單描述一下:測試代碼的主要思路就是讀取一個大文件的內(nèi)容并保存為一個 string ,然后分別使用 std::string 和 std::string_view 的 substr 方法創(chuàng)建很多子字符串.我很好奇這些子字符串的創(chuàng)建過程需要花費多少時間.
我使用了<格林童話>作為程序的讀取文件.代碼中的 grimmTales(第22行) 存儲了文件的內(nèi)容.代碼34行中我向 std::vector 填充了 10000000 個范圍為[0, size - count - 2]的隨機(jī)數(shù)字.接著就開始了正式的性能測試.代碼37行到40行我使用 std::string::substr 創(chuàng)建了很多長度為30的子字符串,之所以設(shè)置長度為30,是為了規(guī)避 std::string 的短字符串優(yōu)化.代碼46行到49行使用 std::string_view::substr 做了相同的工作(創(chuàng)建子字符串).
程序的輸出如下,結(jié)果中包含了文件的長度, std::string::substr 所花費的時間, std::string_view::substr 所花費的時間以及他們之間的比例.我使用的編譯器是 GCC 6.3.0.
Size 30
沒有開啟編譯器優(yōu)化的結(jié)果:
開啟編譯器優(yōu)化的結(jié)果:
編譯器的優(yōu)化對于 std::string::substr 的性能提升并沒有多大作用,但是對于 std::string_view::substr 的性能提升則效果明顯.而 std::string_view::substr 的效率幾乎是 std::string::substr 的 45 倍!
Different sizes
那么如果我們改變子字符串的長度,上面的測試代碼又會有怎樣的表現(xiàn)呢?當(dāng)然,相關(guān)測試我都開啟了編譯器優(yōu)化,并且相關(guān)的數(shù)字我都做了3位小數(shù)的四舍五入.
對于上面的結(jié)果我并不感到驚訝,這些數(shù)字正好反應(yīng)了 std::string::substr 和 std::string_view::substr 的算法復(fù)雜度. std::string::substr 是線性復(fù)雜度(依賴于字符串長度), std::string_view::substr 則是常數(shù)復(fù)雜度(不依賴于字符串長度).最后的結(jié)論就是: std::string_view::substr 的性能要大幅優(yōu)于 std::string::substr.
總結(jié)
以上是生活随笔為你收集整理的[译]C++17,使用 string_view 来避免复制的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 获取本地视频文件的缩略图
- 下一篇: 关于软件开发中遇到的问题解决思路