什么是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