全部博文(43)
分类: LINUX
2011-08-01 12:56:23
3) 非常重要的strict aliasing规则
假设有人问你 (i++)+(++i)+(++i++)结果是多少,你也许会直接回答这和编译器实现有关。不过如果有人问你
int abc()
{
short a[2];
int *b = (int *)a;
a[0]=0x1111;
a[1]=0x1111;
*b = 0x22222222;
printf("%x %x\n", a[0], a[1]);
return 0;
}
这个函数在单板上运行输出结果是多少你可能就毫不犹豫的说是2222 2222了。
事
实上,这段程序和(i++)+(++i)+(++i++)前面一样是不一定的,它的结果与编译器实现、编译参数有关。不信的话你找个arm的单板(别的平
台我没有试过),用ccarm -c -O2 参数编译得到的结果就是1111 1111。如果用参数ccarm -c
-O1编译得到的结果就是2222
2222。造成两种结果的参数不是-O2和-O1而是一个叫strict-aliasing的编译参数。这个参数在-O2优化的时候默认是打开的。刚才两
种不同结果是-fstrict-aliasing和-fno-strict-aliasing造成的。也就是说-O2结果就是1111 1111,-O2
-fno-strict-aliasing结果就是程序员预期的2222 2222。
造成这个现象的原因是-fstrict-aliasing的时候编译器会假设不同类型的指针指向的内存不会重叠来进行优化。
以
上面的例子为例分析,由于a和b类型不同,当打开-fstrict-aliasing,编译器给*b赋值的时候就认为不会影响到a数组里面的值。编译器在
优化的时候分析:既然a数组的值在赋值后没被修改,下一次读取a数组值的时候就没必要再从内存里读取了。因此printf这一行在取a[0],a[1]的
时候就被优化成了从寄存器取值。结果打印出来的是1111 1111。
而如果使用了-fno-strict-aliasing,则编译器认为任何
指针都可能指向同一个内存区域。因此对*b赋值,编译器认为有可能会影响a里面的值了。所以编译器给printf那一行传递参数的时候就认为寄存器里的
a[0],a[1]值已经不一定正确了,只有内存里的才可靠,于是只能老老实实从栈里取值了。
以上的分析是在arm平台上做的。我在x86下也做
了比较,但是两种参数编译出来结果都是2222
2222。主要是因为x86下通用寄存器数量很少,优化的时候不一定总能把变量放在寄存器里,所以这个例子影响不了x86。但在研发支撑体系上有人曾经提
出过-O2生成的代码有问题,就是因为strict aliasing影响的。
2007-10-08 gcc编译优化问题请教,牛人请进!
出现问题的代码如下:
XXXX_UINT32 xxxx_map_get_free
(
XXXX_MAP_PTR IO_map_p
)
{
XXXX_VOID_PTR cur_data_p;
XXXX_BUFF_PTR buff_p;
buff_p = IO_map_p->buff_p;
cur_data_p = buff_p->free_data_p;
buff_p->free_data_p = *(XXXX_VOID_PTR*)cur_data_p;
*(XXXX_UINT32*)cur_data_p = 0;
buff_p->used ++;
return XXXX_SUCCESS;
}
知道了这个特性之后我们写代码的时候就需要特别注意不要偷懒用强制指针转化去给buf赋值。上面那个程序安全的版本是:
int abc()
{
union
{
short a[2];
int b;
}U;
U.a[0]=0x1111;
U.a[1]=0x1111;
U.b = 0x22222222;
printf("%x %x\n", U.a[0], U.a[1]);
return 0;
}
这样无论怎么都不会出错了。
到这里你也许会问,为何要有这个特性,既然-fno-strict-aliasing比较安全,那干脆所有地方都这么用算了。实际上出现-fstrict-aliasing是为了提供更好的优化效果。比如下面的程序:
void sum(int *array, short * num, int n)
{
int i = 0;
for (i = 0;i < n;i++)
{
array[i] += (int)num[0];
}
}
如果使用-fno-strict-aliasing参数编译,编译器认为num和array有可能指向同一片区域。由于编译器认为给array[i]赋值有可能会改变num[0],所以循环内部num[0]的值每次都是从内存里取的。
而如果使用-fstrict-aliasing参数编译。编译器则认为num和array不会互相影响。这样在循环外部就把num[0]的值读取到寄存器中。循环内部每次都是寄存器的值去累加。减少了读取内存的次数,从而优化了速度。
要是一个函数内指针、数组数量众多,编译器进行优化后行为就很可能会与程序员期望的。同时编译参数是针对整个文件起作用的,无法针对单个指针变量和单个函
数。为了明确告诉编译器究竟指针所指内存是否会互相覆盖,在C99标准里引入了新的关键字restrict。restrict使用的地方基本和const
差不多。它指明此指针指向的内存仅仅会被这个指针来修改,其他的指针不会修改这部分内存。这样,明确告诉编译器之后,编译器就可以生成既符合程序员期望又
高效的代码了。不过Tornado2.2里所带的gcc版本相当老,对C99标准支持非常差,不支持restrict关键字,在这里就不多说了。更详细的
可以参考C99标准。建议在编译命令行加上-fno-strict-aliasing,虽然优化效果会打折扣,但是安全。
4) 怎样查看预处理器默认定义的宏
不同版本的gcc默认定义的宏是不太一样的。有时候为了兼容不同平台的gcc可能希望在代码里用编译宏区分开。这样就想知道编译器与处理器默认定义了哪些宏可以提供给我们使用。可以建立一个空文件然后使用下面的命令
ccsimpc -dM -E emptyfilename
或者
ccsimpc -dM -E - < NUL
可以得到输出
#define _stdcall __attribute__((__stdcall__))
#define CPU SIMNT
#define __i386__ 1
#define _X86_ 1
#define __i386 1
#define ___stdcall__ __attribute__((__stdcall__))
#define __GNUC_MINOR__ 96
#define __declspec(x) __attribute__((x))
#define __vxworks 1
#define i386 1
#define __stdcall __attribute__((__stdcall__))
#define __GNUC__ 2
#define __cdecl __attribute__((__cdecl__))
#define __STDC__ 1
#define ___stdcall __attribute__((__stdcall__))
ccarm -dM -E filename
或者
ccarm -dM -E - < NUL
可以得到输出
#define __arm__ 1
#define __ARM_ARCH_4__ 1
#define arm 1
#define __GNUC__ 2
#define __APCS_32__ 1
#define __arm_elf__ 1
#define __arm_elf 1
#define __vxworks 1
#define __CHAR_UNSIGNED__ 1
#define __ARMEL__ 1
#define __ELF__ 1
#define arm_elf 1
#define __GNUC_MINOR__ 9
#define CPU ARMSA110