1.
一直以来对数据对齐都不明白,直到看了这篇文章后才能说是有点感觉,结合代码将理解写下来,以备将来回头加深理解或者是修正错误的认知。
http://www.searchtb.com/2013/04/performance_optimization_tips.html
代码如下,运行的环境是64位的linux机器,内核3.10,gcc 4.8.2
/************************************************************************* > File Name: test_align.c > Author: dingzhengsheng > Mail: [email protected] > Created Time: 2015年04月22日 星期三 15时49分08秒 ************************************************************************/ #include<stdio.h> #include<stdint.h> #include<time.h> #include<stdlib.h> #include <unistd.h> #include<string.h> #include<sys/time.h> #include<sys/resource.h> #define SECTOUSEC 1000000 #define ARR_LEN 10000000 #define OP | //#define DZSTEST #ifdef DZSTEST #pragma pack (push) #pragma pack (1) struct notalignstruct { char a; char b; char c; uint32_t d; }; #pragma pack (pop) struct alignstruct { char a; char b; char c; uint32_t d; }; #else struct notalignstruct { char a; uint32_t d; short int c; }; struct alignstruct { char a; short int c; uint32_t d; }; #endif void case_one(struct notalignstruct *array, uint32_t array_len, uint32_t *sum) { uint32_t value = 1; uint32_t i; for(i=0; i<array_len; i++) value = value OP array[i].d; *sum = *sum OP value; } void case_two(struct alignstruct *array, uint32_t array_len, uint32_t *sum) { uint32_t value = 1; uint32_t i; for(i=0; i<array_len; i++) value = value OP array[i].d; *sum = *sum OP value; } void case_two_yh1(struct alignstruct *array, uint32_t array_len, uint32_t *sum) { uint32_t value = 1; uint32_t i; uint32_t length = array_len - (array_len&0x3); for(i=0; i<length; i+=4) { value = value OP array[i].d; value = value OP array[i+1].d; value = value OP array[i+2].d; value = value OP array[i+3].d; } for(; i<length; i+=4) value = value OP array[i].d; *sum = *sum OP value; } void case_two_yh2(struct alignstruct *array, uint32_t array_len, uint32_t *sum) { uint32_t value = 1; uint32_t i; uint32_t length = array_len - (array_len&0x3); uint32_t value1,value2; for(i=0; i<length; i+=4) { value1 = array[i].d OP array[i+1].d; value2 = array[i+2].d OP array[i+3].d; value = value1 OP value2; } for(; i<length; i+=4) value = value OP array[i].d; *sum = *sum OP value; } void case_two_yh3(struct alignstruct *array, uint32_t array_len, uint32_t *sum) { register uint32_t value = 1; register uint32_t i; uint32_t length = array_len - (array_len&0x3); uint32_t value1,value2; for(i=0; i<length; i+=4) { value1 = array[i].d OP array[i+1].d; value2 = array[i+2].d OP array[i+3].d; value = value1 OP value2; } for(; i<length; i+=4) value = value OP array[i].d; *sum = *sum OP value; } long int get_diff_time(struct timeval *tv1, struct timeval *tv2) { long int n; n = tv2->tv_sec*SECTOUSEC + tv2->tv_usec - tv1->tv_sec*SECTOUSEC - tv1->tv_usec; return n; } void main() { void *p; struct notalignstruct *array = malloc(sizeof(struct notalignstruct) * ARR_LEN); struct alignstruct *arr = malloc(sizeof(struct alignstruct) * ARR_LEN); uint32_t sum; struct timeval tv1,tv2; time_t timep; struct timezone tz; time(&timep); printf("数据对齐的影响比较:\n"); gettimeofday(&tv1,NULL); case_one(array, ARR_LEN, &sum); gettimeofday(&tv2,NULL); printf("sizeof(not)=%u : %ld\n",sizeof(struct notalignstruct),get_diff_time(&tv1, &tv2)); gettimeofday(&tv1,NULL); case_two(arr, ARR_LEN, &sum); gettimeofday(&tv2,NULL); printf("sizeof(not)=%u : %ld\n",sizeof(struct alignstruct),get_diff_time(&tv1, &tv2)); printf("代码优化的性能比较:\n"); gettimeofday(&tv1,NULL); case_two_yh1(arr, ARR_LEN, &sum); gettimeofday(&tv2,NULL); printf("减少cpu循环分支预测数sizeof(align)=%u : %ld\n",sizeof(struct alignstruct),get_diff_time(&tv1, &tv2)); gettimeofday(&tv1,NULL); case_two_yh2(arr, ARR_LEN, &sum); gettimeofday(&tv2,NULL); printf("提高CPU指令流水线并行计算sizeof(align)=%u : %ld\n",sizeof(struct alignstruct),get_diff_time(&tv1, &tv2)); gettimeofday(&tv1,NULL); case_two_yh3(arr, ARR_LEN, &sum); gettimeofday(&tv2,NULL); printf("将高频率的数据放入寄存器sizeof(align)=%u : %ld\n",sizeof(struct alignstruct),get_diff_time(&tv1, &tv2)); }
初始是想比较前两种不同的对齐方式的性能差距
#pragma pack (push)
#pragma pack (1)
struct notalignstruct
{
char a;
char b;
char c;
uint32_t d;
};
#pragma pack (pop)
struct alignstruct
{
char a;
char b;
char c;
uint32_t d;
};
原博文已经说的很清楚了,但是有一点我还有所怀疑就是如果编译器对notalignstruct数组分配的内存的起始地址不是2的N次方怎么办,那么对结构体中的变量d的访问是否仍然会导致两次取内存了,还是说编译器会自动解决这个问题吗?
运行的结果是
数据对齐的影响比较:
sizeof(not)=7 : 43064
sizeof(not)=8 : 38102
代码优化的性能比较:
减少cpu循环分支预测数sizeof(align)=8 : 26263
提高CPU指令流水线并行计算sizeof(align)=8 : 21551
将高频率的数据放入寄存器sizeof(align)=8 : 14229
差距还是挺大啊,然后再看这种情况:
struct notalignstruct
{
char a;
uint32_t d;
short int c;
};
struct alignstruct
{
char a;
short int c;
uint32_t d;
};
这个很明显,对齐的结果是前者会是12个字节的长度,后者是8个字节。程序运行前,我推测的结果是两者的速度应该是一致的,二者均按照4个字节对齐了。
但是结果还是出人意外:
数据对齐的影响比较:
sizeof(not)=12 : 40635
sizeof(not)=8 : 39052
代码优化的性能比较:
减少cpu循环分支预测数sizeof(align)=8 : 26475
提高CPU指令流水线并行计算sizeof(align)=8 : 21788
将高频率的数据放入寄存器sizeof(align)=8 : 14326
[[email protected] bug_test]$ ./test
数据对齐的影响比较:
sizeof(not)=12 : 40594
sizeof(not)=8 : 38828
代码优化的性能比较:
减少cpu循环分支预测数sizeof(align)=8 : 26262
提高CPU指令流水线并行计算sizeof(align)=8 : 21628
将高频率的数据放入寄存器sizeof(align)=8 : 14332
前者的速度小于后者,难道是因为前者占用的内存空间更大,实际物理内存跨页,碎片化更厉害导致访问更慢吗?
然后针对后面这两个结构体的计算函数case_two的优化就有意思了,博文中的这张图片就能说得很清楚了。实际上也很好理解,假如十个人连成一排从货车上已个个箱子里取一包包的货物传递到生产线上,而流水线的生产速度要大于工人传递的速度,而旁边有个检测师站在生成线的头上,会对每个箱子里的头一包货物拦住检查一下账本,将已收货物总数加1,并看是否在要用到的货物限额之内,如果检查到这包货物质量优良,那么整箱的货物都免检通过,否则就得丢掉这包货物,同时10个人手头上拿到的货物也得丢掉,case_two的情况就很明显,一个箱子里里面就放了一包货物,等于每一个都得停下来检查,大家都得在他检查的时候等着。
在我们自己就已经知道这个限额很大,是4XN 再加上零碎的三两包就够了,而我们检查的时候完全可以一个箱子里多放4个,每次只在箱子里第一包货物时做技术检查就可以了。
那这样就快很多了,流水线变得更加流畅了:
sizeof(not)=8 : 38828
代码优化的性能比较:
减少cpu循环分支预测数sizeof(align)=8 : 26262
然后了,工人说包都很轻,能不能一次就拿两个包来传递,货物上得更快了,这样让流水线减少空转的时间:
提高CPU指令流水线并行计算sizeof(align)=8 : 21628
然后检测师说我也能更快一点,收包数就不记到账本上一条一条的加了,我心头默记这样不久更快了嘛,省得翻账本费时间:
将高频率的数据放入寄存器sizeof(align)=8 : 14332
补充:再修改了一下,将length也改成register变量,执行速度更快了,只需要13464。
2.
现在来总结上面的技术与原理:
数据对齐就不说了,分支预测问题,上述的例子for循环实际上并不能说明问题。我推测的是如果是1000000万次for循环,循环体内是if(do something)else (do something)这种情况下性能的影响会非常明显。现代的cpu的分支预测能力已经很强了,像这种for循环的判断,我觉得成功率应该是接近100%的,如果是if那种,且判断的条件时刻都在变得情况,分支预测的成功率就可能会降到50%左右吧。我不知道分支预测的原理与算法,我推想的是这就如同猜硬币的正反一样,可能cpu能够根据当前的风向,当前扔的力度来猜测,历史数据,提高了准确度。内核代码中的likely和unlikely就是程序员告诉编译器哪个分支预测的可能性更高,那么编译器就将可能性高的代码编译的指令放在判断之后,访问的更快。
至于指令的并行处理,我的理解是减少执行的机器代码,取数据的动作还是两次,但是计算的动作已经只需要做一次了。这个理解我自己也是模模糊糊的,等待以后理解更清楚后补充吧。
对寄存器的数据操作是最快的,依次是一级缓存,二级缓存,三级缓存,内存,硬盘。寄存器的速度和cpu的处理速度是同频率的,cpu操作起来毫无延迟,而缓存则要高上一个档次,内存再高上一个档次,最慢的当然就是硬盘了,如果从硬盘取数据到内存再访问,那就慢的没边了。程序中变量value,i,length变量都是放在栈上操作(有可能某一个是放在寄存器中,没看汇编代码,只做推测),那么速度就会比寄存器慢好多,但是寄存器就那么十几个,变量多的时候不能大家都想上,如果将频繁访问的数据放入寄存器中操作,很明显就会在整体上提高处理的速度。最后优化的结果只用了原始的case_one的1/3时间。