缓存行、cpu伪共享和缓存行填充

由于在看disruptor时了解到缓存行,以及缓存行填充的问题,所以各处了解记在这里

一、缓存行

CPU 为了更快的执行代码。于是当从内存中读取数据时,并不是只读自己想要的部分。而是读取足够的字节来填入高速缓存行。根据不同的 CPU ,高速缓存行大小不同。如 X86 是 32BYTES ,而 ALPHA 是 64BYTES 。并且始终在第 32 个字节或第 64 个字节处对齐。这样,当 CPU 访问相邻的数据时,就不必每次都从内存中读取,提高了速度。 因为访问内存要比访问高速缓存用的时间多得多。
这个缓存是CPU内部自己的缓存,内部的缓存单位是行,叫做缓存行。在多核环境下会出现CPU之间的内存同步问题(比如一个核加载了一份缓存,另外一个核也要用到同一份数据),如果每个核每次需要时都往内存中存取(一个在读缓存,一个在写缓存时,造成数据不一致),这会带来比较大的性能损耗,这个问题一般是通过MESI协议来解决的。
这里写图片描述

图1说明了伪共享的问题。在核心1上运行的线程想更新变量X,同时核心2上的线程想要更新变量Y。不幸的是,这两个变量在同一个缓存行中。每个线程都要去竞争缓存行的所有权来更新变量。如果核心1获得了所有权,缓存子系统将会使核心2中对应的缓存行失效。当核心2获得了所有权然后执行更新操作,核心1就要使自己对应的缓存行失效。这会来来回回的经过L3缓存,大大影响了性能。如果互相竞争的核心位于不同的插槽,就要额外横跨插槽连接,问题可能更加严重。

1.Cache的写策略:

1)Write through(写通)
     每次CPU修改了cache中的内容,Cache立即更新内存的内容
2) Write back(写回)
    内核修改cache的内容后,cache并不会立即更新内存中的内容,而是等到这个cache line因为某种原因需要从cache中移除时,cache才会更新内存中的内容。

Write through(写通)由于有大量的访问内存的操作,效率太低,大多数处理器都使用Writeback(写回)策略。
这里写图片描述
Cache如何知道这行有没有被修改?需要一个标志-dirty标志。Dirty标志为1,表示cache的内容被修改,和内存的内容不一致,当该cache line被移除时,数据需要被更新到内存,dirty标志位0(称为clean),表示cache的内容和内存的内容一致。

2.Cache一致性

1)一致性问题的产生-信息不对称导致的问题
在多核处理器中,内存中有一个数据x,值为3,被缓存到core0和core1中,如果core0将x修改为5,而core1
不知道x被修改,还在使用旧数据,就会导致程序出错,这就是cache的不一致。
2)Cache一致性的底层操作
为了保证cache的一致性,处理器提供了两个保证cache一致性的底层操作:Writeinvalidate和Write update。
这里写图片描述

Write invalidate(置无效):当一个内核修改了一份数据,其他内核上如果有这份数据的复制,就置成无效。

这里写图片描述

Write update(写更新):当一个内核修改了一份数据,其他地方如果有这份数据的复制,就都更新到最新值。

Write invalidate是一种简单的方式,不需要更新数据,如果core1和core2以后不再使用变量x,这时候采用write invalidate就非常有效,不过由于一个valid标志对应一个Cache line,将valid标志置成invalid后,这个cache line中其他的有效的数据也不能使用了。Write upodate策略会产生大量的数据更新操作,不过只用更新修改的数据,如果core1和core2会使用变量x,那么writeupdate就比较有效。由于Writeinvalidate简单,大多数处理器都是用Writeinvalidate策略。

MESI协议中包含M、E、S、I四个状态,分别的意思是:

  • M(Modified)位。M 位为1 时表示当前Cache 行中包含的数据与存储器中的数据不一致,而且它仅在本CPU的Cache 中有效,不在其他CPU的Cache
    中存在拷贝,在这个Cache行的数据是当前处理器系统中最新的数据拷贝。当CPU对这个Cache行进行替换操作时,必然会引发系统总线的写周期,将Cache行中数据与内存中的数据同步。
  • E(Exclusive 独占)位。E 位为1 时表示当前Cache行中包含的数据有效,而且该数据仅在当前CPU的Cache中有效,而不在其他CPU的Cache中存在拷贝。在该Cache行中的数据是当前处理器系统中最新的数据拷贝,而且与存储器中的数据一致。
  • S(Shared 共享)位。S 位为1 表示Cache行中包含的数据有效,而且在当前CPU和至少在其他一个CPU中具有副本。在该Cache行中的数据是当前处理器系统中最新的数据拷贝,而且与存储器中的数据一致。
  • I(Invalid 无效)位。I 位为1 表示当前Cache 行中没有有效数据或者该Cache行没有使能。MESI协议在进行Cache行替换时,将优先使用I位为1的Cache行。
    这里写图片描述
    这里写图片描述
    这里写图片描述

MESI协议状态迁移图:
这里写图片描述

Local Read表示本内核读本Cache的值,Local Write表示本内核写Cache中的值,Remote Read表示其他内核
Remote Read读其他Cache中的值,Remote write 表示其他内核写其他Cache的值,箭头表示本Cache line状态的迁移

二、CPU伪共享

cpu在对缓存行进行了不同的操作后,在cpu缓存行中会记录缓存的不同状态。当一个核要对共享的数据进行写操作时,需要给其他核发送RFO(REQUEST FOR OWNER)消息并把其他核的数据改成I态。这是一种比较消耗性能的操作。
cpu的伪共享问题本质是:几个在逻辑上并不包含在同一个内存单元内的数据,由于被cpu加载在同一个缓存行当中,当在多线程环境下,被不同的cpu执行,导致缓存行失效而引起的大量的缓存命中率降低。
例如:当两个线程分别对一个数组中的两份数据进行写操作,每个线程操作不同index上的数据,看上去,两份数据之间是不存在同步问题的,但是,由于他们可能在同一个cpu缓存行当中,这就会使这一份缓存行出现大量的缓存失效,如前所述当一份线程更新时要给另一份线程发送RFO消息并把它的缓存失效掉。

三、CacheLine补齐(缓存行填充)

解决伪共享问题的一个办法是让每一份数据占据一个缓存行:因为缓存行的大小是64个字节,那我们只要让数组中每份数据的大小大于64个字节,就可以保证他们在不同的缓存行当中,就能避免这样的伪共享问题。
比如一个类当中原本只有一个long类型的属性。这样这个类型的对象只占了16个字节(java对象头有8字节),如果这个类型被定义成一个长度为4的数组,这个数组的所有数据都可能在一个缓存行当中,就可能出现伪共享问题,那么这个时候,就可以采用补齐(padding)的办法,在这个类型中加上public long a,b,c,d,e,f,g;这六个无用的属性定义,使得这个类型的一个实例占用内存达到64字节,这样这个类型的伪共享问题就得到了解决,在多线程当中对这个类型的数组进行写操作就能避免伪共享问题。

在Java 8中,可以采用@Contended在类级别上的注释,来进行缓存行填充。这样,多线程情况下的伪共享冲突问题。 感兴趣的同学可以查看该文。
其实,@Contended注释还可以应用于字段级别(Field-Level),当应用于字段级别时,被注释的字段将和其他字段隔离开来,会被加载在独立的缓存行上。在字段级别上,@Contended还支持一个“contention group”属性(Class-Level不支持),同一个group的字段们在内存上将是连续,但和其他字段隔离开来。
执行时,必须加上虚拟机参数-XX:-RestrictContended,@Contended注释才会生效。

disruptor就采用了 缓存行填充来提高程序性能

  • 7
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值