嵌入式C語(yǔ)言自我修養(yǎng)11:有一種函數(shù),叫內(nèi)建函數(shù)

11.1 什么是內(nèi)建函數(shù)

內(nèi)建函數(shù),顧名思義,就是編譯器內(nèi)部實(shí)現(xiàn)的函數(shù)。這些函數(shù)跟關(guān)鍵字一樣,可以直接使用,無(wú)須像標(biāo)準(zhǔn)庫(kù)函數(shù)那樣,要 #include 對(duì)應(yīng)的頭文件才能使用。

站在用戶的角度思考問(wèn)題,與客戶深入溝通,找到象山網(wǎng)站設(shè)計(jì)與象山網(wǎng)站推廣的解決方案,憑借多年的經(jīng)驗(yàn),讓設(shè)計(jì)與互聯(lián)網(wǎng)技術(shù)結(jié)合,創(chuàng)造個(gè)性化、用戶體驗(yàn)好的作品,建站類型包括:成都網(wǎng)站建設(shè)、成都網(wǎng)站設(shè)計(jì)、企業(yè)官網(wǎng)、英文網(wǎng)站、手機(jī)端網(wǎng)站、網(wǎng)站推廣、域名申請(qǐng)、網(wǎng)絡(luò)空間、企業(yè)郵箱。業(yè)務(wù)覆蓋象山地區(qū)。

內(nèi)建函數(shù)的函數(shù)命名,通常以 __builtin 開(kāi)頭。這些函數(shù)主要在編譯器內(nèi)部使用,主要是為編譯器服務(wù)的。內(nèi)建函數(shù)的主要用途如下。

  • 用來(lái)處理變長(zhǎng)參數(shù)列表;
  • 用來(lái)處理程序運(yùn)行異常;
  • 程序的編譯優(yōu)化、性能優(yōu)化;
  • 查看函數(shù)運(yùn)行中的底層信息、堆棧信息等;
  • C 標(biāo)準(zhǔn)庫(kù)函數(shù)的內(nèi)建版本。

因?yàn)閮?nèi)建函數(shù)是編譯器內(nèi)部定義,主要由編譯器相關(guān)的工具和程序調(diào)用,所以這些函數(shù)并沒(méi)有文檔說(shuō)明,而且變動(dòng)而頻繁。對(duì)于程序開(kāi)發(fā)者來(lái)說(shuō),不建議使用這些函數(shù)。

但有些函數(shù),對(duì)于我們了解程序運(yùn)行的底層信息、編譯優(yōu)化很有幫助,而且在 Linux 內(nèi)核中也經(jīng)常使用這些函數(shù),所以還是很有必要去了解 Linux 內(nèi)核中常用的一些內(nèi)建函數(shù)。

11.2 常用內(nèi)建函數(shù)

__builtinreturnaddress(LEVEL)

這個(gè)函數(shù)用來(lái)返回當(dāng)前函數(shù)或調(diào)用者的返回地址。函數(shù)的參數(shù) LEVEl 表示函數(shù)調(diào)用鏈中的不同層次的函數(shù),各個(gè)值代表的意義如下。

  • 0:返回當(dāng)前函數(shù)的返回地址;
  • 1:返回當(dāng)前函數(shù)調(diào)用者的返回地址;
  • 2:返回當(dāng)前函數(shù)調(diào)用者的調(diào)用者的返回地址;
  • ……

我們接下來(lái)寫(xiě)一個(gè)測(cè)試程序。

void f(void)
    {
        int *p;
        p = __builtin_return_address(0);
        printf("f    return address: %p\n",p);
        p = __builtin_return_address(1);;
        printf("func return address: %p\n",p);
        p = __builtin_return_address(2);;
        printf("main return address: %p\n",p);
        printf("\n");
    }
    void func(void)
    {
        int *p;
        p = __builtin_return_address(0);
        printf("func return address: %p\n",p);
        p = __builtin_return_address(1);;
        printf("main return address: %p\n",p);
        printf("\n");
        f();
    }

    int main(void)
    {
        int *p;
        p = __builtin_return_address(0);
        printf("main return address: %p\n",p);
        printf("\n"); 
        func();
        printf("goodbye!\n");
        return 0;
    }

C 語(yǔ)言函數(shù)在調(diào)用過(guò)程中,會(huì)將當(dāng)前函數(shù)的返回地址、寄存器等現(xiàn)場(chǎng)信息保存在堆棧中,然后才會(huì)跳到被調(diào)用函數(shù)中去執(zhí)行。當(dāng)被調(diào)用函數(shù)執(zhí)行結(jié)束后,根據(jù)保存在堆棧中的返回地址,就可以直接返回到原來(lái)的函數(shù)中繼續(xù)執(zhí)行。

在這個(gè)程序中,main() 函數(shù)調(diào)用 func() 函數(shù),在 main() 函數(shù)跳轉(zhuǎn)到 func() 函數(shù)執(zhí)行之前,會(huì)將程序正在運(yùn)行的當(dāng)前語(yǔ)句的下一條語(yǔ)句(如下代碼所示)的地址保存到堆棧中,然后才去執(zhí)行 func(); 這條語(yǔ)句,跳到 func() 函數(shù)去執(zhí)行。func() 執(zhí)行完畢后,如何返回到 main() 函數(shù)呢?很簡(jiǎn)單,將保存到堆棧中的返回地址賦值給 PC 指針,就可以直接返回到 main() 函數(shù),繼續(xù)往下執(zhí)行了。

printf("goodbye!\n");

每一層函數(shù)調(diào)用,都會(huì)將當(dāng)前函數(shù)的下一條指令地址,即返回地址壓入堆棧保存。各層函數(shù)調(diào)用就構(gòu)成 了一個(gè)函數(shù)調(diào)用鏈。在各層函數(shù)內(nèi)部,我們使用內(nèi)建函數(shù)就可以打印這個(gè)調(diào)用鏈上各個(gè)函數(shù)的返回地址。程序的運(yùn)行結(jié)果如下。

main return address:0040124B

func return address:004013C3
main return address:0040124B

f    return address:00401385
func return address:004013C3
main return address:0040124B

__builtinframeaddress(LEVEL)

在函數(shù)調(diào)用過(guò)程中,還有一個(gè)“棧幀”的概念。函數(shù)每調(diào)用一次,都會(huì)將當(dāng)前函數(shù)的現(xiàn)場(chǎng)(返回地址、寄存器等)保存在棧中,每一層函數(shù)調(diào)用都會(huì)將各自的現(xiàn)場(chǎng)信息都保存在各自的棧中。這個(gè)棧也就是當(dāng)前函數(shù)的棧幀,每一個(gè)棧幀有起始地址和結(jié)束地址,表示當(dāng)前函數(shù)的堆棧信息。多層函數(shù)調(diào)用就會(huì)有多個(gè)棧幀,每個(gè)棧幀里會(huì)保存上一層棧幀的起始地址,這樣各個(gè)棧幀就形成了一個(gè)調(diào)用鏈。很多調(diào)試器、GDB、包括我們的這個(gè)內(nèi)建函數(shù),其實(shí)都是通過(guò)回溯函數(shù)棧幀調(diào)用鏈來(lái)獲取函數(shù)底層的各種信息的。比如,返回地址 i、調(diào)用關(guān)系等。在 ARM 系統(tǒng)中,使用 FP 和 SP 這兩個(gè)寄存器,分別指向當(dāng)前函數(shù)棧幀的起始地址和結(jié)束地址。當(dāng)函數(shù)繼續(xù)調(diào)用或者返回,這兩個(gè)寄存器的值也會(huì)發(fā)生變化,總是指向當(dāng)前函數(shù)棧幀的起始地址和結(jié)束地址。

我們可以通過(guò)內(nèi)建函數(shù) __builtinframeaddress(LEVEL),查看函數(shù)的棧幀地址。

  • 0:查看當(dāng)前函數(shù)的棧幀地址
  • 1:查看當(dāng)前函數(shù)調(diào)用者的棧幀地址
  • ……

寫(xiě)一個(gè)程序,打印當(dāng)前函數(shù)的棧幀地址。

void func(void)
{
    int *p;
    p = __builtin_frame_address(0);
    printf("func frame:%p\n",p);
    p = __builtin_frame_address(1);
    printf("main frame:%p\n",p);
}

int main(void)
{
    int *p;
    p = __builtin_frame_address(0);
    printf("main frame:%p\n",p);
    printf("\n");
    func();
    return 0;
}

程序運(yùn)行結(jié)果如下。

main frame:0028FF48

func frame:0028FF28
main frame:0028FF48

11.3 C 標(biāo)準(zhǔn)庫(kù)的內(nèi)建函數(shù)

在 GNU C 編譯器內(nèi)部,實(shí)現(xiàn)了一些和 C 標(biāo)準(zhǔn)庫(kù)函數(shù)類似的內(nèi)建函數(shù)。這些函數(shù)跟 C 標(biāo)準(zhǔn)庫(kù)函數(shù)功能相似,函數(shù)名也相同,只是在前面加了一個(gè)前綴 __builtin。如果你不想使用 C 庫(kù)函數(shù),也可以加個(gè)前綴,直接使用對(duì)應(yīng)的內(nèi)建函數(shù)。

常見(jiàn)的標(biāo)準(zhǔn)庫(kù)函數(shù)如下:

  • 內(nèi)存相關(guān)的函數(shù):memcpy 、memset、memcmp
  • 數(shù)學(xué)函數(shù):log、cos、abs、exp
  • 字符串處理函數(shù):strcat、strcmp、strcpy、strlen
  • 打印函數(shù):printf、scanf、putchar、puts

接下來(lái)我們寫(xiě)個(gè)小程序,使用與 C 標(biāo)準(zhǔn)庫(kù)對(duì)應(yīng)的內(nèi)建函數(shù)。

int main(void)
{    
    char a[100];
    __builtin_memcpy(a,"hello world!",20);
    __builtin_puts(a);

    return 0;
}

程序運(yùn)行結(jié)果如下。

hello world!

通過(guò)運(yùn)行結(jié)果我們看到,使用與 C 標(biāo)準(zhǔn)庫(kù)對(duì)應(yīng)的內(nèi)建函數(shù),同樣也能實(shí)現(xiàn)字符串的復(fù)制和打印,實(shí)現(xiàn) C 標(biāo)準(zhǔn)庫(kù)函數(shù)的功能。

11.4 內(nèi)建函數(shù):__builtinconstantp(n)

編譯器內(nèi)部還有一些內(nèi)建函數(shù),主要用來(lái)編譯優(yōu)化、性能優(yōu)化,如 __builtinconstantp(n) 函數(shù)。該函數(shù)主要用來(lái)判斷參數(shù) n 在編譯時(shí)是否為常量,是常量的話,函數(shù)返回1;否則函數(shù)返回0。該函數(shù)常用于宏定義中,用于編譯優(yōu)化。一個(gè)宏定義,根據(jù)宏的參數(shù)是常量還是變量,可能實(shí)現(xiàn)的方法不一樣。在內(nèi)核中經(jīng)常看到這樣的宏。

#define _dma_cache_sync(addr, sz, dir)        \
do {                            \
    if (__builtin_constant_p(dir))          \
        __inline_dma_cache_sync(addr, sz, dir); \
    else                        \
        __arc_dma_cache_sync(addr, sz, dir);    \
}                            \
while (0);

很多計(jì)算或者操作在參數(shù)為常數(shù)時(shí)可能有更優(yōu)化的實(shí)現(xiàn),在這個(gè)宏定義中,我們實(shí)現(xiàn)了兩個(gè)版本。根據(jù)參數(shù)是否為常數(shù),我們可以靈活選用不同的版本。

11.5 內(nèi)建函數(shù):__builtin_expect(exp,c)

內(nèi)建函數(shù) __builtin_expect 也常常用來(lái)編譯優(yōu)化。這個(gè)函數(shù)有兩個(gè)參數(shù),返回值就是其中一個(gè)參數(shù),仍是 exp。這個(gè)函數(shù)的意義主要就是告訴編譯器:參數(shù) exp 的值為 c 的可能性很大。然后編譯器可能就會(huì)根據(jù)這個(gè)提示信息,做一些分支預(yù)測(cè)上的代碼優(yōu)化。

參數(shù) c 跟這個(gè)函數(shù)的返回值無(wú)關(guān),無(wú)論 c 為何值,函數(shù)的返回值都是 exp。

int main(void)
{    
    int a;
    a = __builtin_expect(3,1);
    printf("a = %d\n",a);

    a = __builtin_expect(3,10);
    printf("a = %d\n",a);

    a = __builtin_expect(3,100);
    printf("a = %d\n",a);
    return 0;
}

程序運(yùn)行結(jié)果如下。

a = 3
a = 3
a = 3

這個(gè)函數(shù)的主要用途就是編譯器的分支預(yù)測(cè)優(yōu)化?,F(xiàn)代 CPU 內(nèi)部,都有 cache 這個(gè)緩存器件。CPU 的運(yùn)行速度很高,而外部 RAM 的速度相對(duì)來(lái)說(shuō)就低了不少,所以當(dāng) CPU 從內(nèi)存 RAM 讀寫(xiě)數(shù)據(jù)時(shí)就會(huì)有一定的性能瓶頸。為了提高程序執(zhí)行效率,CPU 都會(huì)通過(guò) cache 這個(gè) CPU 內(nèi)部緩沖區(qū)來(lái)緩存一定的指令或數(shù)據(jù)。CPU 讀寫(xiě)內(nèi)存 RAM 中的數(shù)據(jù)時(shí),會(huì)先到 cache 里面去看看能不能找到。找到的話就直接進(jìn)行讀寫(xiě);找不到的話,cache 會(huì)重新緩存一部分內(nèi)存數(shù)據(jù)進(jìn)來(lái)。CPU 讀寫(xiě) cache 的速度遠(yuǎn)遠(yuǎn)大于內(nèi)存 RAM,所以通過(guò)這種方式,可以提高系統(tǒng)的性能。

那 cache 如何緩存內(nèi)存數(shù)據(jù)呢?簡(jiǎn)單來(lái)說(shuō),就是依據(jù)空間相近原則。比如 CPU 正在執(zhí)行一條指令,那么下一個(gè)指令周期,CPU 就會(huì)大概率執(zhí)行當(dāng)前指令的下一條指令。如果此時(shí) cache 將下面幾條指令都緩存到 cache 里面,下一個(gè)指令周期 CPU 就可以直接到 cache 里取指、翻譯、執(zhí)行,從而使運(yùn)算效率大大提高。

但有時(shí)候也會(huì)出現(xiàn)意外。比如程序在執(zhí)行過(guò)程中遇到函數(shù)調(diào)用、if 分支、goto 跳轉(zhuǎn)等程序結(jié)構(gòu),會(huì)跳到其它地址執(zhí)行,那么緩存到 cache 中的指令就不是 CPU 要獲取的指令。此時(shí),我們就說(shuō) cache 沒(méi)有命中,cache 會(huì)重新緩存正確的指令代碼給 CPU 讀取,這就是 cache 工作的基本流程。

有了這個(gè)理論基礎(chǔ),我們?cè)诰帉?xiě)程序時(shí),遇到 if/switch 這種選擇分支的程序結(jié)構(gòu),可以將大概率發(fā)生的分支寫(xiě)在前面,這樣程序運(yùn)行時(shí),因?yàn)榇蟾怕拾l(fā)生,所以大部分時(shí)間就不需要跳轉(zhuǎn),程序就相當(dāng)于一個(gè)順序結(jié)構(gòu),從而提高 cache 的命中率。內(nèi)核中已經(jīng)實(shí)現(xiàn)一些相關(guān)的宏,如 likely 和 unlikely,用來(lái)提醒程序員優(yōu)化程序。

11.6 內(nèi)核中的 likely 和 unlikely

Linux 內(nèi)核中,使用 __builtin_expect 內(nèi)建函數(shù),定義了兩個(gè)宏。

#define likely(x) __builtin_expect(!!(x),1)
#define unlikely(x) __builtin_expect(!!(x),0)

這兩個(gè)宏的主要作用,就是告訴編譯器:某一個(gè)分支發(fā)生的概率很高,或者說(shuō)很低,基本不可能發(fā)生。編譯器就根據(jù)這個(gè)提示信息,就會(huì)去做一些分值預(yù)測(cè)的編譯優(yōu)化。在這兩個(gè)宏定義有一個(gè)細(xì)節(jié),就是對(duì)宏的參數(shù) x 做兩次取非操作,這是為了將參數(shù) x 轉(zhuǎn)換為布爾類型,然后與 1 和 0 作比較,告訴編譯器 x 為真或?yàn)榧俚目赡苄院芨摺?/p>

我們接下來(lái)舉個(gè)例子,讓大家感受下,使用這兩個(gè)宏后,編譯器在分支預(yù)測(cè)上的一些編譯變化。

//expect.c
int main(void)
{
    int a;
    scanf("%d",&a);
    if( a==0)
    {
        printf("%d",1);
        printf("%d",2);
        printf("\n");
    }
    else
    {
        printf("%d",5);
        printf("%d",6);
        printf("\n");
    }
    return 0;
}

在這個(gè)程序中,根據(jù)我們輸入變量 a 的值,程序會(huì)執(zhí)行不同的分支代碼。我們接著對(duì)這個(gè)程序反匯編,生成對(duì)應(yīng)的匯編代碼。

$ arm-linux-gnueabi-gcc  expect.c
$ arm-linux-gnueabi-objdump -D a.out
 00010558 <main>:
   10558:    e92d4800    push    {fp, lr}
   1055c:    e28db004    add fp, sp, #4
   10560:    e24dd008    sub sp, sp, #8
   10564:    e59f308c    ldr r3, [pc, #140]  
   10568:    e5933000    ldr r3, [r3]
   1056c:    e50b3008    str r3, [fp, #-8]
   10570:    e24b300c    sub r3, fp, #12
   10574:    e1a01003    mov r1, r3
   10578:    e59f007c    ldr r0, [pc, #124]  
   1057c:    ebffffa5    bl  10418 <__isoc99_scanf@plt>
   10580:    e51b300c    ldr r3, [fp, #-12]
   10584:    e3530000    cmp r3, #0
   10588:    1a000008    bne 105b0 <main+0x58>
   1058c:    e3a01001    mov r1, #1
   10590:    e59f0068    ldr r0, [pc, #104]  
   10594:    ebffff90    bl  103dc <printf@plt>
   10598:    e3a01002    mov r1, #2
   1059c:    e59f005c    ldr r0, [pc, #92]
   105a0:    ebffff8d    bl  103dc <printf@plt>
   105a4:    e3a0000a    mov r0, #10
   105a8:    ebffff97    bl  1040c <putchar@plt>
   105ac:    ea000007    b   105d0 <main+0x78>
   105b0:    e3a01005    mov r1, #5
   105b4:    e59f0044    ldr r0, [pc, #68]
   105b8:    ebffff87    bl  103dc <printf@plt>
   105bc:    e3a01006    mov r1, #6
   105c0:    e59f0038    ldr r0, [pc, #56]
   105c4:    ebffff84    bl  103dc <printf@plt>

觀察 main 函數(shù)的反匯編代碼,我們看到:匯編代碼的結(jié)構(gòu)就是基于我們的 if/else 分支先后順序,依次生成對(duì)應(yīng)的匯編代碼(看 10588:bne 105b0 跳轉(zhuǎn))。我們接著改一下代碼,使用 unlikely 修飾 if 分支,告訴編譯器,這個(gè) if 分支小概率發(fā)生,或者說(shuō)不可能發(fā)生。

//expect.c
int main(void)
{
    int a;
    scanf("%d",&a);
    if( unlikely(a==0) )
    {
        printf("%d",1);
        printf("%d",2);
        printf("\n");
    }
    else
    {
        printf("%d",5);
        printf("%d",6);
        printf("\n");
    }
    return 0;
}

對(duì)這個(gè)程序添加 -O2 優(yōu)化參數(shù)編譯,并對(duì)生成的可執(zhí)行文件 a.out 反匯編。

$ arm-linux-gnueabi-gcc -O2 expect.c
 $ arm-linux-gnueabi-objdump -D a.out
00010438 <main>:
   10438:    e92d4010    push    {r4, lr}
   1043c:    e59f4080    ldr r4, [pc, #128]  
   10440:    e24dd008    sub sp, sp, #8
   10444:    e5943000    ldr r3, [r4]
   10448:    e1a0100d    mov r1, sp
   1044c:    e59f0074    ldr r0, [pc, #116]
   10450:    e58d3004    str r3, [sp, #4]
   10454:    ebfffff1    bl  10420 <__isoc99_scanf@plt>
   10458:    e59d3000    ldr r3, [sp]
   1045c:    e3530000    cmp r3, #0
   10460:    0a000010    beq 104a8 <main+0x70>
   10464:    e3a02005    mov r2, #5
   10468:    e59f105c    ldr r1, [pc, #92]
   1046c:    e3a00001    mov r0, #1
   10470:    ebffffe7    bl  10414 <__printf_chk@plt>
   10474:    e3a02006    mov r2, #6
   10478:    e59f104c    ldr r1, [pc, #76]
   1047c:    e3a00001    mov r0, #1
   10480:    ebffffe3    bl  10414 <__printf_chk@plt>
   10484:    e3a0000a    mov r0, #10
   10488:    ebffffde    bl  10408 <putchar@plt>
   1048c:    e59d2004    ldr r2, [sp, #4]
   10490:    e5943000    ldr r3, [r4]
   10494:    e3a00000    mov r0, #0
   10498:    e1520003    cmp r2, r3
   1049c:    1a000007    bne 104c0 <main+0x88>
   104a0:    e28dd008    add sp, sp, #8
   104a4:    e8bd8010    pop {r4, pc}
   104a8:    e3a02001    mov r2, #1
   104ac:    e59f1018    ldr r1, [pc, #24]
   104b0:    e1a00002    mov r0, r2
   104b4:    ebffffd6    bl  10414 <__printf_chk@plt>
   104b8:    e3a02002    mov r2, #2
   104bc:    eaffffed    b   10478 <main+0x40>

我們對(duì) if 分支條件表達(dá)式使用 unlikely 修飾,告訴編譯器這個(gè)分支小概率發(fā)生。在編譯器開(kāi)啟優(yōu)化編譯條件下,通過(guò)生成的反匯編代碼(10460:beq 104a8),我們可以看到,編譯器將小概率發(fā)生的 if 分支匯編代碼放在了后面,將 else 分支的匯編代碼放在了前面,這樣就確保了程序在執(zhí)行時(shí),大部分時(shí)間都不需要跳轉(zhuǎn),直接按順序執(zhí)行下面大概率發(fā)生的分支代碼。

在 Linux 內(nèi)核中,你會(huì)發(fā)現(xiàn)很多地方使用 likely 和 unlikely 宏修飾,此時(shí)你應(yīng)該知道它們的用途了吧。

本教程根據(jù) C語(yǔ)言嵌入式Linux高級(jí)編程視頻教程 第05期 改編,電子版書(shū)籍可加入QQ群:475504428 下載,更多嵌入式視頻教程,可關(guān)注:
微信公眾號(hào):宅學(xué)部落(armlinuxfun)
51CTO學(xué)院-王利濤老師:http://edu.51cto.com/sd/d344f

當(dāng)前名稱:嵌入式C語(yǔ)言自我修養(yǎng)11:有一種函數(shù),叫內(nèi)建函數(shù)
網(wǎng)頁(yè)路徑:http://muchs.cn/article30/pdpeso.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供靜態(tài)網(wǎng)站、企業(yè)網(wǎng)站制作云服務(wù)器、商城網(wǎng)站、軟件開(kāi)發(fā)、網(wǎng)站收錄

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)

網(wǎng)站托管運(yùn)營(yíng)