什么是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()
這個函數用來判斷兩個變量的類型是否一致,如果一致返回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()
這個函數用來判斷某個表達式是否是一個常量,如果是常量返回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 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()
這個函數返回調用函數的返回地址,參數為調用返回的層級,從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()
這個函數返回調用函數執行時棧內存為其分配的棧幀(stack frame)區間中的高位地址值。參數為調用函數的層級,從0開始并且只能是一個常數。這個函數可以用來實現防止棧內存溢出的棧保護處理。因為調用函數內定義的任何的局部變量的地址都必須要小于這個地址值。
void foo(char *buf)
{
void *frameaddr = __builtin_frame_address(0);
//定義棧內存變量,長度為100個字節。
char local[100];
int buflen = strlen(buf); //獲取傳遞進來的緩存字符串的長度。
if (local + buflen > frameaddr) //進行棧內存溢出判斷。
{
ptrinf("可能會出現棧內存溢出");
return;
}
strcpy(local, buf);
}
- __builtin_choose_expr()
這個函數主要用于實現在編譯時進行分支判斷和選擇處理,從而可以實現在編譯級別上的函數重載的能力。函數的格式為:
__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()
這個函數的主要作用是進行條件分支預測。 函數主要有兩個參數: 第一個參數是一個布爾表達式、第二個參數表明第一個參數的值為真值的概率,這個參數只能取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))
本節參考自:https://blog.csdn.net/jasonchen_gbd/article/details/44948523
- __builtin_prefetch()
這個函數主要用來實現內存數據的預抓取處理。一般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;
}
本節參考自:https://blog.csdn.net/chrysanthemumcao/article/details/8907566