缓存对齐与字节对齐_gcc 默认cacheline对齐吗-程序员宅基地

技术标签: linux操作系统  C++开发  oracle转载博文未看  windows操作系统  

缓存对齐与字节对齐  


一. Cache

【指的是CPU和内存间的那个高速缓存存储器,不是内存里一块做缓存用的区域】

Cache一般来说,需要关心以下几个方面

1)Cache hierarchy

Cache的层次,一般有L1, L2, L3 (L是level的意思)的cache。通常来说L1,L2是集成  在CPU里面的(可以称之为On-chip cache),而L3是放在CPU外面(可以称之为Off-chip cache)。当然这个不是绝对的,不同CPU的做法可能会不太一样。这里面应该还需要加上register,虽然register不是cache,但是把数据放到register里面是能够提高性能的。

2)Cache size

Cache的容量决定了有多少代码和数据可以放到Cache里面,有了Cache才有了竞争,才有了替换,才有了优化的空间。如果一个程序的热点(hotspot)已经完全填充了整Cache,那么再从Cache角度考虑优化就是白费力气了,巧妇难为无米之炊。我们优化程序的目标是把程序尽可能放到Cache里面,但是把程序写到能够占满整个Cache还是有一定难度的,这么大的一个Code path,相应的代码得有多少,代码逻辑肯定是相当的复杂(基本上是不可能,至少我没有见过)。

3)Cache line size

CPU从内存load数据是一次一个cache line;往内存里面写也是一次一个cache line,所以一个cache line里面的数据最好是读写分开,否则就会相互影响。

4)Cache associative

Cache的关联。有全关联(full associative),内存可以映射到任意一个Cache line;也有N-way关联,这个就是一个哈希表的结构,N就是冲突链的长度,超过了N,就需要替换。

5)Cache type

有I-cache(指令cache),D-cache(数据cache),TLB(MMU的cache),每一种又有L1,L2等等,有区分指令和数据的cache,也有不区分指令和数据的cache。

二. 代码层次的优化

1) 字节 alignment (字节对齐)

要理解字节对齐,首先得理解系统内存的组织结构. 把1个内存单元称为1个字节,字节再组成字,在8086时代,16位的机器中1字=2个字节=16bit,而80386以后的32位系统中,1字=4个字节。大多数计算机指令都是对字进行操作,如将两字相加等。也就是说,32位CPU的寄存器为32位,导致指令的操作对象是32位字;16位CPU的寄存器为16位,移动、加、减等指令的操作对象也是16位字。由于指令的原因,内存的寻址也同样是按字进行操作,在16位系统中,如果你访问的只是低8位,内存寻址还是按16位进行,然后再根据A0地址线选择低8位还是高8位,这一过程成为一次内存读(写),在16位系统中,如果读取一个32位数,要花费两个内存读周期(先读低16,再读高16)。同理32位CPU的内存寻址按4个单元进行。

为了达到高效的目的,在16位系统中,变量存储的起始地址是2的倍数,32位系统中,变量存储的起始地址是4的倍数,而这些工作都是由编译器来完成的。下面举个例子来说明这个问题。如下图所示: 缓存对齐与字节对齐 - CR7 - CR7的博客  

    上图是16位系统的内存布局图,深蓝色表示变量覆盖的内存范围,假设变量的大小为2个字节,变量的起始物理内存地址为0000H时,访问这个变量时,只需要一次内存的读写。然而,当变量的内存起始地址为0001H时,cpu将耗费两次读周期进行变量访问,具体过程如下:为了访问变量的低8位,cpu将通过寻址访问起始地址为0000H所在的字,然后找到当前字的高8位;随后cpu再访问0002H所处字的低8位,此低8位就是变量的高8位,这样经过cpu的拼装变量的访问就结束了,可见,需要经过两次读周期才能正确访问变量的值,效率是前者的1/2。

__attribute__((aligned(n)))表示所定义的变量为n字节对齐; 

字节对齐的细节和编译器实现相关,但一般而言,满足三个准则:
1) (结构体)变量的首地址能够被其(最宽)基本类型成员的大小所整除;
2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。

__attribute__ ((packed)) 的作用就是告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐,是GCC特有的语法。这个功能是跟操作系统没关系,跟编译器有关,gcc编译器不是紧凑模式的.例如:

__attribute__ ((packed)) 的作用就是告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐,是GCC特有的语法。

__attribute__((aligned(n)))表示所定义的变量为n字节对齐; 

struct B{ char b;int a;short c;}; (默认4字节对齐)

这时候同样是总共7个字节的变量,但是sizeof(struct B)的值却是12。

下面我们使用预编译指令__attribute__((aligned(n)))来告诉编译器,使用我们指定的对齐值来取代缺省的:

struct C{char b;int a;short c;}; __attribute__((aligned(2)))

这时候同样是总共7个字节的变量,但是sizeof(struct B)的值却是8

struct D{ char b;int a;short c;}; __attribute__ ((packed))

       sizeof(struct C)值是8,sizeof(struct D)值为7。

字节对齐的细节和编译器实现相关,但一般而言,满足三个准则:
1) (结构体)变量的首地址能够被其(最宽)基本类型成员的大小所整除;
2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。

2) Cache line alignment (cache对齐)

数据跨越两个cache line,就意味着两次load或者两次store。如果数据结构是cache line对齐的,  就有可能减少一次读写。数据结构的首地址cache line对齐,意味着可能有内存浪费(特别是  数组这样连续分配的数据结构),所以需要在空间和时间两方面权衡。

对于普通代码,内存边界对齐也是有好处的,可以降低高速缓存(Cache)和内存交换数据的次数。 主要问题是在于Cache本身是分成很多Cache-Line,每条Cache-Line具有一定的长度,比如一般来说L1 Cache每条Cache Line长度在32个字节或64个字节;而L2的会更大,比如64个字节或128个字节。用户每次访问地址空间中一个变量,如果不在Cache当中,那么就需要从内存中先将数据调入Cache中.
       比如现在有个变量 int x;占用4个字节,它的起始地址是0x1234567F;那么它占用的内存范围就在0x1234567F-0x12345682之间。如果现在Cache Line长度为32个字节,那么每次内存同Cache进行数据交换时,都必须取起始地址时32(0x20)倍数的内存位置开始的一段长度为32的内存同Cache Line进行交换.   比如0x1234567F落在范围0x12345660~0x1234567F上,但是0x12345680~0x12345682落在范围 0x12345680~0x1234569F上,也就是说,为了将4个字节的整数变量0x1234567F~0x12345682装入Cache,我们必 须调入两条Cache Line的数据。但是如果int x的起始地址按4的倍数对齐,比如是 0x1234567C~0x1234567F,那么必然会落在一条Cache Line上,所以每次访问变量x就最多只需要装入一条Cache Line的数据了。比如现在一般的malloc()函数,返回的内存地址会已经是8字节对齐的,这个就是为了能够让大部分程序有更好的性能。

1.  __attribute__((aligned(cache_line)))对齐实现;

    struct syn_str { ints_variable; };__attribute__((aligned(cache_line)));

2. 算法实现
  
   引子
    int a; 
    int size = 8;        <----> 1000(bin)
    计算a以size为倍数的下界数:

  就让这个数(要计算的这个数)表示成二进制时,最后三位为0就可以达到这个目标。只要下面这个数与a进行"与运算"就可以了:
  11111111 11111111 11111111 11111000
  而上面这个数实际下就是 ~(size - 1),可以将该数称为size的对齐掩码size_mask.
   计算a以size为倍数的上下界数:
  #define alignment_down(a, size) (a & (~(size-1)) )
  #define alignment_up(a, size)   ((a+size-1) & (~ (size-1)))
  注: 上界数的计算方法,如果要求出比a大的是不是需要加上8就可以了?可是如果a本身就是8的倍数,这样加8不就错了吗,所以在a基础上加上(size - 1), 然后与size的对齐掩码进行与运算.
  例如:
  a=0, size=8, 则alignment_down(a,size)=0, alignment_up(a,size)=0.
  a=6, size=8, 则alignment_down(a,size)=0, alignment_up(a,size)=8.
  a=8, size=8, 则alignment_down(a,size)=8, alignment_up(a,size)=8.
  a=14, size=8,则alignment_down(a,size)=8, alignment_up(a,size)=16.
  注:size应当为2的n次方, 即2, 4, 8, 16, 32, 64, 128, 256, 1024, 2048, 4096 ...
  
   实现例子:

    struct syn_str { int s_variable; };

    void *p = malloc ( sizeof (struct syn_str) + cache_line );

    syn_str *align_p=(syn_str*)((((int)p)+(cache_line-1))&~(cache_line-1);

3) Branch prediction (分支预测)

代码在内存里面是顺序排列的。对于分支程序来说,如果分支语句之后的代码有更大的执行几率,  那么就可以减少跳转,一般CPU都有指令预取功能,这样可以提高指令预取命中的几率。分支预测  用的就是likely/unlikely这样的宏,一般需要编译器的支持,这样做是静态的分支预测。现在也有  很多CPU支持在CPU内部保存执行过的分支指令的结果(分支指令的cache),所以静态的分支预测  就没有太多的意义。如果分支是有意义的,那么说明任何分支都会执行到,所以在特定情况下,静态 分支预测的结果并没有多好,而且likely/unlikely对代码有很大的侵害(影响可读性),所以一般不  推荐使用这个方法.

if(likely(value)) 等价于 if(value)

if(unlikely(value)) 也等价于 if(value)

也就是说 likely() 和 unlikely() 从阅读和理解代码的角度来看,是一样的!!!


这两个宏在内核中定义如下:

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

__builtin_expect() 是 GCC (version >= 2.96)提供给程序员使用的,目的是将“分支转移”的信息提供给编译器,这样编译器可以对代码进行优化,以减少指令跳转带来的性能下降。

__builtin_expect((x),1) 表示 x 的值为真的可能性更大;
__builtin_expect((x),0) 表示 x 的值为假的可能性更大。

也就是说,使用 likely() ,执行 if 后面的语句 的机会更大,使用unlikely(),执行else 后面的语句的机会更大。
例如下面这段代码,作者就认为 prev 不等于 next 的可能性更大,

if (likely(prev != next)) {
       next->timestamp = now;
        ...
} else {
        ...;
}
通过这种方式,编译器在编译过程中,会将可能性更大的代码紧跟着起面的代码,从而减少指令跳转带来的性能上的下降。

下面以两个例子来加深这种理解:

第一个例子: example1.c

int testfun(int x)
{
   if(__builtin_expect(x, 0)) {
         ^^^--- We instruct the compiler, "else" block is more probable
     x = 5;
     x = x * x;
   } else {
       x = 6;
   }
   return x;
}
在这个例子中,我们认为 x 为0的可能性更大

编译以后,通过 objdump 来观察汇编指令,在我的 2.4 内核机器上,结果如下:

# gcc -O2 -c example1.c
# objdump -d example1.o

Disassembly of section .text:

00000000 <testfun>:
   0:   55             push   %ebp
   1:   89 e5            mov    %esp,%ebp
   3:   8b 45 08          mov    0x8(%ebp),%eax
   6:   85 c0            test   %eax,%eax
   8:   75 07            jne    11 <testfun+0x11>
   a:   b8 06 00 00 00        mov    $0x6,%eax
   f:   c9              leave
    10:   c3              ret
    11:   b8 19 00 00 00        mov    $0x19,%eax
    16:   eb f7            jmp    f <testfun+0xf>


可以看到,编译器使用的是 jne (不相等跳转)指令,并且 else block 中的代码紧跟在后面。

8:   75 07              jne    11 <testfun+0x11>
a:   b8 06 00 00 00          mov    $0x6,%eax


第二个例子: example2.c

int testfun(int x)
{
  if(__builtin_expect(x, 1)) {
           ^^^ --- We instruct the compiler, "if" block is more probable
       x = 5;
       x = x * x;
  } else {
       x = 6;
  }
       return x;
}
在这个例子中,我们认为 x 不为 0 的可能性更大

编译以后,通过 objdump 来观察汇编指令,在我2.4内核机器上,结果如下:

# gcc -O2 -c example2.c
# objdump -d example2.o


Disassembly of section .text:

00000000 <testfun>:
   0:   55                 push   %ebp
   1:   89 e5                mov    %esp,%ebp
   3:   8b 45 08              mov    0x8(%ebp),%eax
   6:   85 c0                test   %eax,%eax
   8:   74 07                je     11 <testfun+0x11>
   a:   b8 19 00 00 00             mov    $0x19,%eax
   f:   c9                 leave
10:   c3                   ret
11:   b8 06 00 00 00                 mov    $0x6,%eax
16:   eb f7                  jmp    f <testfun+0xf>


这次编译器使用的是 je (相等跳转)指令,并且 if block 中的代码紧跟在后面。

   8:   74 07                je     11 <testfun+0x11>
   a:   b8 19 00 00 00             mov    $0x19,%eax

 

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/haiross/article/details/38899019

智能推荐

MqttAndroidClient使用-程序员宅基地

文章浏览阅读4.3k次。MQTT是一个由IBM主导开发的物联网传输协议,它被设计用于轻量级的发布/订阅式消息传输,旨在为低带宽和不稳定的网络环境中的物联网设备提供可靠的网络服务。应项目要求使用MQTT协议实现客户端与服务端通信。_mqttandroidclient

anaconda哪个版本是 python3.6_windows10(64位)Anaconda3+Python3.6搭建Tensorflow(cpu版本)及keras...-程序员宅基地

文章浏览阅读1.2k次。1.本来电脑安装的是anaconda3 5.3.1,但安装的python版本是3.7,后来卸载了安装anaconda3 4.4.0,这个版本是3.62.打开Anaconda Promptconda --version //检查Anaconda是否成功安装(如果成功会显示版本号)conda update conda //更新conda版本conda create -n tensorflow p..._anaconda3+对应python3.6

第十一届蓝桥杯单片机省赛题目解析_蓝桥杯单片机考什么-程序员宅基地

文章浏览阅读1.1w次,点赞18次,收藏124次。第十一届蓝桥杯单片机省赛题目解析这次题目照样还是LED、数码管、按键三大模块,还有模拟电压输入(ADC)、AT24C02(EEPROM),都是常见的外设,且之前题目出现过的,所以不难,考的都是基本功。1 数码管显示数码管主要显示获取的电压、计数和参数设置三个界面,不过要注意上电之后就要显示电压数据界面。2 LED当电压小于设置的参数时5s后L1亮,这个还是很好做的。当计数为奇数时L2亮,计数的话,我是先判断电压是..._蓝桥杯单片机考什么

LightOJ - 1067 Combinations(快速幂+逆元)_快速幂求逆元 在线oj-程序员宅基地

文章浏览阅读484次。DescriptionGiven n differentobjects, you want to take k of them. How many ways to can do it? For example, saythere are 4 items; you want to take 2 of them. So, you can do it 6 ways. Take 1, _快速幂求逆元 在线oj

Fairseq学习日记:注定麻烦的旅程_final_lr_scale-程序员宅基地

文章浏览阅读6.6k次,点赞10次,收藏23次。现在开头:Fairseq是一个正在快速迭代的产品,而且是开源的!这不是表扬,这意味着三件事情:1.他没有文档!所有框架代码都没有任何注释,包括函数docstring都没有2.他没有经过有效测试,估计是抢时间吧!即使是官网Readme里的例子也是无法跑起来的!3.他是一个框架,而且是一个非常不Pythonic的框架,充斥着inline/包装器/莫名其妙的语法。虽然这三点决定他真的对不住Facebook的金字招牌,但是作为一个学习者,总要把他运行起来,那么开始这场针对 FaceBOOK派“全_final_lr_scale

[Linux][Busybox]分享你不知道Top 命令参数_busybox top-程序员宅基地

文章浏览阅读5.1k次。目录摘要:基本操作与命令介绍:进入top后交互一点点新的操作Author: Keivn.Xu [email protected]摘要: 玩过Linux一定使用过busybox top命令,但下面的操作方法,你不一定有见过。基本操作与命令介绍:console:/ $ busybox top -help top: invalid optio..._busybox top

随便推点

[BAT]Win7下可用于bat文件中的call调用-程序员宅基地

文章浏览阅读982次。全部已在本机验证,可能根据用户安装的系统功能不同而有些命令无效。设备管理器 hdwwiz.cpl 或devmgmt.msc添加或删除程序 appwiz.cpl管理工具 control admintoolsBluetooth文件传送向导 fsquirt计算器 calc证书管理控制台 certmgr.msc字符映..._window bat call 环境

计算机视觉——相机标定-程序员宅基地

文章浏览阅读1.1w次,点赞8次,收藏82次。相机标定_相机标定

js获取年月日 时分秒的日期格式_js 时分秒 时间格式-程序员宅基地

文章浏览阅读1.1k次。const date = new Date()const year = date.getFullYear()const month = date.getMonth() + 1 >= 10 ? date.getMonth() + 1 : `0${date.getMonth() + 1}`const day = date.getDate() >= 10 ? date.getDate() : `0${date.getDate()}`const hours = date.getHours() ._js 时分秒 时间格式

【模拟】Oracle重建索引_oracle 重建索引-程序员宅基地

文章浏览阅读2.2k次。摘要:简述重建索引的情况及重建索引_oracle 重建索引

keil迁移到cubeIDE_cubeide生成代码 keil打开-程序员宅基地

文章浏览阅读2.7k次。以stm32f105rct6为例子,keil迁移到cubeIDE_cubeide生成代码 keil打开

语音处理:Python实现dBFS刻度和采样值相互转换_dbfs 频域-程序员宅基地

文章浏览阅读781次。以对数域常用的dBFS刻度为例,支持主流音频信号位深:整型16/24/32位和浮点32位,编写Python实现对数域和采样值单位互换功能_dbfs 频域