扯淡-Linux 内核中的 GCC 特性在程序中的应用

来源:互联网 发布:远行星号舰船数据 编辑:程序博客网 时间:2024/06/04 23:23

    前言:工作闲暇之际喜欢阅读linux内核代码,为之简洁而震撼,congding时爱参考内核代码。今天在一个应用中看到GCC的一个特性,感觉收益非浅,参考几篇文章。

    声明: 此博文只是一个读书笔记,有疏漏错误之处欢迎指正! 由于能力有限给你带来的困惑请见谅!

    目的:了解这些特殊的 GCC 特性,学习如何在 Linux 内核以及应用程序中使用它们。

    GCC 和 Linux 是出色的组合。尽管它们是独立的软件,但是 Linux 完全依靠 GCC 在新的体系结构上运行。Linux 还利用 GCC 中的特性(称为扩展)实现更多功能和优化。

    功能扩展

    1、GCC 允许通过变量的引用识别类型,来支泛型编程,例如使用 typeof 构建 min 和 max 等依赖于类型的操。

 #define min(x, y) ({\typeof(x) _min1 = (x);\typeof(y) _min2 = (y);\(void) (&_min1 == &_min2);\ _min1 < _min2 ? _min1 : _min2; })

(void) (&_min1 == &_min2) 的作用是判断两个操作数的类型是否一致,如果两个不同类型的比较,会在编译是给出警告:warning: comparison of distinct pointer types lacks a cast (参数分别是unsigned char 和 int)。

     2、范围扩展

     GCC 支持范围,在 C 语言的许多方面都可以使用范围。其中之一是 switch/case 块中的 case 语句。使用 switch/case 也可以通过使用跳转表实现进行编译器优化。下面这段程序你是否用过,是不是比不加break条理更清楚?

见 ./linux/drivers/scsi/sd.c

static int sd_major(int major_idx){switch (major_idx) {case 0:return SCSI_DISK0_MAJOR;case 1 ... 7:return SCSI_DISK1_MAJOR + major_idx - 1;case 8 ... 15:return SCSI_DISK8_MAJOR + major_idx - 8;default:BUG();return 0;/* shut up gcc */}}

   范围还支持更复杂的初始化。例如,以下代码指定数组中几个子范围的初始值。

int widths[] = { [0 ... 9] = 1, [10 ... 99] = 2, [100] = 3 };


      3、判断调用地址函数  

void * __builtin_return_address( unsigned int level );
这个函数的说明,英文的解释最清楚:This function returns the return address of the current function, or of one of its callers. The level argument is number of frames to scan up the call stack. A value of 0 yields the return address of the current function, a value of 1 yields the return address of the caller of the current function, and so forth. When inlining the expected behavior is that the function returns the address of the function that is returned to. To work around this behavior use the noinline function attribute.

一个简单例子:

#include <stdio.h> void func(){    printf("func @ %p\n", __builtin_return_address(0));    printf("funcall @ %p\n", __builtin_return_address(1));}void funcall(){     func();     printf("funcall really  %p\n", __builtin_return_address(0));} int main(){     funcall();     return 0;}
结果:
[root@localhost study]# ./builtinfunc @ 0x8048406funcall @ 0x8048429funcall really  0x8048429

    4、分支预测(优化性能)

      在 Linux 内核中最常用的优化技术之一是 __builtin_expect。在开发人员使用有条件代码时,常常知道最可能执行哪个分支,而哪个分支很少执行。如果编译器知道这种预测信息,就可以围绕最可能执行的分支生成最优的代码。
如下所示,__builtin_expect 的使用方法基于两个宏 likely 和 unlikely(见 ./linux/include/linux/compiler.h)

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

       通过使用 __builtin_expect,编译器可以做出符合提供的预测信息的指令选择决策。这使执行的代码尽可能接近实际情况。它还可以改进缓存和指令流水线。

       例如,如果一个条件标上了 “likely”,那么编译器可以把代码的 True 部分直接放在分支指令后面(这样就不需要执行分支指令)。通过分支指令访问条件结构的 False 部 分,这不是最优的方式,但是访问它的可能性不大。按照这种方式,代码对于最可能出现的情况是最优的。

__builtin_expect(!!(x), 1)  这个是说,如果x == 0,那么结果就是 0,如果x == 1, 那么结果就是1,使用了!!是为了让x转化成bool型的。
#include <stdio.h>#include <stdlib.h>#define likely(x)       __builtin_expect(!!(x), 1)#define unlikely(x)     __builtin_expect(!!(x), 0)int main(){    unsigned char x=1;    if(likely(x==1))    {       x++;    }    else    {        x--;    }    return x;}
 8048394:       55                      push   %ebp 8048395:       89 e5                   mov    %esp,%ebp 8048397:       83 ec 10                sub    $0x10,%esp 804839a:       c6 45 ff 01             movb   $0x1,-0x1(%ebp) 804839e:       80 7d ff 01             cmpb   $0x1,-0x1(%ebp) 80483a2:       0f 94 c0                sete   %al 80483a5:       0f b6 c0                movzbl %al,%eax 80483a8:       85 c0                   test   %eax,%eax 80483aa:       74 06                   je     80483b2 <main+0x1e>   //关键是这里 x!=1时才跳转, 80483ac:       80 45 ff 01             addb   $0x1,-0x1(%ebp) 80483b0:       eb 04                   jmp    80483b6 <main+0x22> 80483b2:       80 6d ff 01             subb   $0x1,-0x1(%ebp) 80483b6:       0f b6 45 ff             movzbl -0x1(%ebp),%eax 80483ba:       c9                      leave

这个函数的应用场景当你知道有一路分支执行的概率大于另外一个分支时,告诉编译器,在小概率事件的分支发生跳转。

       5、内存的预抓取

另一种重要的性能改进方法是把必需的数据缓存在接近处理器的地方。缓存可以显著减少访问数据花费的时间。大多数现代处理器都有三类内存:

  • 一级缓存通常支持单周期访问
  • 二级缓存支持两周期访问
  • 系统内存支持更长的访问时间
    三个参数说明:
  • 数据的地址
  • rw 参数,使用它指明预抓取数据是为了执行读操作,还是执行写操作
  • locality 参数,使用它指定在使用数据之后数据应该留在缓存中,还是应该清除

    为了尽可能减少访问延时并由此提高性能,最好把数据放在最近的内存中。手工执行这个任务称为预抓取。GCC 通过内置函数__builtin_prefetch 支持数据的手工预抓取。在需要数据之前,使用这个函数把数据放到缓存中。如下所示,__builtin_prefetch函数接收三个参数:

void __builtin_prefetch( const void *addr, int rw, int locality );
内核中的一个用例:
#ifndef ARCH_HAS_PREFETCH#define prefetch(x) __builtin_prefetch(x)#endifstatic inline void prefetch_range(void *addr, size_t len){#ifdef ARCH_HAS_PREFETCHchar *cp;char *end = addr + len;for (cp = addr; cp < end; cp += PREFETCH_STRIDE)prefetch(cp);#endif} 

6、变量属性

    除了本文前面讨论的函数属性之外,GCC 还为变量和类型定义提供了属性。最重要的属性之一是 aligned 属性,它用于在内存中实现对象对齐。除了对于性能很重要之外,某些设备或硬件配置也需要对象对齐。aligned 属性有一个参数,它指定所需的对齐类型。

(见 ./linux/arch/i386/mm/init.c)。在需要页面对齐时,定义 PAGE_SIZE 对象

char __nosavedata swsusp_pg_dir[PAGE_SIZE]__attribute__ ((aligned (PAGE_SIZE)));
总结:

GNC 还有其它很好的扩展用例,这些一般只常见与内核之中,在代码中如果能充分运用,对程序性能也是一个提高。

下面是一个综合的例子(详见参考博客中):

#include <stdio.h> //typeof可以获得变量的类型,这相当于高级语言的反射//typeof不仅支持基本类型,也支持函数指针、结构体和C++对象//下面的宏可保证无副作用//min(++a, ++b)同样能正常工作#define min_t(type, x, y) \( {type __x = (x); type __y = (y); __x < __y ? __x : __y;} ) #define min(x, y) \({const typeof(x) _x = x;   \ const typeof(y) _y = y; \ (void) (&_x == &_y);    \ _x < _y? _x : _y;}) //下面这个宏定义于Linux Kernel 2.6.34中的//include/linux/typecheck.h头文件中//注意这个宏返回值始终为1//int a; float b;//"&a == &b"会得到一个编译警告,该宏的作用仅在于此//因为C++比C有着更加严格的类型控制//如果使用g++,"&a == &b"会得到一个编译错误//error: comparison between distinct pointer types ‘int*’ and ‘float*’ lacks a cast#define typecheck(type,x) \({type __dummy; \typeof(x) __dummy2; \(void)(&__dummy == &__dummy2); \1; \}) #define COUNT 10 //GCC提供了一些内建函数//__builtin_return_address用于函数的返回地址void func(){printf("func @ %p\n", __builtin_return_address(0));printf("main @ %p\n", __builtin_return_address(1));} int sum(int a, int b){return a + b;} typedef struct stu{char name[20];int age;} student; int main(){//__FILE__, __LINE__是标准特性//__FUNCTION__是GNU C添加的,在调试程序时有时候特别有用printf("in function: %s of %s, %d line\n", __FUNCTION__, __FILE__, __LINE__); const int count1 = 2; //__builtin_constant_p用于判断变量是否为常量if(__builtin_constant_p(COUNT)){printf("yes, it is a compile constant var\n");}else{printf("no, it is not a compile constant var\n");} func(); printf("main @ %p really\n", __builtin_return_address(0));//结构体初始化//标准C使用: .name = "Ecy Fu"student stu2 ={name : "Ecy Fu",age : 25,}; printf("the name of stu2: %s\n", stu2.name); int a = 3, b = 4;float c = 4.5;//第一种方法使用麻烦if(min_t(int, a, b))printf("a < b is true\n");min_t(int, b, c);   //no warning    //gcc_ext2.c:117: warning: comparison of distinct pointer types lacks a castmin(b, c); typeof(int(*)(int, int)) pfunc; typeof(pfunc) pfunc2; pfunc2 = ∑ printf("a+b=%d\n", pfunc2(a, b)); //'({})'的使用,注意最后面的分号,否则通不过编译int x = ({a = 3 + 4; 1;});  printf("a = %d, x = %d\n", a, x);printf("size of long: %d\n", sizeof(3ul));  //unsigned long//压缩对齐,这样就不会填充任何字节//大小为1+8+1+4+2=16typedef struct{char a;double b;char c;int d;short e;}__attribute__((packed)) node1;//GCC默认对齐到4,所以大小为4+8+4+4+4=24typedef struct{char a;double b;char c;int d;short e;}node2; //这个__attribute__((aligned(8)))似乎并不起作用typedef struct{char a;double b;char c;int d;short e;}__attribute__((aligned(8))) node3;    printf("size of node1 is : %d\n", sizeof(node1));printf("size of node2 is : %d\n", sizeof(node2));printf("size of node3 is : %d\n", sizeof(node3)); return 0;}
结果:
[root@localhost study]# ./builtinin function: main of builtin.c, 57 lineyes, it is a compile constant varfunc @ 0x8048481main £À 0xa6dce6main @ 0xa6dce6 reallythe name of stu2: Ecy Fua < b is truea+b=7a = 7, x = 1size of long: 4size of node1 is : 16size of node2 is : 24size of node3 is : 24

参考:http://www.ibm.com/developerworks/cn/linux/l-gcc-hacks/    --M. Tim Jones 大牛的文章

           http://rdc.taobao.com/blog/cs/?p=1675   --淘宝核心系统博客

           http://www.fuzhijie.me/?p=242   --绚丽也尘埃

PS:鸣谢 参考博客作者!!

原创粉丝点击