什么是 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
欢迎大家访问欧阳大哥 2013 的 GitHub 地址和简书地址
来源: http://www.jianshu.com/p/d04bb63e9b81