详解C++17下的string_view
看到一個(gè)介紹 C++17 的系列博文(原文),有十來篇的樣子,覺得挺好,看看有時(shí)間能不能都簡單翻譯一下,這是第五篇~
當(dāng)字符串?dāng)?shù)據(jù)的所有權(quán)已經(jīng)確定(譬如由某個(gè)string對(duì)象持有),并且你只想訪問(而不修改)他們時(shí),使用 std::string_view 可以避免字符串?dāng)?shù)據(jù)的復(fù)制,從而提高程序效率,這(指程序效率)也是這篇文章的主要內(nèi)容.
這次要介紹的 string_view 是 C++17 的一個(gè)主要特性.
我假設(shè)你已經(jīng)了解了一些 std::string_view 的知識(shí),如果沒有,可以看看我之前的這篇文章.C++ 中的 string 類型在堆上存放自己的字符串?dāng)?shù)據(jù),所以當(dāng)你處理 string 類型的時(shí)候,很容易就會(huì)產(chǎn)生(堆)內(nèi)存分配.
Small string optimisation
我們先看下以下的示例代碼:
#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) {}int main() {std::cout << std::endl;std::cout << "std::string" << std::endl;// 這里會(huì)調(diào)用上面的new函數(shù),輸出16字節(jié)(根據(jù)編譯器的不同,值可能不同,下同)std::string small = "0123456789"; small = "12345678"; // 這里不會(huì)再調(diào)用上面的new函數(shù),因?yàn)殚L度小于16字節(jié)// 因?yàn)樽址L度大于16了,這里會(huì)再調(diào)用上面的new函數(shù),輸出32字節(jié)small = "1234567890123456789000066666";/* 這里會(huì)調(diào)用上面的new函數(shù),第一次輸出16字節(jié)(默認(rèn)為substr分配16字節(jié)內(nèi)存)第二次是發(fā)現(xiàn)為small.substr(5)長度大于16,所以重新為substr分配內(nèi)存以便能容納下它,輸出32*/std::string substr = small.substr(5);std::cout << " " << substr << std::endl;std::cout << std::endl;std::cout << "getString" << std::endl;getString(small);// 這里不會(huì)調(diào)用上面的new函數(shù),因?yàn)閟mall長度沒有變得更長getString("0123456789"); // 這里會(huì)調(diào)用上面的new函數(shù),因?yàn)殚L度小于16,所以輸出16const char message[] = "0123456789";getString(message); // 這里會(huì)調(diào)用上面的new函數(shù),因?yàn)殚L度小于16,所以輸出16std::cout << std::endl;system("pause()");return 0; }代碼第6到第10行,我重載了全局的 new 操作符,這樣我就能跟蹤(堆)內(nèi)存的分配了,而后,代碼分別在第23、31、39、41行創(chuàng)建了string對(duì)象,所以這幾處代碼都會(huì)產(chǎn)生(堆)內(nèi)存分配.相關(guān)的程序輸出如下:
咦, 紅色方框中的32字節(jié)是怎么產(chǎn)生的?這是怎么回事?其實(shí) string 類型只有在字符串超過指定大小(具體實(shí)現(xiàn)相關(guān))時(shí)才會(huì)申請(qǐng)(堆)內(nèi)存,對(duì)于 MSVC 來說,指定大小為 15, 對(duì)于 GCC 和 Clang,這個(gè)值則為 23.關(guān)于string的內(nèi)存配置機(jī)制,當(dāng)超過指定編譯器下string內(nèi)存配置的長度會(huì)重新分配內(nèi)存,所以這里重新分配內(nèi)存了,從而輸出紅色方框的32字。string內(nèi)存分配機(jī)制具體請(qǐng)參見《STL庫中string類內(nèi)存布局的探究》。
這也就意味著,較短的字符串?dāng)?shù)據(jù)是直接存儲(chǔ)于 string 的對(duì)象內(nèi)存中的,不需要分配(堆)內(nèi)存.
從現(xiàn)在開始,示例代碼中的字符串將擁有至少30個(gè)字符,這樣我們就不需要關(guān)注短字符串優(yōu)化了.好了,帶著這個(gè)前提(字符串長度>=30個(gè)字符),讓我們重新開始講解.
No memory allocation required
現(xiàn)在, std::string_view 無需復(fù)制字符串?dāng)?shù)據(jù)的優(yōu)點(diǎn)就更加明顯了(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行因?yàn)閯?chuàng)建了 string 對(duì)象 所以會(huì)分配(堆)內(nèi)存,但是代碼29行,30行,47行,48行,49行也相應(yīng)的創(chuàng)建了 string_view 對(duì)象,但是并沒有發(fā)生(堆)內(nèi)存分配!輸出結(jié)果如下:
上圖輸出結(jié)果中紅色框都表示指定的字符串變量因?yàn)殚L度不夠(大于編譯器默認(rèn)為該字符串分配的內(nèi)存長度),重新發(fā)生了內(nèi)存分配,上圖中的16是定義字符串時(shí),編譯器默認(rèn)為該字符串分配的內(nèi)存空間。
這個(gè)結(jié)果令人印象深刻,(堆)內(nèi)存分配是一個(gè)非常耗時(shí)的操作,盡量的避免(堆)內(nèi)存分配會(huì)給程序帶來很大的性能提升,使用 string_view 能提升程序效率的原因也正是在此,當(dāng)你需要?jiǎng)?chuàng)建很多 string 的子字符串時(shí), string_view 帶來的效率提升將更加明顯.
O(n) versus O(1)
std::string 和 std::string_view 都有 substr 方法, std::string 的 substr 方法返回的是字符串的子串,而 std::string_view 的 substr 返回的則是字符串子串的"視圖".聽上去似乎兩個(gè)方法功能上比較相似,但他們之間有一個(gè)非常大的差別:?std::string::substr 是線性復(fù)雜度, std::string_view::substr 則是常數(shù)復(fù)雜度.這意味著 std::string::substr 方法的性能取決于字符串的長度,而std::string_view::substr 的性能并不受字符串長度的影響.
讓我們來做一個(gè)簡單的性能對(duì)比:
#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é)果之前,讓我先來簡單描述一下:測試代碼的主要思路就是讀取一個(gè)大文件的內(nèi)容并保存為一個(gè) string ,然后分別使用 std::string 和 std::string_view 的 substr 方法創(chuàng)建很多子字符串.我很好奇這些子字符串的創(chuàng)建過程需要花費(fèi)多少時(shí)間.
我使用了<格林童話>作為程序的讀取文件.代碼中的 grimmTales(第22行) 存儲(chǔ)了文件的內(nèi)容.代碼34行中我向 std::vector 填充了 10000000 個(gè)范圍為[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 所花費(fèi)的時(shí)間, std::string_view::substr 所花費(fèi)的時(shí)間以及他們之間的比例.我使用的編譯器是 GCC 6.3.0.
Size 30
沒有開啟編譯器優(yōu)化的結(jié)果:
開啟編譯器優(yōu)化的結(jié)果:
編譯器的優(yōu)化對(duì)于 std::string::substr 的性能提升并沒有多大作用,但是對(duì)于 std::string_view::substr 的性能提升則效果明顯.而 std::string_view::substr 的效率幾乎是 std::string::substr 的 45 倍!
Different sizes
那么如果我們改變子字符串的長度,上面的測試代碼又會(huì)有怎樣的表現(xiàn)呢?當(dāng)然,相關(guān)測試我都開啟了編譯器優(yōu)化,并且相關(guān)的數(shù)字我都做了3位小數(shù)的四舍五入.
對(duì)于上面的結(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)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一加 Ace 2V 手机支持红外遥控和全
- 下一篇: 拳皇97ol八神技能怎么连招?八神连招教