缓存对齐与字节对齐_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

智能推荐

FTP命令字和返回码_ftp 登录返回230-程序员宅基地

文章浏览阅读3.5k次,点赞2次,收藏13次。为了从FTP服务器下载文件,需要要实现一个简单的FTP客户端。FTP(文件传输协议) 是 TCP/IP 协议组中的应用层协议。FTP协议使用字符串格式命令字,每条命令都是一行字符串,以“\r\n”结尾。客户端发送格式是:命令+空格+参数+"\r\n"的格式服务器返回格式是以:状态码+空格+提示字符串+"\r\n"的格式,代码只要解析状态码就可以了。读写文件需要登陆服务器,特殊用..._ftp 登录返回230

centos7安装rabbitmq3.6.5_centos7 安装rabbitmq3.6.5-程序员宅基地

文章浏览阅读648次。前提:systemctl stop firewalld 关闭防火墙关闭selinux查看getenforce临时关闭setenforce 0永久关闭sed-i'/SELINUX/s/enforcing/disabled/'/etc/selinux/configselinux的三种模式enforcing:强制模式,SELinux 运作中,且已经正确的开始限制..._centos7 安装rabbitmq3.6.5

idea导入android工程,idea怎样导入Android studio 项目?-程序员宅基地

文章浏览阅读5.8k次。满意答案s55f2avsx2017.09.05采纳率:46%等级:12已帮助:5646人新版Android Studio/IntelliJ IDEA可以直接导入eclipse项目,不再推荐使用eclipse导出gradle的方式2启动Android Studio/IntelliJ IDEA,选择 import project3选择eclipse 项目4选择 create project f..._android studio 项目导入idea 看不懂安卓项目

浅谈AI大模型技术:概念、发展和应用_ai大模型应用开发-程序员宅基地

文章浏览阅读860次,点赞2次,收藏6次。AI大模型技术已经在自然语言处理、计算机视觉、多模态交互等领域取得了显著的进展和成果,同时也引发了一系列新的挑战和问题,如数据质量、计算效率、知识可解释性、安全可靠性等。城市运维涉及到多个方面,如交通管理、环境监测、公共安全、社会治理等,它们需要处理和分析大量的多模态数据,如图像、视频、语音、文本等,并根据不同的场景和需求,提供合适的决策和响应。知识搜索有多种形式,如语义搜索、对话搜索、图像搜索、视频搜索等,它们可以根据用户的输入和意图,从海量的数据源中检索出最相关的信息,并以友好的方式呈现给用户。_ai大模型应用开发

非常详细的阻抗测试基础知识_阻抗实部和虚部-程序员宅基地

文章浏览阅读8.2k次,点赞12次,收藏121次。为什么要测量阻抗呢?阻抗能代表什么?阻抗测量的注意事项... ...很多人可能会带着一系列的问题来阅读本文。不管是数字电路工程师还是射频工程师,都在关注各类器件的阻抗,本文非常值得一读。全文13000多字,认真读完大概需要2小时。一、阻抗测试基本概念阻抗定义:阻抗是元器件或电路对周期的交流信号的总的反作用。AC 交流测试信号 (幅度和频率)。包括实部和虚部。​图1 阻抗的定义阻抗是评测电路、元件以及制作元件材料的重要参数。那么什么是阻抗呢?让我们先来看一下阻抗的定义。首先阻抗是一个矢量。通常,阻抗是_阻抗实部和虚部

小学生python游戏编程arcade----基本知识1_arcade语言 like-程序员宅基地

文章浏览阅读955次。前面章节分享试用了pyzero,pygame但随着想增加更丰富的游戏内容,好多还要进行自己编写类,从今天开始解绍一个新的python游戏库arcade模块。通过此次的《连连看》游戏实现,让我对swing的相关知识有了进一步的了解,对java这门语言也有了比以前更深刻的认识。java的一些基本语法,比如数据类型、运算符、程序流程控制和数组等,理解更加透彻。java最核心的核心就是面向对象思想,对于这一个概念,终于悟到了一些。_arcade语言 like

随便推点

【增强版短视频去水印源码】去水印微信小程序+去水印软件源码_去水印机要增强版-程序员宅基地

文章浏览阅读1.1k次。源码简介与安装说明:2021增强版短视频去水印源码 去水印微信小程序源码网站 去水印软件源码安装环境(需要材料):备案域名–服务器安装宝塔-安装 Nginx 或者 Apachephp5.6 以上-安装 sg11 插件小程序已自带解析接口,支持全网主流短视频平台,搭建好了就能用注:接口是公益的,那么多人用解析慢是肯定的,前段和后端源码已经打包,上传服务器之后在配置文件修改数据库密码。然后输入自己的域名,进入后台,创建小程序,输入自己的小程序配置即可安装说明:上传源码,修改data/_去水印机要增强版

verilog进阶语法-触发器原语_fdre #(.init(1'b0) // initial value of register (1-程序员宅基地

文章浏览阅读557次。1. 触发器是FPGA存储数据的基本单元2. 触发器作为时序逻辑的基本元件,官方提供了丰富的配置方式,以适应各种可能的应用场景。_fdre #(.init(1'b0) // initial value of register (1'b0 or 1'b1) ) fdce_osc (

嵌入式面试/笔试C相关总结_嵌入式面试笔试c语言知识点-程序员宅基地

文章浏览阅读560次。本该是不同编译器结果不同,但是尝试了g++ msvc都是先计算c,再计算b,最后得到a+b+c是经过赋值以后的b和c参与计算而不是6。由上表可知,将q复制到p数组可以表示为:*p++=*q++,*优先级高,先取到对应q数组的值,然后两个++都是在后面,该行运算完后执行++。在电脑端编译完后会分为text data bss三种,其中text为可执行程序,data为初始化过的ro+rw变量,bss为未初始化或初始化为0变量。_嵌入式面试笔试c语言知识点

57 Things I've Learned Founding 3 Tech Companies_mature-程序员宅基地

文章浏览阅读2.3k次。57 Things I've Learned Founding 3 Tech CompaniesJason Goldberg, Betashop | Oct. 29, 2010, 1:29 PMI’ve been founding andhelping run techn_mature

一个脚本搞定文件合并去重,大数据处理,可以合并几个G以上的文件_python 超大文本合并-程序员宅基地

文章浏览阅读1.9k次。问题:先讲下需求,有若干个文本文件(txt或者csv文件等),每行代表一条数据,现在希望能合并成 1 个文本文件,且需要去除重复行。分析:一向奉行简单原则,如无必要,绝不复杂。如果数据量不大,那么如下两条命令就可以搞定合并:cat a.txt >> new.txtcat b.txt >> new.txt……去重:cat new...._python 超大文本合并

支付宝小程序iOS端过渡页DFLoadingPageRootController分析_类似支付宝页面过度加载页-程序员宅基地

文章浏览阅读489次。这个过渡页是第一次打开小程序展示的,点击某个小程序前把手机的开发者->network link conditioner->enable & very bad network 就会在停在此页。比如《支付宝运动》这个小程序先看这个类的.h可以看到它继承于DTViewController点击左上角返回的方法- (void)back;#import "DTViewController.h"#import "APBaseLoadingV..._类似支付宝页面过度加载页

推荐文章

热门文章

相关标签