CSAPP_06 存储器的层次结构

一、随机访问存储器(Random-Access Memory, RAM)

1.1 Static RAM

SRAM将每个bit位存储在一个双稳态(bitstable)存储器单元里面,每个存储单元需要6个晶体管来实现。关于双稳态结构,我们可以借助摆钟模型来理解。当摆钟倾斜到最左边或者最右边的时候它是稳定的,当摆钟位于中间位置是亚稳态的(metastable),任何细微的影响都会使它摆动到最左边或者最右边。正是由于SRAM具有双稳态的性质,只要有电它就会永远保持它的值。
在这里插入图片描述

1.2 Dynamic RAM

与SRAM相比,DRAM的原理是电容充电。对于DRAM每个bit位对应于一个晶体管和电容,当然,这个电容是非常小的。与SRAM不同,DRAM对电磁干扰是十分敏感的,当电容上的电压被扰乱之后,就永远无法恢复,暴露在光线下,也会导致电容电压发生改变。

虽然RSAM的存取要比DRAM快,并且SRAM对于光和电噪声不敏感,但是SRAM需要更多的晶体管造价成本也更加高昂。
在这里插入图片描述

1.3 传统的DRAM

如下图所示,DRAM芯片由16个超单元组成,每个单元包含8个bit位信息,通过这张图,我们可以看到,所有的超单元被组织成一个4*4的阵列,每个超单元可以通过类似坐标的方式(i,j)进行寻址,整个DRAM芯片通过地址引脚与数据引脚与内存控制器相连,简单来讲内存控制器主要用来管理内存。
在这里插入图片描述
例如,要从DRAM中读取超单元(2,1)。内存控制器会首先发送行地址2。DRAM的响应是将行2的整体内容复制到一个内部缓冲行当中。
在这里插入图片描述
接下来内存控制器发送列地址1到DRAM,DRAM的响应是从,行缓冲区复制出超单元(2,1)的8位,然后把它们发送给内存控制器
在这里插入图片描述
为什么要分两次发送,不是增加了访问时间吗?
这是因为设计人员将DRAM芯片设计成了2维度阵列,而不是芯片数组,这样的好处是可以降低地址引脚的数量,如果将图中16*8的DRAM用线性数组表示,那么地址就是0~15,为了实现寻址需要4个引脚,而二维阵列的方式只需要两个引脚就可以了。

1.4 内存模块

图中展示了一个内存模块的基本组成,这个内存模块一共使用了8个DRAM芯片,分别用编号0~7表示,每个芯片的大小是8M*8bit,也就是64Mbit,也就是8MB。每个超单元可以存储8bit也就是1字节的数据,那么对于8字节的数据就需要8个超单元来存储,不过这8个超单元并不在同一个DRAM芯片上而是平均分布在8个DRAM芯片上。其中DRAM0存储低8位,DRAM1存储下一个字节,以此类推。

如果要取出内存地址A处的一个字的时候,内存控制器将A转换成一个超单元地址(i, j)并将他们发送到内存模块。内存模块会将(i,j)广播到每个DRAM。作为响应每个内存模块输出它的(i,j)位置的内容,模块中的电路收集这些内容,并把它们合成一个64位的字,再返回给内存控制器。

在这里插入图片描述

1.5 新型的DRAM

为了跟上处理器的速度,市场上会定期推出新的DRAM,这些新的DRAM都是基于传统的DRAM单元,然后进行一些优化,例如我们经常在电脑配置清单中看到DDR3,DDR4,LPDDR等字样,其中DDR的全程是(Double Data-Rate Synchronous DRAM, DDR SDRAM),这里我们只需要知道同步比异步速度更快就好了。DDR2,DDR3,DDR4这些缩写中的数字表示不同代,例如4代的读写速度要快过3代,速度的提升主要依赖于提高预取缓冲区的位数,例如DDR4的预取缓冲区是16bit,DDR3的预取缓冲区是8bit。LPDDR(Lower Power)的功耗更低,不过DDR4的延迟更小。手机以及一些商务笔记本会采用LPDDR,所以LPDDR更适合在一些功耗敏感的设备上。

1.6 非易失性存储ROM

如果断电,DRAM和SRAM会丢失他们的信息,从这个意义上来说,它们是易失(volatile)的。非易失性(nonvolatile)存储,即使在断电后仍然能保存它们的信息,现在有很多非易失性存储器。因为历史原因,虽然ROM中有很多类型既可以读也可以写,但是他们整体上都被称为只读存储器(Read-Only Memory, ROM)。ROM是以他们能够被重新写入的次数,和写入所采取的方式来进行区分的。

PROM(Programmable ROM)只能被编程一次。可擦写可编程(Erasable Programmable ROM, EPROM)能够擦除和重新编程的次数可以达到10000次。存储在ROM设备中的程序通常被称为固件(firmware)。

1.7 访问内存

在这里插入图片描述
考虑当cpu执行加载操作movq A, %rax时会发生什么:

  • 首先CPU将A的地址放到系统总线上,I/O桥将信号传递到内存总线;
  • 接下来主存储感知到内存总线上的信号,从内存总线上获取到读取地址,将数据写入到内存总线;
  • I/O桥将内存总线信号翻译成系统总线信号,然后沿着系统总线传递。最后,CPU感觉到系统总线上的数据,从总线上读取数据,并存放到寄存器%rax中。

二、磁盘存储

2.1 磁盘的结构

磁盘是由盘片(platter)组成的。每个盘片有两个面或者称之表面(surface),表面覆盖着磁性记录材料。盘片中央有一个可以旋转的主轴(spindle),它使得盘片以固定的旋转速率(rotational rate)旋转,通常是5400~15000r/min(Revolution Per Minute,RPM)。
在这里插入图片描述
每个表面是由一组称为磁道(track)的同心圆组成。每个磁道被划分为一组扇区(sector),每个扇区大小为512字节,扇区之间由一些间隙(gap)分隔开,这些间隙中不存储数据为。间隙存储用来标识扇区的格式化位,不能用来存储数据。

在这里插入图片描述

2.2 磁盘的操作

磁盘通过读/写(read/write head)来读写存储在磁性表面的数据,而读写头连接在传动臂(actuator arm)的一端,通过传动臂可以在半径方向上移动读写头,将读写头固定在盘面的任何磁道上,这样的机械运动称为讯道(seek)。读写头垂直排列一致行动任何时候,读写头都位于同一个柱面上。
在这里插入图片描述

对于磁盘而言,1GB=10^9 bytes1TB=10^12 bytes

对于扇区的访问速度主要分为三部分:
T a c c e s s = T s e e k + T r o r a t i o n + T t r a n s f e r T_{access} = T_{seek}+T_{roration}+T_{transfer} Taccess=Tseek+Troration+Ttransfer

  • 寻道时间(Seek Time):为了读取某个扇区的内容,传动臂需要将读/写头移动到包含目标扇区的磁道上。传动臂移动所需要的时间称为寻道时间。寻道时间的长短取决于当前磁道与目标磁道之间的距离。现代驱动器中平局寻道时间 T a v g − s e e k T_{avg-seek} Tavgseek通常为3-9ms
  • 旋转时间(Rotation Time):接下来,等待目标扇区的第一个位旋转到读/写头下才能够读取数据。最坏的情况下,读写头刚好错过了目标扇区,因此必须等待磁盘转一整圈。所以最大旋转延迟是
    T m a x − r o t a t i o n = 1 ∗ 60 ∗ 1000 R P M T_{max-rotation} = \frac{1*60*1000} {RPM} Tmaxrotation=RPM1601000
    平均旋转时间avg-roration通常是最大旋转时间的一半。
  • 传送时间(Transfer Time):当目标扇区的第一个位位于读写头的下方的时候,驱动器就可以读取或者写该扇区的内容了。一个扇区的传送时间依赖于每个磁道扇区的数量和磁盘的旋转速度。一个扇区的平均传送时间可以粗略估计为:

T a v g − t r a n s f e r = 1 ∗ 60 ∗ 1000 R P M ∗ 1 平均扇区数量 T_{avg-transfer} = { \frac{1*60*1000} {RPM} } * \frac {1} {平均扇区数量} Tavgtransfer=RPM1601000平均扇区数量1

考虑一个参数如下的磁盘:

参数
旋转速度7200RPM
T a v g − s e e k T_{avg-seek} Tavgseek9ms
单条磁道平均扇区数量400

对于这个磁盘,平局旋转延迟为:

T a v g − r o r a t i o n = 1 2 ∗ 1 ∗ 60 ∗ 1000 7200 ≈ 4 m s T_{avg-roration} = { \frac {1} {2} } * {\frac{1*60*1000} {7200}} \approx 4ms Tavgroration=21720016010004ms

平局传送时间为:

T a v g − t r a n s f e r = 1 400 ∗ 1 ∗ 60 ∗ 1000 7200 ≈ 0.02 m s T_{avg-transfer} = { \frac {1} {400} } * {\frac{1*60*1000} {7200}} \approx 0.02ms Tavgtransfer=4001720016010000.02ms

单个扇区的平均访问时间为:

T a c c e s s = T s e e k + T a v g − r o r a t i o n + T a v g − a c c e s s = 9 + 4 + 0.02 = 13.0 s m s T_{access} = T_{seek} + T_{avg-roration} + T_{avg-access} = 9 + 4 + 0.02 = 13.0s ms Taccess=Tseek+Tavgroration+Tavgaccess=9+4+0.02=13.0sms

2.3 逻辑磁盘块

从操作系统的角度来看,整个磁盘被抽象成了一个个逻辑块序列,每个逻辑块的大小为512byte,磁盘内部有一个小的固件设备叫逻辑控制器,它维护着逻辑号码与实际磁盘扇区之间的映射关系。

当操作系统要读区一个扇区的内容的时候,操作系统会发送一个命令到磁盘控制器,让它读取某个逻辑块。控制器会将该逻辑块的号码翻译为(盘面,磁道,扇区)的三元组,然后通过读写头来读取扇区的内容,将它们复制到内存当中。
在这里插入图片描述

三、固态硬盘

固态硬盘由一个或者多个闪存芯片组成,它使用了闪存芯片取代了传动臂加盘片这种机械式的工作方式。除此之外,固态硬盘还包含了一个闪存转换层(Flash translation layer),它的功能与磁盘控制器类似,都是将操作系统对于逻辑块的请求翻译为对于底层无力设备的访问。不同的是,闪存芯片是基于Nand flash实现的。

在这里插入图片描述

每一颗闪存芯片是由一个或者多个Die组成。每个Die可以分为多个plane,每个plane包含多个block,需要注意的是这里的block和逻辑块是没有关系的。Block内部又被分成多个Page,对于固态硬盘数据是以Page为单位进行读写的。与机械磁盘不同,不同固态硬盘page的大小可能是不同的。

在这里插入图片描述

传统的机械磁盘包含读和写两个操作,对于固态硬盘,除了这两个基本操作之外,还多了一个擦除的操作。因为闪存编程原理的限制,只能将1改为0,不能将0改为1。所以,一个page在写入操作之前所有的位都是1,写入操作就是将一些位从1变为0

需要注意的是,写入操作是以page为单位的,而且写入之前页是需要擦除的,不能直接覆盖。

对于擦除操作是以block为单位的。擦除操作的本质就是将所有的block-bit变为1。在经历一定次数的擦除操作之后,block就会发生磨损,一旦一个block发生磨损之后,就不能够再使用了。

在这里插入图片描述

四、高速缓存存储组织结构

4.1 通用高速缓存组织结构

考虑一个计算机当中,每个存储器的地址有m(16,32,64)位,形成M=2^m个不同的地址(内存空间大小)。而它的高速缓存被组织成S=2^s个高速缓存组(cache set),每个组包含E个高速缓存行(cache line)。每行由三部分组成:

  • 有效位(valid bit):有效位用来表示当前行是否是有效的。
  • 标记位置(tag bit):标记位可以唯一标识存储在告诉缓存中的块,位数t=m-(b+s)
  • 数据块(block):数据块用来存储缓存的具体数据,大小为B=2^b个字节。

4.2 直接映射高速缓存

根据每个组中高速缓存的行数E,高速缓存被分为不同的类。每个组中只有一行(E=1)的类被称为直接映射高速缓存(direct-mapped cache)。

假设我们有这样一个系统,它有一个CPU、一个寄存器文件、一个L1告诉缓存、和一个主存。当CPU执行一条读取内存字w的指令,它向L1高速缓存请求这个字。如果L1高速缓存有w的一个缓存副本,那么就会命中L1高速缓存,高速缓存会抽取w,返回给CPU。如果没有命中,L1高速缓存会向主存请求包含w的块的一个副本,CPU必须等待。当L1高速缓存获取到请求块的时候,会将请求块缓存到自己的一个行当中,然后从中抽取出w,将它返回给CPU。

高速缓存确定一个请求是否命中,然后抽取请求字的过程分为三步:1)组选择;2)行匹配;3)字抽取;

1. 组选择

在这一步当中,高速缓存从w的地址中间抽取s个组索引位。这些位置会被解释为一个对应于组索引值的无符号整数。例如下面这个例子中00001会解释为组1的整数索引。
在这里插入图片描述

2. 行匹配

下图展示了行匹配是如何工作的。在这个例子中被选中的组仅仅存在一个告诉缓存行,该缓存行的valid_bit=1,表示它是一个有效行。因为高速缓存行的标记位与地址中的标记位相匹配,所以我们知道我们想获取的自一定存在于当前缓存行当中。换句话说也就是缓存命中,如果有效位置没有被设置,或者标记不匹配,那么我们就得到一个缓存不命中。
在这里插入图片描述
3. 字选择

一旦命中,我们就会根据w地址所提供的块偏移获取到CPU请求的数据。在示例当中,块偏移是100,这表明w的副本是从块中的字节4开始的(我们假设字长为4字节)

4. 缓存不命中时的行替换

如果发生缓存不命中那么需要从存储层次结构的下一层取出被请求的块,然后将新的块存储在告诉缓存当中。

5. 综述:运行中的告诉缓存举例

一个具体的例子能够帮助清楚这个过程,假设我们的高速缓存描述如下:
( S , E , B , m ) = ( 4 , 1 , 2 , 4 ) (S,E,B,m) = (4,1,2,4) (S,E,B,m)=(4,1,2,4)
换句话说,告诉缓存有4个组,每组一行,每个块2个字节,而地址是4位的。我们还假设每个字都是单字节的
在这里插入图片描述

这里的Block number并不是指的L1 Cache中的BLOCK Cache

在这里插入图片描述
当CPU读取1101也就是m[13]的时候,发现m[13]set_index=10=2,会将block 6读取到set 2当中,并将有效位置设置为1,并将tag设置为1
在这里插入图片描述

当CPU读取1000也就是m[8]的时候,发现m[8]set_index=8,但是对比tag发现1 != 0,所以需要使用block 4替换原来set_0中的数据快。

6. 直接映射高速缓存的冲突不命中问题

考虑一个计算两个浮点数积的情况:

float dotprod(float x[8], float y[8]){
	float sum = 0.0f;
	int i;

	for (i=0; i<8; i++) 
		sum += x[i]*y[i]
	return sum;
}

当CPU访问x[0]的时候发生不命中,从而将x[0]x[1]x[2]x[3]的内容加载到set0的block中,当CPU访问y[0]的时候发生不命中,从而导致y[0]y[1]y[2]y[3]替换掉了x[0]x[1]x[2]x[3]下次访问还是会发生缓存不命中。

在这里插入图片描述

如何解决上述问题,可以通过数据填充的方式,在数组x[8]y[8]之间填充24个字节的数据:

在这里插入图片描述

4.3 组相联高速缓存

直接映射高速缓存中不命中造成的问题源于每个组只有一行这个限制。组组相联高速缓存(set associative cache)放松了这条限制,所以每个组都保存有多于一个的高速缓存行。一个1<E<C/B的告诉缓存通常被称为E路组相联告诉缓存。
在这里插入图片描述

1. 组选择

组选择和直接映射高速缓存的组选择方式一样,通过直接映射来对于组进行选择。

2. 行匹配和字选择

在这里插入图片描述
组相联的行匹配过程如上图所示,将目标字w的tag分别与缓存行中每个组的tag进行匹配,如果相匹配,则代表缓存命中,然后根据block offset信息进行字抽取即可。

3. 缓存不命中

如果CPU选择的字不在任何一行,那么就是缓存不命中,告诉缓存必须从内存中取出这个字的块。该替换哪一行呢?当然,如果有空行,那它就是不错的选择,如果没有通常会使用LFU(Least-Frequently-Used, LFU)或者LRU(Least-Recently-Used, LRU)。这些策略需要额外的时间和策略。

4.4 全相联高速缓存

全相联高速缓存(fully associative cache)是一个包含所有高速缓存行的组:

在这里插入图片描述

1. 组选择

由于全相联高速缓存只存在一个组,所以不需要组索引位,地址也被分成标记位和块偏移位。

2. 行匹配和字选择

在这里插入图片描述

全相联告诉缓存中的行匹配和字选择与组相联中的一样。因为高速缓存电路必须并行搜索很多相匹配的标记,构造一个又大又快的高速缓存很困难。因此,全相联告诉缓存只适合做小的高速缓存,如TLB(页表缓存器)。

4.5 有关写的问题

假设我们要写一个已经缓存了的字w(写命中, write hit),在告诉缓存中更新了w的副本之后,如何更新低一级的副本呢?

  • write-through(写穿透): 直接更新w在内存中的副本,缺点就是每次写会会引起总线流量。
  • write-back(写回):尽可能推迟更新,只有当替换算法驱逐更新块的时候,才将它写入到内存中。可以显著减少总线流量,缺点是增加了复杂性,高速缓存必须为每个缓存行维护一个额外的修改位(dirty bit),表明这个告诉缓存是否被修改过。

写不命中:

  • write-allocate(写分配):将相应的低一级别的缓存块加载到告诉缓存中,然后更新这个告诉缓存块。
  • not-write-allocate(写不分配):避开高速缓存,直接把字写到低一层当中。

写穿透通常与写不分配搭配使用,写回通常与写分配搭配使用。

五、MESI协议

5.1 MESI协议介绍

MESI其实是四个状态单词的开头字母的缩写,分别是:

  • Modified,已修改
  • Exclusive,独占
  • Shared,共享
  • Invalidated,已失效

已修改代表该Cache Block上的数据已经被更新过,但是还没有写到内存里。而已失效状态,表示的是这个Cache Block里的数据已经失效了,不可以读取该状态的数据。

独占共享状态都代表Cache Block里的数据是干净的,也就是说,这个时候Cache Block里的数据和内存里面的数据是一致性的。

独占共享的差别在于,独占状态的时候,数据只存储在一个CPU核心的Cache里,而其他CPU核心的Cache没有该数据。这个时候,如果要向独占的Cache写数据,就可以直接自由地写入,而不需要通知其他CPU核心,因为只有你这有这个数据,就不存在缓存一致性的问题了,于是就可以随便操作该数据。

另外,在独占状态下的数据,如果有其他核心从内存读取了相同的数据到各自的Cache,那么这个时候,独占状态下的数据就会变成共享状态。

那么,共享状态代表着相同的数据在多个CPU核心的Cache里都有,所以当我们要更新 Cache里面的数据的时候,不能直接修改,而是要先向所有的其他CPU核心广播一个请求,要求先把其他核心的Cache中对应的Cache Line标记为无效状态,然后再更新当前Cache里面的数据。

我们举个例子来看看这四种状态的变化:

  1. ACPU核心从内存读取变量i的值,数据被缓存在ACPU核心自己的Cache里面,此时其他CPU核心的Cache没有缓存该数据,于是标记Cache Line状态为独占(Exclusive),此时其 Cache中的数据与内存是一致的;
  2. 然后B号 CPU 核心也从内存读取了变量i的值,此时会发送消息给其他CPU核心,由于ACPU核心已经缓存了该数据,所以会把数据返回给BCPU核心。在这个时候,AB核心缓存了相同的数据,Cache Line的状态就会变成共享(Shared),并且其Cache中的数据与内存也是一致的;
  3. ACPU核心要修改Cachei变量的值,发现数据对应的Cache Line的状态是共享状态,则要向所有的其他CPU核心广播一个请求,要求先把其他核心的Cache中对应的Cache Line标记为**无效(Invalidated)状态,然后ACPU核心才更新Cache 里面的数据,同时标记Cache Line已修改(Modified)**状态,此时Cache中的数据就与内存不一致了。
  4. 如果A号CPU核心继续修改Cachei变量的值,由于此时的Cache Line已修改状态,因此不需要给其他CPU核心发送消息,直接更新数据即可。
  5. 如果ACPU核心的Cache里的i变量对应的Cache Line要被替换,发现 Cache Line状态是已修改(Modified)状态,就会在替换前先把数据同步到内存。

所以,可以发现当Cache Line状态是**已修改(Modified)或者独占(Exclusive)**状态时,修改更新其数据不需要发送广播给其他CPU核心,这在一定程度上减少了总线带宽压力。

事实上,整个MESI的状态可以用一个有限状态机来表示它的状态流转。还有一点,对于不同状态触发的事件操作,可能是来自本地CPU核心发出的广播事件,也可以是来自其他CPU核心通过总线发出的广播事件。下图即是 MESI 协议的状态图:

在这里插入图片描述

5.2 cache伪共享

1. c语言DEMO

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

#define COUNT 5000000000

struct _t {
#if defined(PADDING)
	long p1, p2, p3, p4, p5, p6, p7;
#endif
	long x;
#if defined(PADDING)
	long p9, p10, p11, p12, p13, p14, p15;
#endif
};

struct _t t1;
struct _t t2;

void *test_thread1(void *arg)
{
	clock_t start = clock();
	for (long i = 0; i < COUNT; i++) t1.x = i;
	clock_t end = clock();
  	double cost = (double)(end-start)/CLOCKS_PER_SEC;
  	printf("test_thread1_cost=%f\n", cost);

	return NULL;
}

void *test_thread2(void *arg)
{
	clock_t start = clock();
	for (long i = 0; i < COUNT; i++) t2.x = i;
	clock_t end = clock();
  	double cost = (double)(end-start)/CLOCKS_PER_SEC;
  	printf("test_thread2_cost=%f\n", cost);
	return NULL;
}

int main(int argc, char *argv[])
{
	pthread_t test1_thread_t;
	pthread_t test2_thread_t;

	if (pthread_create(&test1_thread_t, NULL, test_thread1, "test_1_thread") != 0) {
		printf("test1_thread_t create error\n");
		exit(1);
	}

	if (pthread_create(&test2_thread_t, NULL, test_thread2, "test_2_thread") != 0) {
		printf("test2_thread_t create error\n");
		exit(1);
	}

	pthread_join(test1_thread_t, NULL);
	pthread_join(test2_thread_t, NULL);

	return EXIT_SUCCESS;
}
  • makefile
no-padding: main.c
	clang main.c -O0 -o no-padding -Wall -lpthread

padding: main.c
	clang main.c -O0 -o padding -Wall -lpthread -DPADDING

no-padding-perf: no-padding
	perf stat -e cache-references -e cache-misses ./no-padding

padding-perf: padding
	perf stat -e cache-references -e cache-misses ./padding

运行结果:

# time ./no-padding 
test_thread1_cost=17.980636
test_thread2_cost=19.469469

________________________________________________________
Executed in   10.48 secs    fish           external
   usr time   19.47 secs  520.00 micros   19.47 secs
   sys time    0.00 secs  268.00 micros    0.00 secs

# time ./padding 
test_thread2_cost=4.193434
test_thread1_cost=4.207044

________________________________________________________
Executed in    2.11 secs    fish           external
   usr time    4.21 secs  582.00 micros    4.21 secs
   sys time    0.00 secs  301.00 micros    0.00 secs

1. JAVA语言DEMO

class Counter {

    public volatile long count1 = 0;
    // long p1, p2, p3, p4, p5, p6, p7;
    public volatile long count2 = 0;

}

public class FalseSharingExample {

	public static void main(String[] args) {

		Counter counter1 = new Counter();
		Counter counter2 = counter1;

		long iterations = 1_000_000_000;

		Thread thread1 = new Thread(() -> {
			long startTime = System.currentTimeMillis();
			for(long i=0; i<iterations; i++) {
				counter1.count1++;
			}
			long endTime = System.currentTimeMillis();
			System.out.println("total time: " + (endTime - startTime));
		});
		Thread thread2 = new Thread(() -> {
			long startTime = System.currentTimeMillis();
			for(long i=0; i<iterations; i++) {
				counter2.count2++;
			}
			long endTime = System.currentTimeMillis();
			System.out.println("total time: " + (endTime - startTime));
		});

		thread1.start();
		thread2.start();
	}
}
  • makefile
run:
	javac FalseSharingExample.java && java FalseSharingExample

运行结果:

# make run (no-padding)
javac FalseSharingExample.java && java FalseSharingExample
total time: 29702
total time: 29891
# make run (padding)
javac FalseSharingExample.java && java FalseSharingExample
total time: 6285
total time: 6285

从demo来看,有效利用高速缓存确实对性能存在大幅度提升

五、实战

5.1 如何查看CPU的cache信息

## 缓存级别 
> cat /sys/devices/system/cpu/cpu0/cache/index0/level		
1
## 那些CPU共享该cache
> cat /sys/devices/system/cpu/cpu0/cache/index0/shared_cpu_list 
0
## 256=2^8个组
> cat /sys/devices/system/cpu/cpu0/cache/index0/number_of_sets 
256
## cache的大小为64KB
jack@jack-pc ~/D/week> cat /sys/devices/system/cpu/cpu0/cache/index0/size 
64K
## 每个set包含4行
jack@jack-pc ~/D/week> cat /sys/devices/system/cpu/cpu0/cache/index0/ways_of_associativity 
4
## block的大小为64字节
jack@jack-pc ~/D/week> cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size 
64
## 64KB = 64*4*256

参考

  1. csapp课堂视频-memory cache
  2. 视频-直接映射高速缓存
  3. csapp-mountain源码
  4. csapp相关资源下载网站
  5. cpu的cache信息查看
  6. CPU伪共享
  7. java处理伪共享
  8. mesi动画
  9. perftools统计缓存命中
  10. 知乎-伪共享-凌乱记
  11. golang测试伪共享
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值