C++11在時空效能方面的改進
這篇我們聊聊C++11在時間和空間上的改進點;
主要包括以下方面:
新增的高效容器:array、forward_list以及unordered containers;
以及常量表示式、靜態斷言和move語義;
大小固定容器 array
std::array是一個支援隨機訪問且大小(size)固定的容器,它是c++11中新增的容器。它有如下特點:
- 不預留多餘空間,只分配必須空間(譯註:size() == capacity())。
- 可以使用初始化表(initializer list)的方式進行初始化。
- 儲存了自己的size資訊。
- 不支援隱式指標型別轉換。
可以認為它是一個很不錯的內建陣列型別。示例:
array<int,6> a = { 1, 2, 3 }; a[3]=4; int x = a[5]; // array的預設資料元素為0,所以x的值變成0 int* p1 = a; // 錯誤: std::array不能隱式地轉換為指標 int* p2 = a.data(); // 正確,data()得到指向第一個元素的指標
可以認為array是一個緊縮版的vector,它比vector高效(沒有自動空間分配),但缺少了push_back這樣的神器,使得它的使用場景一般是用來替換c++內建的陣列型別,而不是vector;
前向列表 forward_list
c++11新增的容器:前向列表 forward_list
前向列表是一個能夠在任意位置快速插入和刪除的容器(列表都這特性,前向列表當然也具有這特性),但不支援快速隨機存取。
它是用單向連結串列實現的,相比較於它的C實現而言沒有什麼額外開銷。相較於std::list而言,此容器耗費的空間更少,因為它是單向的,不是雙向的。
std::forward_list<int> mylist (3,5); // 3 ints with value 5 for (int& x : mylist ) std::cout << " " << x;
雜湊表[無序容器] unordered containers
hash容器在很多之前的編譯器中就包含進來了;比如gcc 較早的版本中,它存在於tr1名稱空間中;
以unordered_map為例,unordered_map基於雜湊表實現,元素之間無序儲存;
而map基於紅黑樹實現,元素之間有序(通過operator< 進行比較);
hash版本的查詢時間複雜度為O(1),在資料量很大時,比紅黑樹的版本效率高很多;
對比在C++11中和之前使用上的區別:
// c++0x中: #include <tr1/unordered_map> std::tr1:: unordered_map< char,int > map1; map1.insert(std::pair<char,int>('a',100) ); // C++11中: #include <unordered_map> std::unordered_map< char,int > map1; map1.insert(std::pair<char,int>('a',100) );
常量表示式 constexpr
編譯期計算(Compile-time evaluation):常量表示式
在程式中,有些計算是與執行時無關的;每次執行都是相同的結果;
常量表示式允許讓這些計算髮生在編譯時,而不是在執行時;
這樣利用編譯時的計算能力,將顯著提升程式執行時的效果;
eg:對函式申明為constexpr
constexpr int multiply (int x, int y) { return x * y; } // 將在編譯時計算 const int val = multiply( 10, 10 ); cin >> x; // 由於輸入引數x只有在執行時確定,所以以下這個不會在編譯時計算,但執行沒問題 const int val2 = mutliply(x,x);
靜態斷言 static_assert
static_assert提供一個編譯時的斷言檢查。如果斷言為真,什麼也不會發生。如果斷言為假,編譯器會列印一個特殊的錯誤資訊。由於是在編譯期間執行的,所以它不會影響執行時的效能;
expression在編譯期進行求值,當結果為false(即:斷言失敗)時,將string作為錯誤訊息輸出。例如:
static_assert(sizeof(long) >= 8, “64-bit code generation required for this library.”); struct S { X m1; Y m2; }; static_assert(sizeof(S)==sizeof(X)+sizeof(Y), ”unexpected padding in S”);
static_assert在判斷程式碼的編譯環境方面十分有用,比如判斷當前編譯環境是否64位。但需要注意的是,由於static_assert在編譯期進行求值,它不能對那些依賴於執行期計算的值的進行檢驗。例如:
int f(int* p, int n) { //錯誤:表示式“p == 0”不是一個常量表示式 static_assert(p == 0, “p is not null”); }
正確的做法是在執行期進行判斷,假如條件不成立則丟擲異常;
下面這段程式碼原本期望只做用於整數型別。
template <typename T1, typename T2> auto add(T1 t1, T2 t2) -> decltype(t1 + t2) { return t1 + t2; }
但是如果有人寫出如下程式碼,編譯器並不會報錯
std::cout << add(1, 3.14) << std::endl; std::cout << add("one", 2) << std::endl;
程式會列印出4.14和”e”。但是如果我們加上編譯時斷言,那麼以上兩行將產生編譯錯誤。
template <typename T1, typename T2> auto add(T1 t1, T2 t2) -> decltype(t1 + t2) { static_assert(std::is_integral<T1>::value, "Type T1 must be integral"); static_assert(std::is_integral<T2>::value, "Type T2 must be integral"); return t1 + t2; } error C2338: Type T2 must be integral see reference to function template instantiation 'T2 add<int,double>(T1,T2)' being compiled with [ T2=double, T1=int ] error C2338: Type T1 must be integral see reference to function template instantiation 'T1 add<const char*,int>(T1,T2)' being compiled with [ T1=const char *, T2=int ]
move語義和右值引用
move語義和右值介紹
左值就是一個有名字的物件,而右值則是一個無名物件(臨時物件)。move語義允許修改右值(以前右值被看作是不可修改的,等同於const T&型別)。
void incr(int& a) { ++a; } int i = 0; incr(i); // i變為1 //錯誤:0不是一個左值 incr(0); // 0不是左值,無法直接繫結到非const引用:int&。 // 假如可行,那麼在呼叫時,將會產生一個值為0的臨時變數, // 用於繫結到int&中,但這個臨時變數將在函式返回時被銷燬, // 因而,對於它的任何更改都是沒有意義的, // 所以編譯器拒絕將臨時變數繫結到非const引用,但對於const的引用, // 則是可行的 ”&&”表示“右值引用”。右值引用可以繫結到右值(但不能繫結到左值): X a; X f(); X& r1 = a; // 將r1繫結到a(一個左值) X& r2 = f(); // 錯誤:f()的返回值是右值,無法繫結 X&& rr1 = f(); // OK:將rr1繫結到臨時變數 X&& rr2 = a; // 錯誤:不能將右值引用rr2繫結到左值a
考慮如下函式:
template<class T> swap(T& a, T& b) // 老式的swap函式 { T tmp(a);// 現在有兩份"a" a = b; // 現在有兩份"b" b = tmp; // 現在有兩份tmp(值同a) }
如果T是一個拷貝代價相當高昂的型別,例如string和vector,那麼上述swap()操作也將煞費氣力;我們的初衷其實並不是為了把這些變數拷來拷去,我是僅僅想將變數a,b,tmp的值做一個“移動”(即通過tmp來交換a,b的值)。
移動賦值操作背後的思想是,“賦值”不一定要通過“拷貝”來做,還可以通過把源物件簡單地“偷換”給目標物件來實現。例如對於表示式s1=s2,我們可以不從s2逐字拷貝,而是直接讓s1“侵佔”s2內部的資料儲存;
我們可以通過move()操作符來實現源物件的“移動”:
template <class T> void swap(T& a, T& b) //“完美swap”(大多數情況下) { T tmp = move(a); // 變數a現在失效(譯註:內部資料被move到tmp中了) a = move(b); // 變數b現在失效(譯註:內部資料被move到a中了,變數a現在“滿血復活”了) b = move(tmp); // 變數tmp現在失效(譯註:內部資料被move到b中了,變數b現在“滿血復活”了) }
move(x) 意味著“你可以把x當做一個右值”,把move()改名為rval()或許會更好,但是事到如今,move()已經使用很多年了。在C++11中,move()模板函式以及右值引用被正式引入。
將拷貝改進成移動操作,減少建立不必要的物件,節省了物件的空間分配消耗和構造析構的呼叫;
move對演算法中的改進
基於move的std::sort()和std::set::insert()要比基於copy的對應版本快15倍以上。不過它對標準庫中已有操作的效能改善不多,因為它們的實現中已經使用了類似的方法進行優化了(例如string,vector使用了調優過的swap操作來代替copy了)。當然如果你自己的程式碼中包含了move操作的話,就能自動從新標準庫中獲益了。
move對容器的改進
在C++11的標準庫中,所有的容器都提供了移動建構函式和移動賦值操作符,那些插入新元素的操作,如insert()和push_back(), 也都有了可以接受右值引用的版本。最終的結果是,在沒有使用者干預的情況下,標準容器和演算法的效能都提升了,而這些都應歸功於拷貝操作的減少。
以vector為例,定義“移動建構函式(move constructors)”和“移動賦值操作符(move assignments”來“移動”而非複製它們的引數:
template<class T> class vector { // … vector(const vector&); // 拷貝建構函式 vector(vector&&); // 移動建構函式 vector& operator= (const vector&); // 拷貝賦值函式 vector& operator =(vector&&); // 移動賦值函式 }; //注意:移動建構函式和移動賦值操作符接受 // 非const的右值引用引數,而且通常會對傳入的右值引用引數作修改
容器新增了move版的構造和賦值函式後,它最重要的內涵就是允許我們高效的從函式中返回一個容器:
vector<int> make_random(int n) { vector<int> ref(n); // 產生0-255之間的隨機數 for(auto x& : ref) x = rand_int(0,255); return ref; } vector<int> v = make_random(10000); for (auto x : make_random(1000000)) cout << x << '\n';
上邊程式碼的關鍵點是vector沒有被拷貝操作(vector ref的記憶體空間不會在函式返回時被stack自動回收了,move assignment通過右值引用精巧的搞定了這個問題)。對比我們現在的兩種慣用法:在自由儲存區來分配vector的空間,我們得負擔上記憶體管理的問題了;通過引數傳進已經分配好空間的vector,我們得要寫不太美觀的程式碼了。
原地安置操作 Emplace operations
在大多數情況下,push_back()使用移動建構函式(而不是拷貝建構函式)來保證它更有效率,不過在極端情況下我們可以走的更遠。為何一定要進行拷貝/移動操作?為什麼不能在vector中分配好空間,然後直接在這個空間上構造我們需要的物件呢?做這種事兒的操作被叫做”原地安置”(emplace,含義是:putting in place)。
舉一個emplace_back()的例子:
vector<pair<string,int>> vp; string s; int i; while(cin>>s>>i) vp.emplace_back(s,i);
emplace_back()接受了可變引數模板變數並通過它來構造所需型別。至於emplace_back()是否比push_back()更有效率,取決於它和可變引數模板的具體實現。如果你認為這是一個重要的問題,那就實際測試一下。否則,就從美感上來選擇它們吧。
參考
http://www.stroustrup.com/C++11FAQ.html
https://www.chenlq.net/books/cpp11-faq
相關文章
- TestComplete 8 在Web測試方面的改進Web
- IBM Rational CM Server 在 WAN 效能、可靠性以及可伸縮性方面的功能改進IBMServer
- CRM管理系統改進企業在營銷方面的弊端
- MySQL 5.7 新特性 共享臨時表空間及臨時表改進MySql
- Chrome渲染管道的效能改進Chrome
- 【翻譯】.NET 5中的效能改進
- ASP.NET Core 6 的效能改進ASP.NET
- 【譯】.NET 7 中的效能改進(五)
- 【譯】.NET 7 中的效能改進(六)
- 【譯】.NET 7 中的效能改進(一)
- 【譯】.NET 7 中的效能改進(二)
- 【譯】.NET 7 中的效能改進(七)
- 【譯】.NET 7 中的效能改進(八)
- 【譯】.NET 7 中的效能改進(九)
- 【譯】.NET 7 中的效能改進(十)
- 【譯】.NET 7 中的效能改進(三)
- 【譯】.NET 7 中的效能改進(四)
- 【譯】.NET 7 中的效能改進(十一)
- 【譯】.NET 7 中的效能改進(十二)
- 【譯】.NET 7 中的效能改進(十三)
- MySQL5.6.12的Innodb效能改進MySql
- 臨時表空間的增刪改查
- GNOME 3.36 釋出,對視覺和效能進行了改進視覺
- Mqttnet記憶體與效能改進錄MQQT記憶體
- Strom及DRPC效能測試與改進RPC
- 使用 ProxySQL 改進 MySQL SSL 的連線效能MySql
- oracle效能改進方法論告訴我們!Oracle
- 如何改變win7登陸時介面的圖片Win7
- oracle 臨時表空間的增刪改查Oracle
- ORACLE 臨時表空間的增刪改查:Oracle
- oracle臨時表空間的增刪改查Oracle
- Django中重定向頁面的時候使用名稱空間Django
- 效能改進之專案例會匯入實踐
- [譯] 使用 Kotlin 協程改進應用效能Kotlin
- 【譯】ASP.NET Core 6 中的效能改進ASP.NET
- 改進資料庫效能-SQL查詢優化資料庫SQL優化
- C++11獲取時間C++
- 關於Redis命令keys在效能方面的說明Redis