step 0.1 先來1題
關(guān)于c++的inline關(guān)鍵字,以下說法正確的是(4.)
1. 使用inline關(guān)鍵字的函數(shù)會被編譯器在調(diào)用處展開 <--------不一定都會辕狰,有些會被編譯器拒絕味赃。
2. 頭文件中可以包含inline函數(shù)的聲明 <--------可以从祝?會有警告(所以我們姑且認(rèn)為不可以)
3. 可以在同一個項目的不同源文件內(nèi)定義函數(shù)名相同但實現(xiàn)不同的inline函數(shù) <--------可以是可以漆撞,也編譯通過苞慢,但會產(chǎn)生意想不到的結(jié)果艾扮,所以還是不推薦.
4. 定義在Class聲明內(nèi)的成員函數(shù)默認(rèn)是inline函數(shù) <--------ok
5. 優(yōu)先使用Class聲明內(nèi)定義的inline函數(shù) <----------|
6. 優(yōu)先使用Class實現(xiàn)的內(nèi)inline函數(shù)的實現(xiàn) <----------|--這兩個其實沒有誰優(yōu)先的掉盅,例如函數(shù)體太大有循環(huán)就不推薦使用inline
step 0.2 函數(shù)的調(diào)用過程
主要是通過這個來深♂入♂
理解為什么函數(shù)調(diào)用存在一定開銷。
函數(shù)的調(diào)用過程主要有(模糊)
一個大坑 - movq為AT&T指令赃阀,而我們課本上學(xué)的是MASM指令霎肯,兩者的方向是反的(我鈤了你大爺!)
AT&T的mov語法(movq為復(fù)制8個字節(jié)榛斯,雙字) -movq src,dest
call指令 - 主要是2部分操作組成:pushl %eip & ljmp
ret指令 - 主要是 popl %eip.
-
以下兩條指令用于切換堆棧上下文.
- enter指令 -
等價于
- enter指令 -
push ebp
ebp ← esp
- leave指令 -
等價于
esp ← ebp
pop ebp
(其中 esp - 棧頂指針观游;ebp - 棧基址指針)
- 下面詳細(xì)闡述一下函數(shù)調(diào)用的具體過程.
示例代碼 test_func_.c
int foo2(){ <---------為啥要兩個函數(shù)驮俗,主要是因為假如下面的foo不調(diào)用其他函數(shù)的話懂缕,在foo的匯編代碼里就不會有第30行了,即棧頂指針就不用了意述,想想也是合理提佣,這樣減少了一些代碼量
int kkkkk[4];
return 0;
}
int foo(int k1, int k2, int k3){
int s1[5] = {1,2,3,4,5};
int s = 1;
foo2();
return k1+s;
}
int main(){
int s=5;
int s2=40;
foo(400, 500, 600);
return 0;
}
gcc -S test_func_.c -o test_func_.s
的匯編結(jié)果:(不要怕,只看最關(guān)鍵的地方)
1 .file "test_func_.c"
2 .text
3 .globl foo2
4 .type foo2, @function
5 foo2: <---------foo2加進(jìn)來的原因是為了更好地展示調(diào)用函數(shù)的一般流程
6 .LFB0:
7 .cfi_startproc
8 pushq %rbp
9 .cfi_def_cfa_offset 16
10 .cfi_offset 6, -16
11 movq %rsp, %rbp
12 .cfi_def_cfa_register 6
13 movl $0, %eax
14 popq %rbp
15 .cfi_def_cfa 7, 8
16 ret
17 .cfi_endproc
18 .LFE0:
19 .size foo2, .-foo2
20 .globl foo
21 .type foo, @function
22 foo: <---------主要關(guān)心這個
23 .LFB1:
24 .cfi_startproc
25 pushq %rbp <------------舊基址進(jìn)棧
26 .cfi_def_cfa_offset 16
27 .cfi_offset 6, -16
28 movq %rsp, %rbp <------------新基址為當(dāng)前的棧頂
29 .cfi_def_cfa_register 6
30 subq $64, %rsp <-----------當(dāng)本函數(shù)有調(diào)用其他函數(shù)的動作時荤崇,才會把棧頂向下移拌屏,親測。例如把上面的foo2及相關(guān)調(diào)用刪除术荤,這句就沒了倚喂。
31 movl %edi, -52(%rbp) <-----------一系列的操作都是用棧基址寄存器+偏移量來操作的
32 movl %esi, -56(%rbp)
33 movl %edx, -60(%rbp)
34 movl $1, -32(%rbp)
35 movl $2, -28(%rbp)
36 movl $3, -24(%rbp)
37 movl $4, -20(%rbp)
38 movl $5, -16(%rbp)
39 movl $1, -36(%rbp)
40 movl $0, %eax
41 call foo2
42 movl -36(%rbp), %eax
42 movl -36(%rbp), %eax
43 movl -52(%rbp), %edx
44 addl %edx, %eax
45 leave
46 .cfi_def_cfa 7, 8
47 ret
48 .cfi_endproc
49 .LFE1:
50 .size foo, .-foo
51 .globl main
52 .type main, @function
53 main:
54 .LFB2:
55 .cfi_startproc
56 pushq %rbp
57 .cfi_def_cfa_offset 16
58 .cfi_offset 6, -16
59 movq %rsp, %rbp
60 .cfi_def_cfa_register 6
61 subq $16, %rsp
62 movl $5, -8(%rbp)
63 movl $40, -4(%rbp)
64 movl $600, %edx
65 movl $500, %esi
66 movl $400, %edi
67 call foo
68 movl $0, %eax
69 leave
70 .cfi_def_cfa 7, 8
71 ret
72 .cfi_endproc
73 .LFE2:
74 .size main, .-main
75 .ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
76 .section .note.GNU-stack,"",@progbits
現(xiàn)在的編譯器優(yōu)化呀,我真是too young端圈!
總結(jié)如下面這個圖:
堆棧分析
[如果太小焦读,請點這個](https://www.processon.com/view/57938177e4b06834741b7e17)
安利一下這個在線流程圖編輯
總結(jié)一下,
- 函數(shù)調(diào)用通過call(調(diào)用處)/ret(被調(diào)用函數(shù)內(nèi)部)實現(xiàn)ip入棧和跳轉(zhuǎn).
- 進(jìn)入函數(shù)的棧上下文切換是在函數(shù)體內(nèi)部進(jìn)行棽杖ǎ基址入棧和棿;危基址賦值.
- 進(jìn)入函數(shù)的棧上下文切換有可能不對%esp進(jìn)行減.
- 退出函數(shù)時的棧上下文切換通過leave指令實現(xiàn).
反正總之,只要是函數(shù)調(diào)用至少需要多4~5條指令宴倍,假如這個函數(shù)本身的指令很少张症,那是十分影響效率的。
step 1. 深入
- 什么是內(nèi)聯(lián)函數(shù)?(為什么要用內(nèi)聯(lián))
函數(shù)的調(diào)用存在一定代價鸵贬,如果函數(shù)本身執(zhí)行的邏輯很少(假設(shè)一個極端情況就是函數(shù)本身執(zhí)行的時間小于函數(shù)的調(diào)用代價)俗他,這樣就造成效率低下。(*)
C中這種情境下常用宏來解決阔逼。但宏本身又缺少參數(shù)檢查等機(jī)制兆衅,難以debug.
inline函數(shù)就在這種情況下采用,即結(jié)合了宏和函數(shù)兩種機(jī)制的優(yōu)點.- inline vs. 宏:
- 宏不能:
預(yù)處理器不能進(jìn)行類型安全檢查嗜浮,或者進(jìn)行自動類型轉(zhuǎn)換羡亩。
對象外的宏展開不能訪問對象的私有成員。 - 而對象外的內(nèi)聯(lián)函數(shù)調(diào)用卻可以:
安全檢查:內(nèi)聯(lián)函數(shù)參數(shù)在編譯階段進(jìn)行安全檢查危融。
自動類型轉(zhuǎn)換:內(nèi)聯(lián)函數(shù)參數(shù)可以進(jìn)行自動類型轉(zhuǎn)換夕春。
訪問私有成員:這個是宏無法辦到的。
編譯器將在調(diào)用處展開专挪,省去函數(shù)調(diào)用的代價。
- 宏不能:
- inline vs. 宏:
- 如何使用內(nèi)聯(lián)函數(shù)?
- 定義在類聲明中的函數(shù)將默認(rèn)被認(rèn)為是內(nèi)聯(lián)的片排。
- 編譯器可能拒絕內(nèi)聯(lián):函數(shù)體過大或者存在循環(huán)/遞歸等寨腔。
- 在函數(shù)定義處寫inline關(guān)鍵字,僅在聲明處寫inline不會起作用
(UPDATE:而且在聲明處寫inline還會造成一個警告 -inline function ‘void base::foo()’ used but never defined
)溉潭。 - ** 多文件調(diào)用內(nèi)聯(lián):如果內(nèi)聯(lián)函數(shù)的定義不在本文件中(例如base.h聲明了一個類废封,base.cc定義其的某個成員函數(shù)為內(nèi)聯(lián)虑绵,而在main.cc中調(diào)用base的某個對象的此成員函數(shù))將可能**產(chǎn)生一個鏈接階段的錯誤,類似于
也可能沒有出錯乾蛤,但會產(chǎn)生歧義,跟編譯器實現(xiàn)有關(guān)(捅僵?)家卖。例如main.cc:(.text+0x21): undefined reference to `base::foo()' collect2: error: ld returned 1 exit status
附1 PHASE_4
的例子。 - 解決上一點最好的辦法是庙楚,把inline函數(shù)的定義搬到.h中上荡,誰需要調(diào)用inline誰就include我這個.h免得出現(xiàn)意想不到的結(jié)果。(這也是實際場景中經(jīng)常做的馒闷。)
附1.
main.cc
#include "../common.h"http://include stdio.h and so on.
#include "base.h"
//inline void base::foo(){ kk = 200001; }//<-----------在PHASE_2/3去掉注釋
int test(base & kk);
int main()
{
base b(1000);
b.foo();
b.bar();
test(b);
b.bar();
return 0;
}
test.cc
#include "base.h"
//inline void base::foo(){ kk = 100001; } //<-----------在PHASE_3/4去掉注釋
int test(base & kk){
kk.foo();
return 0;
}
base.cc
#include "base.h"
inline void base::foo(){ kk = 10; }
base::base(int s)
: kk(s){
}
base.h
#ifndef BASE_H
#define BASE_H
#include "../common.h"
class base{
public:
base(int s);
//inline void foo();
/*
base.h:8:18: error: inline function ‘void base::foo()’ used but never defined [-Werror]
inline void foo();
^
cc1plus: all warnings being treated as errors
*/
void foo();
void bar(){ printf("%d\n",kk);}
private:
int kk;
};
#endif
Makefile(順便復(fù)習(xí)了一把Makfile酪捡!@H鳌!)
target=test
main_src=main.cc test.cc
base_src=base.cc
base_target=$(patsubst %.cc,%.o,$(base_src))
CXX=g++
CXXFLAGS=-Werror
.PHONY:clean
all:$(main_src) $(base_target)
$(CXX) $(CXXFLAGS) $^ -o $(target)
$(base_target):$(base_src) #如果Make命令運行時沒有指定目標(biāo),默認(rèn)會執(zhí)行Makefile文件的第一個目標(biāo)逛薇。
$(CXX) -c $^ -o $@
clean:
rm -rf $(target) $(base_target)
- PHASE_1 main.cc & test.cc都沒有inline base::foo的定義,出錯捺疼。
/tmp/ccINLKCW.o: In function `main':
main.cc:(.text+0x21): undefined reference to `base::foo()'
/tmp/cc1MfEWZ.o: In function `test(base&)':
test.cc:(.text+0x14): undefined reference to `base::foo()'
collect2: error: ld returned 1 exit status
make: *** [all] Error 1
- PHASE_2 main.cc中的注釋行去掉注釋:
root@vm1:/home/work/share/toys/CSE274.me/02_Cpp_Intro/testinline# ./test
200001
200001
- PHASE_3 test.cc中的注釋行也去掉注釋,同時main.cc保持與PHASE_2一致永罚。
root@vm1:/home/work/share/toys/CSE274.me/02_Cpp_Intro/testinline# ./test
200001
200001
- PHASE_4 只保留test.cc的inline定義.
root@vm1:/home/work/share/toys/CSE274.me/02_Cpp_Intro/testinline# ./test
100001
100001
這里PHASE_4啤呼,也是inline函數(shù)定義不在本文件中,但沒有出現(xiàn)ld錯誤尤蛮。