LLVM編譯器中的內建(built-in)函式

歐陽大哥2013發表於2019-05-05

什麼是built-in 函式?

在一些.h標頭檔案中或者實現程式碼中經常會看到一些以__builtin_開頭的函式宣告或者呼叫,比如下面的標頭檔案#include <secure/_string.h>中的函式定義:

//這裡的memcpy函式的由內建函式__builtin___memcpy_chk來實現。
#if __has_builtin(__builtin___memcpy_chk) || defined(__GNUC__)
#undef memcpy
/* void *memcpy(void *dst, const void *src, size_t n) */
#define memcpy(dest, ...) \
		__builtin___memcpy_chk (dest, __VA_ARGS__, __darwin_obsz0 (dest))
#endif
複製程式碼

這些__builtin_開頭的符號其實是一些編譯器內建的函式或者編譯優化處理開關等,其作用類似於巨集。巨集是高階語言用於預編譯時進行替換的原始碼塊,而內建函式則是用於在編譯階段進行替換的機器指令塊。因此編譯器的這些內建函式其實並不是真實的函式,而只是一段指令塊,起到編譯時的內聯功能。

內建函式和非內建函式的呼叫的區別

在一些編譯器中會對一些標準庫的函式實現改用內建函式來代替,可以起到效能優化的作用。因為執行這些函式呼叫會在編譯時變為直接指令塊的執行,而不會產生指令跳轉、堆疊等相關的操作而引起的函式呼叫開銷(有一些函式直接就有一條對應的機器指令來實現,如果改用普通函式呼叫勢必效能大打折扣)。不同的編譯器對內建函式的支援不盡相同,而且對於是否用內建函式來實現標準庫函式也沒有統一的標準。比如對於GCC來說它所支援的內建函式都在GCC內建函式列表中被定義和宣告,這些內建函式大部分也被LLVM編譯器所支援。

本文不會介紹所有的內建函式,而是隻介紹其中幾個特殊的內建函式以及使用方法。熟練使用這些內建函式可以提升程式的執行效能以及擴充套件一些程式設計的模式。

  • __builtin_types_compatible_p(type1, type2)

這個函式用來判斷兩個變數的型別是否一致,如果一致返回true否則返回false。這裡的變數會忽略一些修飾關鍵字,比如const int 和 int 會被認為是相同的變數型別。可以用這個函式來判斷某個變數是否是特定的型別,還可以用這個函式來做一些型別檢查相關的防護邏輯。一般這個函式都和typeof關鍵字一起使用。

 int a, b
 long c;

int ret1= __builtin_types_compatible_p(typeof(a), typeof(b));  //true
int ret2 = __builtin_types_compatible_p(typeof(a), typeof(c)); //false 
int ret3 = __builtin_types_compatible_p(int , const int);  //true
if  (__builtin_types_compatible_p(typeof(a), int))   //true
{
    
}

複製程式碼
  • __builtin_constant_p(val)

這個函式用來判斷某個表示式是否是一個常量,如果是常量返回true否則返回false。

   int a = 10;
   const int b = 10;
   int ret1  = __builtin_constant_p(10);  //true
   int ret2 = __builtin_constant_p(a);  //false
   int ret3 = __builtin_constant_p(b);  //true
複製程式碼
  • __builtin_offsetof(struct, name)

這個函式用來獲取一個結構體成員在結構中的偏移量。函式的第一個引數是結構體型別,第二個引數是其中的資料成員的名字。

struct S
{
    char m_a;
    long m_b;
};

int offset1 = __builtin_offsetof(struct S, m_a);  //0
int offset2 = __builtin_offsetof(struct S, m_b);  //8

struct S s;
s.m_a = 'a';
s.m_b = 10;
    
char m_a = *(char*)((char*)&s + offset1);   //'a'
long m_b = *(long*)((char*)&s + offset2);  // 10
複製程式碼
  • __builtin_return_address(level)

這個函式返回撥用函式的返回地址,引數為呼叫返回的層級,從0開始,並且只能是一個常數。假如有一個函式呼叫棧為A->B->C->D。那麼在D函式內呼叫__builtin_return_address(0)返回的是C函式呼叫D函式的下一條指令的地址,如果呼叫的是__builtin_return_address(1)則返回B函式呼叫C函式的下一條指令的地址,依次類推。這個函式的一個應用場景是被呼叫者內部可以根據外部呼叫者的不同而進行差異化處理。

//這個例子演示一個函式foo。如果是被fout1函式呼叫則返回1,被其他函式呼叫時則返回0。

#include <dlfcn.h>

extern int foo();

void fout1()
{
    printf("ret1 = %d\n", foo());    //ret1 = 1
}

void fout2()
{
    printf("ret2 = %d\n", foo());    //ret2= 0
}

int foo()
{
     void *retaddr = __builtin_return_address(0);  //這個返回地址就是呼叫者函式的某一處的地址。  
    //根據返回地址可以通過dladdr函式獲取呼叫者函式的資訊。
    Dl_info dlinfo;
    dladdr(retaddr, &dlinfo);
    if (dlinfo.dli_saddr == fout1)
        return 1;
    else
        return 0;
}
複製程式碼

__builtin_return_address()函式的另外一個經典的應用是iOS系統中用ARC進行記憶體管理時對返回值是OC物件的函式和方法的特殊處理。比如一個函式foo返回一個OC物件時,系統在編譯時會對返回的物件呼叫objc_autoreleaseReturnValue函式,而在呼叫foo函式時則會在編譯時插入如下的三條彙編指令:

//arm64位的指令
bl foo
mov fp, fp    //這條指令看似無意義,其實這是一條特殊標誌指令。
bl objc_retainAutoreleasedReturnValue
複製程式碼

如果考察objc_autoreleaseReturnValue函式的內部實現就會發現其內部用了__builtin_return_address函式。objc_autoreleaseReturnValue函式通過呼叫__builtin_return_address(0)返回的地址的內容是否是mov fp,fp來進行特殊的處理。具體原理可以參考這些函式的實現,因為它們都已經開源。

  • __builtin_frame_address(level)

這個函式返回撥用函式執行時棧記憶體為其分配的棧幀(stack frame)區間中的高位地址值。引數為呼叫函式的層級,從0開始並且只能是一個常數。這個函式可以用來實現防止棧記憶體溢位的棧保護處理。因為呼叫函式內定義的任何的區域性變數的地址都必須要小於這個地址值。

void foo(char *buf)
{
   void *frameaddr =  __builtin_frame_address(0);
  
   //定義棧記憶體變數,長度為100個位元組。
   char local[100];

   int buflen = strlen(buf);   //獲取傳遞進來的快取字串的長度。
   iflocal + buflen > frameaddr)  //進行棧記憶體溢位判斷。
   {
         ptrinf("可能會出現棧記憶體溢位");
         return;
   }
   
  strcpy(local, buf);
}

複製程式碼
  • __builtin_choose_expr(exp, e1, e2)

這個函式主要用於實現在編譯時進行分支判斷和選擇處理,從而可以實現在編譯級別上的函式過載的能力。函式的格式為: __builtin_choose_expr(exp, e1, e2) 其所表達的意思是判斷表示式exp的值,如果值為真則使用e1程式碼塊的內容,而如果值為假時則使用e2程式碼塊的內容。這個函式一般都和__builtin_types_compatible_p函式一起使用,將型別判斷作為表示式引數。比如下面的程式碼:

void fooForInt(int a)
{
    printf("int a = %d\n", a);
}

void fooForDouble(double a)
{
    printf("double a=%f\n", a);
}

//如果x的資料型別是整型則使用fooForInt函式,否則使用fooForDouble函式。
#define fooFor(x) __builtin_choose_expr(__builtin_types_compatible_p(typeof(x), int), fooForInt(x), fooForDouble(x))

//根據傳遞進入的引數型別來決定使用哪個具體的函式。
fooFor(10);
fooFor(10.0);

複製程式碼
  • __builtin_expect(bool exp, probability)

這個函式的主要作用是進行條件分支預測。 函式主要有兩個引數: 第一個引數是一個布林表示式、第二個參數列明第一個引數的值為真值的概率,這個引數只能取1或者0,當取值為1時表示布林表示式大部分情況下的值是真值,而取值為0時則表示布林表示式大部分情況下的值是假值。函式的返回就是第一個引數的表示式的值。 在一條指令執行時,由於流水線的作用,CPU可以完成下一條指令的取指,這樣可以提高CPU的利用率。在執行一條條件分支指令時,CPU也會預取下一條執行,但是如果條件分支跳轉到了其他指令,那CPU預取的下一條指令就沒用了,這樣就降低了流水線的效率。__builtin_expect 函式可以優化程式編譯後的指令序列,使指令儘可能的順序執行,從而提高CPU預取指令的正確率。例如:

if (__builtin_expect (x, 0))
     foo ();
複製程式碼

表示x的值大部分情況下可能為假,因此foo()函式得到執行的機會比較少。這樣編譯器在編譯這段程式碼時就不會將foo()函式的彙編指令緊挨著if條件跳轉指令。再例如:

if (__builtin_expect (x, 1))
     foo ();
複製程式碼

表示x的值大部分情況下可能為真,因此foo()函式得到執行的機會比較大。這樣編譯器在編譯這段程式碼時就會將foo()函式的彙編指令緊挨著if條件跳轉指令。

為了簡化函式的使用,iOS系統的兩個巨集fastpath和slowpath來實現這種分支優化判斷處理。


#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))

複製程式碼

本節參考自:blog.csdn.net/jasonchen_g…

  • __builtin_prefetch(addr, rw, locality)

這個函式主要用來實現記憶體資料的預抓取處理。一般CPU內部都會提供幾級快取記憶體,在快取記憶體中進行資料存取要比在記憶體中速度快。因此為了提升效能,可以預先將某個記憶體地址中的資料讀取或寫入到快取記憶體中去,這樣當真實需要對記憶體地址進行存取時實際上是在快取記憶體中進行。而__builtin_prefetch函式就是用來將某個記憶體中的資料預先載入或寫入到快取記憶體中去。函式的格式如下: __builtin_prefetch(addr, rw, locality) 其中addr就是要進行預抓取的記憶體地址。 rw是一個可選引數取值只能取0或者1,0表示未來要預計對記憶體進行讀操作,而1表示預計對記憶體進行寫操作。locality 取值必須是常數,也稱為“時間區域性性”(temporal locality) 。時間區域性性是指,如果程式中某一條指令一旦執行,則不久之後該指令可能再被執行;如果某資料被訪問,則不久之後該資料會被再次訪問。該值的範圍在 0 - 3 之間。為 0 時表示,它沒有時間區域性性,也就是說,要訪問的資料或地址被訪問之後的短時間內不會再被訪問;為 3 時表示,被訪問的資料或地址具有高 時間區域性性,也就是說,在被訪問不久之後非常有可能再次訪問;對於值 1 和 2,則分別表示具有低 時間區域性性 和中等 時間區域性性。該值預設為 3 。 一般執行資料預抓取的操作都是在地址將要被訪問之前的某個時間進行。通過資料預抓取可以有效的提高資料的存取訪問速度。比如下面的程式碼實現對陣列中的所有元素執行頻繁的寫之前進行預抓取處理:

//定義一個陣列,在接下來的時間中需要對陣列進行頻繁的寫處理,因此可以將陣列的記憶體地址預抓取到快取記憶體中去。
int arr[10];
for (int i = 0; i < 10; i++)
{
     __builtin_prefetch(arr+i, 1, 3);
}

//後面會頻繁的對陣列元素進行寫入處理,因此如果不呼叫預抓取函式的話,每次寫操作都是直接對記憶體地址進行寫處理。
//而當使用了快取記憶體後,這些寫操作可能只是在快取記憶體中執行。
for (int i = 0; i < 1000000; i++)
{
     arr[i%10] = i;
}

複製程式碼

本節參考自:blog.csdn.net/chrysanthem…


歡迎大家訪問歐陽大哥2013的github地址

相關文章