一馁菜、內存問題
1川尖、堆和棧
首先需要確定的是這里堆和棧不是數(shù)據結構中堆和棧的概念隧魄。
- 相同點:
都分別叫heap stack 翻譯完全一樣筋岛。 - 不同點
數(shù)據結構中的堆娶视,大部分在堆排序這一部分的內容中,堆指的是數(shù)據放在一起的直觀概念睁宰。 棧的數(shù)據結構主要就是一個先入后出的概念肪获,在解決很多問題時候的基礎數(shù)據結構定義。
但是在OS內存的堆存儲區(qū)和棧存儲區(qū)的概念則有不同柒傻。
操作系統(tǒng)啟動后孝赫,給每一個啟動后的應用分配一個虛擬存儲區(qū),由系統(tǒng)將虛擬存儲區(qū)的內存地址映射到物理地址(具體映射方式和內存管理方式詳細參見操作系統(tǒng)的基礎原理)红符。一個應用程序的操作系統(tǒng)內存分配分為以下幾個部分:
1青柄、棧區(qū)(stack) — 由編譯環(huán)境決定 ,存放函數(shù)的參數(shù)值预侯,局部變量的值等致开。其操作方式類似于數(shù)據結構中的棧。其內存的分配由系統(tǒng)決定萎馅。
2双戳、堆區(qū)(heap) — 一般由應用程序的編寫者來管理分配和釋放, 若應用本身不釋放糜芳,這中間可能會造成內存泄漏等飒货,程序結束時可能由OS回收 千诬。
3、全局區(qū)(靜態(tài)區(qū))(static)—膏斤,全局變量和靜態(tài)變量的存儲是放在一塊的徐绑,初始化的全局變量和靜態(tài)變量在一塊區(qū)域, 未初始化的全局變量和未初始化的靜態(tài)變量在相鄰的另一塊區(qū)域莫辨。 應用程序結束后OS釋放傲茄。
4、字符串常量區(qū) — 常量字符串就是放在這里的沮榜。 應用程序結束后OS釋放盘榨。
5、程序代碼區(qū) — 存放函數(shù)體的二進制代碼蟆融。
(如果要應對這其中的問題草巡,對于const 控制的參數(shù),形參等型酥,具體參見后文的const山憨,static用法等)
根據以上概念,棧是由系統(tǒng)分配好的一塊連續(xù)內存存儲區(qū)域弥喉,(Windows上一般為2M大小郁竟,Linux上由于芯片架構的差異,略有不同 具體可以用ulimit -a查詢所有的內存由境,其中ulimit -s可以查到棧內存區(qū)的大小)棚亩;而堆的話一般就是通過malloc(C語言),new(C++語言)來調用的虏杰,而malloc分配內存需要一整套比較復雜點的算法讥蟆,而在棧上分配內存就簡單方便的的多,直接在已經固定的內存區(qū)域取用即可纺阔,其分配管理方式就是基于棧的管理方式瘸彤,并且還有專門的寄存器來管理棧頂位置,這個在效率上要比在堆上效率要好的多州弟。其中malloc在堆內存區(qū)分配的時候的算法钧栖,以及可能帶來的問題參見下文)低零。
2婆翔、new/delete,malloc/free 的問題說明
- 重點說說new 和 malloc的事情
首先所有的內存分配都可以用malloc來分配掏婶,new是專屬C++啃奴,具體怎么實現(xiàn),可以參考重載后的代碼實現(xiàn)部分雄妥,可以選擇是否使用malloc來實現(xiàn)最蕾,new可以有返回類型依溯,由編譯器來計算分配內存的大小,可以用new[] 來分配分好組的內存瘟则±杪可以被重載實現(xiàn),異常(比如分配不到內存等情況)有明確的返回醋拧。 malloc就是一件事:根據傳入的參數(shù)從堆內存區(qū)中分配好內存并返回分配好后的指針慷嗜,如果沒成功則返回NULL。 - malloc內存分配算法:
todo
3丹壕、 內存泄漏以及智能指針的問題
內存泄漏就是已經分配的內存庆械,不再有被使用的可能直到該程序結束后被OS回收。
在C++中菌赖,一般就是由于各種復雜邏輯導致的new/delete, malloc/free沒有成對出現(xiàn)或者有些情況下缭乘,程序異常退出某個循環(huán)結構后沒有正常free or delete。
內存泄漏問題的解決:
內存泄漏檢查工具
valgrind : Linux環(huán)境下: valgrid內存泄漏檢查工具琉用。
mtrace : GNU擴展, 用來跟蹤malloc, mtrace為內存分配函數(shù)(malloc, realloc, memalign, free)安裝hook函數(shù)堕绩。
dmalloc :用于檢查C/C++內存泄露的工具。
mpatrol 邑时,逛尚,* 一個跨平臺的 C++ 內存泄漏檢測器
memwatch :和dmalloc一樣,它能檢測未釋放的內存刁愿、同一內存被釋放多次绰寞、位址存取錯誤等
智能指針
早期C++開始,用
auto_ptr(new int)
來定定義指針铣口,這樣就不需要手動寫刪除的代碼了滤钱。
到C++11開始后,C++11標準中改用unique_ptr脑题、shared_ptr及weak_ptr等智能指針來自動回收堆分配的對象件缸。這里我們可以看一個C++11中使用新的智能指針的簡單例子。
unique_ptr<int>up1(new int(11)); //無法復制的unique_ptr
unique_ptr<int> up2=up1; //不能通過編譯
cout<<*up1<<endl; //11
unique_ptr<int>up3=move(up1); //現(xiàn)在p3是數(shù)據唯一的unique_ptr智能指針
cout<<*up3<<endl; //11
cout<<*up1<<endl; //運行時錯誤
up3.reset(); //顯式釋放內存
up1.reset(); //不會導致運行時錯誤
cout<<*up3<<endl; //運行時錯誤
shared_ptr<int>sp1(new int(22));
shared_ptr<int>sp2=sp1;
cout<<*sp1<<endl; //22
cout<<*sp2<<endl; //22
sp1.reset();
cout<<*sp2<<endl; //22
unique_ptr, shared_ptr, weak_ptr, make_unique
當智能指針遇到右值引用叔遂?他炊?
可以先看現(xiàn)代C++部分后再返回此處。已艰?痊末??哩掺?
關于指針和引用 std::function std::bind //todo
二凿叠、面向對象的問題
1、內存模型
C++編譯器自動生成的函數(shù)
在C++98編譯器中
class A {}
編譯器給生成的代碼如下:
class A {
public:
A(); // 構造函數(shù)
A(const A& a); // 拷貝構造函數(shù)
virtual ~A(); // 析構函數(shù),此處應該是virtual
A& operator=(const A& a); // 拷貝賦值函數(shù)
}
C++11編譯器之后盒件, 首先有了右值引用的加入蹬碧,默認生成的函數(shù)增加了 移動構造函數(shù)和移動拷貝函數(shù) ,此時代碼生成如下:
class A {
public:
A(); // 構造函數(shù)
A(const A& a); // 拷貝構造函數(shù)
A(A&& a); // 移動構造函數(shù)炒刁,此處右值引用
virtual ~A(); // 析構函數(shù)恩沽,此處應該是virtual
A& operator=(const A& a); // 拷貝賦值函數(shù)
A& operator=( A&& a); // 移動賦值函數(shù)
}
= delete 和 = default 兩個工具
= default
如上所述,如果默認的構造函數(shù)A() 其中有針對其中成員變量進行初始化默認值的話翔始,就一定需要自己寫下這部分代碼飒筑,而不能依賴編譯器自動生成;但是編譯器自動生成的函數(shù)比手動編寫的執(zhí)行效率要高绽昏,在此之前协屡,似乎沒有找到可以更高效執(zhí)行自己手寫函數(shù)的辦法,在C++11之后全谤,加上 = default的可以讓手寫的這部分代碼獲得同等于自動生成代碼的執(zhí)行效率肤晓。= delete
使得編譯器在生成這段代碼的時候,如果有定義 = delete 就默認禁用該函數(shù)认然。
用處补憾,例如
int add(int m, int n) = delete;
float add(float m, float n);
此時int類型就不能作用add函數(shù)的輸入參數(shù)。
手動實現(xiàn) class String的例子卷员,例子中對于構造函數(shù)盈匾,拷貝構造函數(shù),移動構造函數(shù)(C++11新增)賦值函數(shù)毕骡,析構函數(shù)(本例子中沒考慮base類的情況削饵,如果是base類的情況,需要是virtual函數(shù))未巫,重要相關的均已注釋窿撬。
class String {
public:
// 構造函數(shù)
String(char *str = NULL){
//空構造也會創(chuàng)建一個長度為1的字符串數(shù)組
if(str == NULL){
m_data = new char[1];
m_data[0] = '\0';
m_size = 0;
}
else{
m_size = strlen(str);
m_data = new char[m_size + 1];
strcpy(m_data, str);
}
}
//拷貝構造函數(shù)
String(const String &str){
m_size = str.m_size;
m_data = new char[m_size + 1];
strcpy(m_data, str.m_data);
}
//移動構造函數(shù), 此處右值引用
String(String&& str) noexcept // noexcept 用處叙凡?
:m_size(str.m_size) {
cout<<"construct &&"<<endl;
str.m_size = 0;
m_data = str.m_data;
str.m_data = nullptr; // 移動之后原來的引用就廢止了
};
~String() {
delete [] m_data;
} ;
String& operator=(const String& a) {
if(this == &str) {
return *this;
}
delete [] m_data;
m_size = str.m_size;
m_data = new char[m_size + 1];
strcpy(m_data, str.m_data);
return *this;
}
String& operator=( String&& str) noexcept { // 移動賦值函數(shù)
if(this == &str) {
return *this;
}
delete [] m_data;
m_size = str.m_size;
m_data = str.m_data;
str.m_data = nullptr;
return *this;
}
private:
char * m_data;
int m_size;
}
其中的移動構造函數(shù)
也可以寫成這樣
// Move constructor.
String(String&& other) noexcept
: m_data(nullptr)
, m_size(0) {
cout<<"construct && std::move"<<endl;
*this = std::move(other);
}
虛函數(shù)的問題
面向對象虛函數(shù) 菱形繼承 todo
菱形繼承問題
面向對象虛函數(shù) 菱形繼承 todo
2劈伴、面向對象的基礎概念
繼承 組合問題 設計模式相關問題 todo
三、const 握爷、static
const的作用:
- 定義常量
const int constValue = 10; // 一般為定義一個常量
- 修飾變量
const int* p = 10; // 表示指針p指向的內容10 是常量违施,不可變
char * const p; // 就是將p聲明為常指針薄坏,它的地址不能改變
const double *const p // 這里指的是指針是常量择浊,指針指向的內容也是常量
- 修飾函數(shù)的形參
void fun(const char* ch); // 參數(shù)指針所指的內容為常量不可變
void fun(const int v); // 表示傳入的參數(shù)不可變范咨,但是由于是形參,此處const無意義
void fun(char* const p); // 指針本身不可變师抄,但是由于是形參漓柑,此處const無意義
void fun(const A& a); // 引用傳遞,同時也可以限制參數(shù)a 在函數(shù)體內不可改變叨吮,和形參不同之處在于 無需多一次拷貝
- 修飾返回值
const char *GetChar(void){}; // 表示該指針不能被改動辆布,只能把該指針賦給const修飾的同類型指針變量。
const char *ch=GetChar(); //此處const必不可少茶鉴,只能返回const修飾的指針
- 修飾函數(shù)體
class Ex
{
public:
int function(void) const ; // 這里限制了在function函數(shù)體內不可以修改成員變量锋玲,否則編譯階段報錯
}
static的幾個問題:
static的問題分類兩大類
static 修飾變量,面向過程中的問題
- 靜態(tài)全局變量
靜態(tài)全局變量的特點
靜態(tài)全局變量在全局區(qū)(靜態(tài)區(qū)涵叮,參考本文開頭第一部分)分配內存惭蹂。
靜態(tài)全局變量的可見范圍是本文件。
應用本身如果沒有初始化的話割粮,靜態(tài)變量會自動初始化為0盾碗。
static int n; // 編譯成功后,由于靜態(tài)全局變量不能再其他文件使用舀瓢,所以該變量不能超出該文件
void fn()
{
n++;
cout<<n<<endl;
}
int main() {
n = 10;
f(); // 編譯成功后廷雅,由于靜態(tài)全局變量不能在其他文件使用,所以該變量不能超出該文件
}
- 靜態(tài)局部變量
static修靜態(tài)局部變量有以下特點:
靜態(tài)局部變量在全局數(shù)據區(qū)分配內存京髓;
靜態(tài)局部變量在程序執(zhí)行到該對象的聲明處時被首次初始化航缀,即以后的函數(shù)調用不再進行初始化;
靜態(tài)局部變量一般在聲明處初始化堰怨,如果沒有顯式初始化芥玉,會被程序自動初始化為0;
靜態(tài)局部變量始終駐留在全局數(shù)據區(qū)备图,直到程序運行結束灿巧。但其作用域為局部作用域,當定義它的函數(shù)或語句塊結束時揽涮,其作用域隨之結束砸烦;
void fn()
{
static n=10;
// 由于靜態(tài)局部變量的存儲空間在全局數(shù)據分配,而普通局部變量在棧上分配绞吁,
// 所以即使本函數(shù)運行結束之后幢痘,n這個變量也一直保留,待到下次調用時可以繼續(xù)使用該值家破,
// 不需要額外的全局數(shù)據變量保存颜说,相比全局變量而言,這個更容易控制
cout<<n<<endl;
n++;
}
- 靜態(tài)函數(shù)
靜態(tài)函數(shù)的作用就是汰聋,該函數(shù)在本文件可見门粪。
static void fn(); // 聲明靜態(tài)函數(shù)
void fn() {
}
修飾類,在面向對象的問題
- 靜態(tài)成員變量
在面向對象的語境下烹困,靜態(tài)成員變量意味著該數(shù)據只和類發(fā)生關系玄妈,不屬于任何對象。
class A{
public:
void f() {
cout<<"static member data : "<<s<<endl;
}
private:
static int s;
};
int A::s = 10; // 初始化可以不必有static修飾
靜態(tài)成員變量在全局數(shù)據區(qū)域分配內存,本類的所有對象共享拟蜻,由于內存分配在全局的靜態(tài)區(qū)內绎签,所以他需要在初始化值,如果沒有會默認初始化一個0酝锅。靜態(tài)成員變量的存在和對象無關诡必,只要定義類就有該變量分配的內存。 其他成員變量初始化如果沒有指定搔扁,則是一個不可靠的值爸舒。
private、protected稿蹲、public 這方面的權限和其他成員變量一樣扭勉。
由于靜態(tài)變量內存分配在全局靜態(tài)區(qū),不屬于任何對象苛聘,所以涂炎,sizeof 運算符不會計算 靜態(tài)成員變量。
- 靜態(tài)成員函數(shù)
class A{
public:
void f() {
cout<<"static member data : "<<s<<endl;
}
static void sf() { // 如果分開寫焰盗,后續(xù)可不再寫static
}
private:
static int s;
};
int A::s = 10; // 初始化可以不必有static修飾
關于靜態(tài)成員函數(shù):
出現(xiàn)在類體外的函數(shù)定義不能指定關鍵字static璧尸;
因為靜態(tài)函數(shù)屬于類,所以靜態(tài)成員之間可以相互訪問熬拒,即靜態(tài)成員函數(shù)(僅)可以訪問靜態(tài)成員變量爷光、靜態(tài)成員函數(shù);靜態(tài)成員函數(shù)不能訪問屬于對象的非靜態(tài)成員函數(shù)和非靜態(tài)成員變量澎粟;
非靜態(tài)成員函數(shù)可以任意地訪問靜態(tài)成員函數(shù)和靜態(tài)數(shù)據成員蛀序;
沒有this指針,靜態(tài)成員函數(shù)與類的全局函數(shù)相比速度上會稍快活烙;
調用靜態(tài)成員函數(shù)徐裸,可用:
A a;
a.sf();
A::sf();
這兩種方式都可以調用靜態(tài)成員函數(shù)。
四啸盏、現(xiàn)代C++問題
C++ 11增加了不少現(xiàn)代特性重贺。
右值引用
現(xiàn)代C++11 以及之后的改進
- 在C++98中,有如下代碼:
class RightRefEx {
public:
RightRefEx()
:d(new int(0)){
cout<<"Construct:"<<++n_cstr<<endl;
}
RightRefEx(const RightRefEx &h)
:d(new int(*h.d)){
cout<<"Copyconstruct:"<<++n_cptr<<endl;
}
~RightRefEx() {
cout<<"Destruct:"<<++n_dstr<<endl;
}
int *d;
static int n_cstr;
static int n_dstr;
static int n_cptr;
};
int RightRefEx::n_cstr = 0;
int RightRefEx::n_dstr = 0;
int RightRefEx::n_cptr = 0;
RightRefEx GetTemp() {
return RightRefEx();
}
int main()
{
RightRefEx a = GetTemp();
}
使用g++ rightRef.cpp -fno-elide-constructors
編譯回懦。這里 -fno-elide-constructors
表示編譯器將采用RVO優(yōu)化气笙。也即是左值右值引用傳遞優(yōu)化。
一句RightRefEx a = GetTemp();
調用了一次默認構造函數(shù)怯晕,兩次拷貝構造函數(shù)潜圃,以及其中穿插了三次析構函數(shù)(具體結果可以自行運行并分析一下),實際項目中 一般不建議這么寫舟茶。如果其中有大量的深拷貝的話谭期,就會發(fā)生多次的內存分配 然后釋放的情況堵第。 這里也順便給上一節(jié)中的靜態(tài)成員變量的一個使用例子。
在C++11中可以避免這些情況的存在隧出,新的移動語義模型move semantics
踏志,可以把一個對象中的已分配的內存直接傳遞給新的對象使用,而不需要重新分配在釋放的過程鸳劳。
移動構造函數(shù)具體可參考上文中內存管理一節(jié)中 class String那個類的示例狰贯。
- 左值 右值 右值引用的概念
左值: 個有實際名字的也搓,可以取地址操作的 比如int a = b+c;
,&a
可以取地址赏廓,所以a是左值。
右值:上述中(b+c)
算是右值傍妒。
發(fā)展到C++11之后幔摸,右值的概念有了進一步的發(fā)揚光大。右值分為將亡值 (xvalue)和純右值(prvalue)兩層概念颤练,在上述RightRefEx的例子中既忆,多個拷貝構造函數(shù)以及getTemp()
這樣的函數(shù)返回時都構造了一些臨時對象,這些臨時對象可以理解為xvalue嗦玖,但是如果將這個臨時變量保存下來患雇,其引用就是右值引用,如果有右值引用的話宇挫,這個臨時變量將會一直存在苛吱,并且可以通過右值引用獲得這個臨時變量的值。比如上文中GetTemp()
函數(shù)如果可以返回右值引用器瘪,那么構建出來的臨時變量將會一直存在翠储,然后用右值引用傳遞出來,這個值就可以表達出來橡疼, 并通過 a 獲得值援所。
RightRefEx && a = GetTemp();
- std::move()的問題
std::move() 可以將左值強制轉換為右值。
所以上述GetTemp()
函數(shù)可以改為:
RightRefEx && GetTemp() {
return std::move(RightRefEx());
}
這時欣除,只需要一次構造函數(shù)調用和一次析構函數(shù)調用了住拭,減少了內存分配釋放的消耗。
<type_traits>里有不少可以判斷的類型历帚,//todo滔岳。
- 完美轉發(fā) 引用折疊
void f1(T t) {
f2(t);
}
如果T不是基礎數(shù)據類型,但部分情況我們都是用引用傳遞來傳參數(shù)抹缕,這樣的話澈蟆,減少了很多拷貝開銷。這里函數(shù)f1沒有問題卓研,但是對于f2的參數(shù)有一定的要求趴俘。 引用上引用折疊之后睹簇,可以這樣設計
void f1(T && t) {
f2(std::forward(t));
}
引用折疊todo。
類型推導
auto
decltype
int i;
decltype(i) j = 0; // i是int類型寥闪,推導出 decltype(i) 是int類型
cout << typeid(j).name() << endl; // 打印出"i", g++表示integer
float a;
double b;
decltype(a + b) c;
cout << typeid(c).name() << endl; // 打印出"d", g++表示double
decltype 判斷左值 右值問題時的規(guī)則太惠。
1)如果e是一個沒有帶括號的標記符表達式或者類成員訪問表達式,那么decltype(e)就是e所命名的實體的類型疲憋。
此外凿渊,如果e是一個被重載的函數(shù),則會導致編譯時錯誤缚柳。
2)否則埃脏,假設e的類型是T,如果e是一個將亡值(xvalue)秋忙,那么decltype(e)為T&&彩掐。
3)否則,假設e的類型是T灰追,如果e是一個左值堵幽,則decltype(e)為T&。
4)否則弹澎,假設e的類型是T朴下,則decltype(e)為T。
auto 和 decltype推導類型時苦蒿,遇到cv(const, volatile)的類型推導規(guī)則: auto不可以“繼承”殴胧, decltype可以“繼承”。
POD type_traits // todo
lambda表達式&仿函數(shù)
lambda函數(shù)
lambda的語法規(guī)則:
[capture] (parameters)mutable -> returntype {statement}
[capture]:捕捉列表刽肠。[]是lambda引出符溃肪。
(parameters):參數(shù)列表。與普通函數(shù)的參數(shù)列表一致音五。如果不需要參數(shù)傳遞惫撰,括號()可以一起省略。mutable:mutable修飾符躺涝。默認情況下厨钻,lambda函數(shù)是一個const函數(shù),mutable可以取消其常量性坚嗜。在使用該修飾符時夯膀,參數(shù)列表不可省略(即使參數(shù)為空)
->returntype:返回類型。用追蹤返回類型形式聲明函數(shù)的返回類型苍蔬。出于方便诱建,不需要返回值的時候也可以連同符號->一起省略。
{statement}:函數(shù)體碟绑。
極端情況下俺猿,C++11中最簡略的lambda函數(shù): []{};
int main(){
int a = 1;
int b = 2;
[=] { return a + b;}; // 省略了參數(shù)列表與返回類型茎匠,返回類型由編譯器推斷為int
auto fun1 = [&](int c) { b = a + c; }; // 省略了返回類型,無返回值
auto fun2 = [=, &b](int c)->int { return b += a + c; }; // 各部分都很完整
}
捕獲列表的意思:
·[var]表示值傳遞方式捕捉變量var诵冒。
·[=]表示值傳遞方式捕捉所有父作用域的變量(包括this)汽馋。
·[&var]表示引用傳遞捕捉變量var。
·[&]表示引用傳遞捕捉所有父作用域的變量(包括this)豹芯。
·[this]表示值傳遞方式捕捉當前的this指針药磺。
仿函數(shù)
- 仿函數(shù) : 類的operator()被重載告组,行為也是一個函數(shù)
class Price{
private:
float _rate;
public:
Price(float rate): _rate(rate){}
float operator()(float price) {
return price * (1 - _rate/100);
}
};
int main(){
float trate = 5.5f;
Price Hangi(tax_rate);
auto Changi2 =
[trate](float price)->float{ return price * (1 - tax_rate/100); };
float p1 = Hangi(3699); // 仿函數(shù)
float p2 = Changi2(2899); // lambda表達式
}
五便锨、 多線程問題
C++11多線程的實現(xiàn)方式
從C++11開始放案,C++開始在語言層面實現(xiàn)了多線程的吱殉,此前所有的多線程實現(xiàn)都極度依賴OS層面的實現(xiàn)稿湿。隨著技術的發(fā)展饺藤,線程的支持逐步開始從操作系統(tǒng)層面到芯片層面的支持涕俗。
1神帅、 future
- future 主要涉及到 promise 和 packaged_task兩個template找御,另外還有一個async凹联,蔽挠、比原、todo
2量窘、 mutex互斥信號量
- mutex 互斥信號量
static long long total = 0;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
void* func(void *) {
long long i;
for(i = 0; i < 100000LL;i++) {
pthread_mutex_lock(&m);
total += i;
pthread_mutex_unlock(&m);
}
}
int main() {
pthread_t t1, t2;
if (pthread_create(&t1, NULL, &func, NULL)){
throw;
}
if (pthread_create(&t2, NULL, &func, NULL)){
throw;
}
pthread_join(t1, NULL);
pthread_join(t2, NULL);
cout << total << endl;
return 0;
}
為了防止t1和t2競爭total這個資源而增加了一個mutex來控制訪問。
另外還可以用atomic來完成這個目的。
六审葬、原子類atomic
- atomic
atomic的定義
先看下面一段代碼
void exchange(int ) {
int a = 1;
int b = a;
}
gcc asmex.cpp -lstdc++ -std=c++11 -S -o asm.s
得到匯編文件,未做優(yōu)化官册,得到的代碼如下(去掉不相關代碼):
__Z8exchangei: ## @_Z8exchangei
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
movl %edi, -4(%rbp) ## 形參
movl $1, -8(%rbp) ## 給a賦值 1
movl -8(%rbp), %eax ## 通過eax寄存器把a的值傳遞給b
movl %eax, -12(%rbp)
popq %rbp
retq
.cfi_endproc
這里結構比較簡略。
源代碼改atomic之后:
void exchange(int ) {
atomic<int> a {1};
int b = a;
}
得到的匯編代碼
__Z8exchangei: ## @_Z8exchangei
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $16, %rsp
movl %edi, -4(%rbp) ## 此前和前面一樣,形參
movl L___const._Z8exchangei.a(%rip), %eax ## rip是64位機器的指令寄存器满粗,下一個要執(zhí)行指令的存放地址挤聘。
movl %eax, -8(%rbp) 组去;
leaq -8(%rbp), %rcx ## rcx 64位通常用來計數(shù)器的寄存器
movq %rcx, %rdi ## rdi 64位 字符串操作目的地址 和rsi一起 執(zhí)行串復制
callq __ZNKSt3__113__atomic_baseIiLb0EEcviEv
movl %eax, -12(%rbp)
addq $16, %rsp
popq %rbp
retq
.cfi_endproc
__ZNKSt3__113__atomic_baseIiLb0EEcviEv
經過兩次調用(這里略去這部分代碼),參考源碼atomic 中
typedef __atomic_base<_Tp*> __base;
.........
_LIBCPP_CONSTEXPR atomic(_Tp* __d) _NOEXCEPT : __base(__d) {}
其中
和 base類 __atomic_base 的構造键闺。然后到__atomic_base中, __base實現(xiàn)部分
struct __cxx_atomic_impl : public _Base {
#if _GNUC_VER >= 501
static_assert(is_trivially_copyable<_Tp>::value,
"std::atomic<Tp> requires that 'Tp' be a trivially copyable type");
#endif
_LIBCPP_INLINE_VISIBILITY __cxx_atomic_impl() _NOEXCEPT _LIBCPP_DEFAULT
_LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR explicit __cxx_atomic_impl(_Tp value) _NOEXCEPT
: _Base(value) {}
};
然后,在匯編代碼中最后調用到 :
@_ZNSt3__1L17__cxx_atomic_loadIiEET_PKNS_22__cxx_atomic_base_implIS1_EENS_12memory_orderE
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
movq %rdi, -8(%rbp)
movl %esi, -12(%rbp)
movq -8(%rbp), %rax
movl -12(%rbp), %ecx
movl %ecx, %edx
decl %edx
subl $2, %edx ## 從這里開始原子性的邏輯實現(xiàn)
## 后續(xù)大致邏輯就是符合一定要求的時候才可以進入指定的存儲區(qū)域讀取數(shù)據,
## 不然就要等待,這樣確保了數(shù)據的一致性(本人解讀,如有誤還請讀者指教)
movq %rax, -24(%rbp) ## 8-byte Spill rax通常執(zhí)行加法的需要
movl %ecx, -28(%rbp) ## 4-byte Spill ecx通常做計數(shù)處理
jb LBB4_2
jmp LBB4_5
LBB4_5:
movl -28(%rbp), %eax ## 4-byte Reload
subl $5, %eax
je LBB4_3
jmp LBB4_1
LBB4_1:
movq -24(%rbp), %rax ## 8-byte Reload
movl (%rax), %ecx
movl %ecx, -16(%rbp)
jmp LBB4_4
LBB4_2:
movq -24(%rbp), %rax ## 8-byte Reload
movl (%rax), %ecx
movl %ecx, -16(%rbp)
jmp LBB4_4
LBB4_3:
movq -24(%rbp), %rax ## 8-byte Reload
movl (%rax), %ecx
movl %ecx, -16(%rbp)
LBB4_4:
movl -16(%rbp), %eax
popq %rbp
retq
.cfi_endproc
以上是關于atomic變量的一些簡單分析。
- 內存模型
為了確保atomic的原子性颠印,隱含互斥量的使用降低了不少的效率:
atomic<int> a {0};
atomic<int> b {0};
//int a = 0;
//int b = 0;
int ValueSet(int) {
int t = 1;
for(int i = 0; i < 1000000000ll; i++) {
a++;b++;
}
}
int Observer(int) {
cout << "(" << a << ", " << b << ")" << endl; // 輸出不確定,但是atomic<int>的類型會比int小很多
}
int main() {
thread t1(ValueSet, 0);
thread t2(Observer, 0);
t2.join();
t1.join();
cout << "Got (" << a << ", " << b << ")" << endl;
// 運行到這,定義atomic<int> 運行時長超過int類型數(shù)據不少询件,具體和機型有關
}
可以明顯看出atomic<T> 為了確保原子性刻蟹,做出了很大性能犧牲座咆,其中之一就是原子類型數(shù)據是不可以亂序執(zhí)行的,在對性能有高要求的情況下哺呜,需要更有效的使用這些變量,又要確保不會由于編譯器優(yōu)化導致的非預期結果玻墅,比如可以要求部分情況下原子變量可以被并行執(zhí)行澳厢,可以亂序, 其中涉及到內存模型 memory-order的問題徐伐。
引入一個 強順序和弱順序的內存模型。
以X86位代表的芯片就是強順序模型性穿,所以強順序,就是生成的匯編指令在執(zhí)行的時候胯舷,按照我們看到的順序執(zhí)行炊汹,不會亂。
以PowerPC 霸褒、ArmV7位代表的弱順序模型,生成的匯編指令在執(zhí)行的時候殊轴,會被優(yōu)化后,沒有按照原先設定的順序執(zhí)行孽文,弱順序內存模型的出現(xiàn)主要是為了提高指令的執(zhí)行效率。
對于弱順序執(zhí)行的處理器而言,為了保證執(zhí)行的順序,在指令中加入了一個內存柵欄(memory barrier愕贡,也有翻譯成內存屏障的),一般用sync表示憨琳。
代碼表示如下:
a.store(t,memory_order_relaxed);
原子存儲操作(store)可以使用memorey_order_relaxed菌湃、memory_order_release、memory_order_seq_cst下愈。
原子讀取操作(load)可以使用memorey_order_relaxed、memory_order_consume叫编、memory_order_acquire、memory_order_seq_cst霞篡。
- 線程局部存儲TLS
int thread_local errorCode;
C++11規(guī)定了這種變量歸于線程顶滩。但是其實現(xiàn)給該變量分配內存盐欺,如何分配和管理與編譯器本身的實現(xiàn)有關冗美。