精华内容
下载资源
问答
  • CPU缓存和伪共享

    2019-04-15 14:36:35
    内存比CPU慢很多,现在获取内存中的一条数据大概需要200多个CPU周期(CPU cycles),而CPU寄存器一般情况下1个CPU周期就够了。 网页浏览器为了加快速度,会在本机存缓存以前浏览过的数据;传统数据库或NoSQL数据库为了...

    CPU缓存

    CPU是计算机的大脑,它负责执行程序的指令;内存负责存数据,包括程序自身数据。内存比CPU慢很多,现在获取内存中的一条数据大概需要200多个CPU周期(CPU cycles),而CPU寄存器一般情况下1个CPU周期就够了。

    网页浏览器为了加快速度,会在本机存缓存以前浏览过的数据;传统数据库或NoSQL数据库为了加速查询,常在内存设置一个缓存,减少对磁盘(慢)的IO。同样内存与CPU的速度相差太远,于是CPU设计者们就给CPU加上了缓存(CPU Cache)。如果需要对同一批数据操作很多次,那么把数据放至离CPU更近的缓存,会给程序带来很大的速度提升。例如,做一个循环计数,把计数变量放到缓存里,就不用每次循环都往内存存取数据了。下面是CPU Cache的简单示意图:
    在这里插入图片描述
    随着多核的发展,CPU Cache分成了三个级别:L1、 L2、L3。级别越小越接近CPU,所以速度也更快,同时也代表着容量越小。L1是最接近CPU的,它容量最小,例如32K,速度最快,每个核上都有一个L1 Cache(准确地说每个核上有两个L1 Cache,一个存数据 L1d Cache,一个存指令 L1i Cache)。L2 Cache 更大一些,例如256K,速度要慢一些,一般情况下每个核上都有一个独立的L2 Cache;L3 Cache是三级缓存中最大的一级,例如12MB,同时也是最慢的一级,在同一个CPU插槽之间的核共享一个L3 Cache。就像数据库cache一样,获取数据时首先会在最快的cache中找数据,如果没有命中(Cache miss)则往下一级找,直到三层Cache都找不到,那只有向内存要数据了。一次次地未命中,代表取数据消耗的时间越长。

    缓存行(Cache Line)

    Cache的最小组成单位是cache line。每一个缓存都是由很多个cache line组成的。每个cache line的大小一般的32字节或者64字节。cache中的数据操作也是以cache line为单位的,也就是一个cache line上的数据会被同时操作。一个Java long型占8个字节,所以从一条缓存行上可以获取到8个long型变量,如果访问一个long型数组,当有一个long被加载到cache中,将会无消耗地加载了另外7个,所以可以非常快地遍历数组。

    MESI协议

    多核CPU都有自己的专门缓存(一般伪L1,L2),以及用一个CPU插槽之间的核共享缓存(一般为L3),不同核心的CPU缓存中难免会加载同样的数据,那么如何保证数据的一致性呢,就是 MESI 协议了。
    在 MESI 协议中,每个 Cache line 有4个状态,可用 2 个 bit 表示,它们分别是:

    1. M(Modified):这行数据有效,数据被修改了,和内存中的数据不一致,数据只存在于本 Cache 中
    2. E(Exclusive):这行数据有效,数据和内存中的数据一致,数据只存在于本 Cache 中;
    3. S(Shared):这行数据有效,数据和内存中的数据一致,数据存在于很多 Cache 中
    4. I(Invalid):这行数据无效

    那么,假设有一个变量i=3(应该是包括变量i的缓存块,块大小为缓存行大小);已经加载到多核(a,b,c)的缓存中,此时该缓存行的状态为S;此时其中的一个核a改变了变量i的值,那么在核a中的当前缓存行的状态将变为M,b,c核中的当前缓存行状态将变为I。如下图:

    在这里插入图片描述

    伪共享

    然而缓存行也存在问题:假设CPU的第一个核需要操作a变量,第二个核需要操作b变量,表面看a和b是没有任何关系的,但是a和b在同一个cache line中,这样假设核心一修改了变量a的值,那么它将会刷新所有和a相关的缓存的数据,b变量也就会受到牵连,最后导致核心二再去缓存读取b变量的时候出现cache miss,需要重新到主存加载新数据,这就是所谓的false share(伪共享)
    在这里插入图片描述

    如何避免伪共享?

    为了避免由于 false sharing 导致 Cache Line 从 L1,L2,L3 到主存之间重复载入,我们可以使用数据填充的方式来避免,即单个数据填充满一个CacheLine。
    一个典型的例子就是lmax disruptor中的缓存行填充技术。假设cache line的大小是64字节,一个cache line能容纳8个long型变量,disruptor中的做法是在long变量的前面和后面分别填充7个long变量,这样就可以避免我们需要的变量和其他变量在同一个cache line,解决伪共享问题。这就是用空间换时间。

    小结

    内存执行速度跟不上CPU执行速度,设置缓存改善CPU对数据的读取。缓存容易引发伪共享问题:多核下同个数据可能分布在不同的缓存中或者同一个cache line下,为了避免数据出现不同步的问题,根据MESI协议对数据进行标记,判断数据是否"干净";解决伪共享的问题可以通过数据填充的方式进行,即单个cache line上只填一个数据。

    展开全文
  • cpu共享问题

    千次阅读 2012-09-23 21:53:48
    CPU内部也会有自己的缓存,内部的缓存单位是行,叫做缓存行。在多核环境下会出现CPU之间的内存同步问题(比如一个核加载了一份缓存,另外一个核也要用到同一份数据),如果每个核每次需要时都往内存中存取,这会带来...

              CPU内部也会有自己的缓存,内部的缓存单位是行,叫做缓存行。在多核环境下会出现CPU之间的内存同步问题(比如一个核加载了一份缓存,另外一个核也要用到同一份数据),如果每个核每次需要时都往内存中存取,这会带来比较大的性能损耗,这个问题一般是通过MESI协议来解决的。

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

    • M(修改, Modified): 本地处理器已经修改缓存行, 即是脏行, 它的内容与内存中的内容不一样. 并且此cache只有本地一个拷贝(专有).
    • E(专有, Exclusive): 缓存行内容和内存中的一样, 而且其它处理器都没有这行数据
    • S(共享, Shared): 缓存行内容和内存中的一样, 有可能其它处理器也存在此缓存行的拷贝
    • I(无效, Invalid): 缓存行失效, 不能使用
             
              cpu在对缓存行进行了不同的操作后,在cpu缓存行中会记录缓存的不同状态。当一个核要对共享的数据进行写操作时,需要给其他核发送RFO(REQUEST FOR OWNER)消息并把其他核的数据改成I态。这是一种比较消耗性能的操作。
              cpu的伪共享问题本质是:几个在逻辑上并不包含在同一个内存单元内的数据,由于被cpu加载在同一个缓存行当中,当在多线程环境下,被不同的cpu执行,导致缓存行失效而引起的大量的缓存命中率降低。
              例如:当两个线程分别对一个数组中的两份数据进行写操作,每个线程操作不同index上的数据,看上去,两份数据之间是不存在同步问题的,但是,由于他们可能在同一个cpu缓存行当中,这就会使这一份缓存行出现大量的缓存失效,如前所述当一份线程更新时要给另一份线程发送RFO消息并把它的缓存失效掉。
              解决这个问题的一个办法是让这个数组中不同index的数据在不同的缓存行:因为缓存行的大小是64个字节,那我们只要让数组中没份数据的大小大于64个字节,就可以保证他们在不同的缓存行当中,就能避免这样的伪共享问题。
              比如一个类当中原本只有一个long类型的属性。这样这个类型的对象只占了16个字节(java对象头有8字节),如果这个类型被定义成一个长度为4的数组,这个数组的所有数据都可能在一个缓存行当中,就可能出现伪共享问题,那么这个时候,就可以采用补齐(padding)的办法,在这个类型中加上public long a,b,c,d,e,f,g;这六个无用的属性定义,使得这个类型的一个实例占用内存达到64字节,这样这个类型的伪共享问题就得到了解决,在多线程当中对这个类型的数组进行写操作就能避免伪共享问题。
              

              

    展开全文
  • 缓存行、cpu共享和缓存行填充

    千次阅读 2017-07-07 23:23:35
    CPU 为了更快的执行代码。于是当从内存中读取数据时,并不是只读自己想要的部分。而是读取足够的字节来填入高速缓存行。根据不同的 CPU ,高速缓存行大小不同。如 X86 是 32BYTES ,而 ALPHA 是 64BYTES 。并且始终...

    由于在看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就采用了 缓存行填充来提高程序性能

    展开全文
  • 看了很多网上讲解java伪共享、缓存行填充和CPU缓存的MESI等等,零零碎碎,目前感觉就这篇文章讲的最清楚,忍不住转载下。 原文如下: 认识CPU Cache CPU Cache概述 随着CPU的频率不断提升,而内存...

    转载至http://geek.csdn.net/news/detail/114619

    看了很多网上讲解java伪共享、缓存行填充和CPU缓存的MESI等等,零零碎碎,目前感觉就这篇文章讲的最清楚,忍不住转载下。


    原文如下:


    认识CPU Cache

    CPU Cache概述

    随着CPU的频率不断提升,而内存的访问速度却没有质的突破,为了弥补访问内存的速度慢,充分发挥CPU的计算资源,提高CPU整体吞吐量,在CPU与内存之间引入了一级Cache。随着热点数据体积越来越大,一级Cache L1已经不满足发展的要求,引入了二级Cache L2,三级Cache L3。(注:若无特别说明,本文的Cache指CPU Cache,高速缓存)CPU Cache在存储器层次结构中的示意如下图:

    图片描述

    计算机早已进入多核时代,软件也越来越多的支持多核运行。一个处理器对应一个物理插槽,多处理器间通过QPI总线相连。一个处理器包含多个核,一个处理器间的多核共享L3 Cache。一个核包含寄存器、L1 Cache、L2 Cache,下图是Intel Sandy Bridge CPU架构,一个典型的NUMA多处理器结构:

    图片描述

    作为程序员,需要理解计算机存储器层次结构,它对应用程序的性能有巨大的影响。如果需要的程序是在CPU寄存器中的,指令执行时1个周期内就能访问到他们。如果在CPU Cache中,需要1~30个周期;如果在主存中,需要50~200个周期;在磁盘上,大概需要几千万个周期。充分利用它的结构和机制,可以有效的提高程序的性能。

    以我们常见的X86芯片为例,Cache的结构下图所示:整个Cache被分为S个组,每个组是又由E行个最小的存储单元——Cache Line所组成,而一个Cache Line中有B(B=64)个字节用来存储数据,即每个Cache Line能存储64个字节的数据,每个Cache Line又额外包含一个有效位(valid bit)、t个标记位(tag bit),其中valid bit用来表示该缓存行是否有效;tag bit用来协助寻址,唯一标识存储在CacheLine中的块;而Cache Line里的64个字节其实是对应内存地址中的数据拷贝。根据Cache的结构题,我们可以推算出每一级Cache的大小为B×E×S。

    图片描述

    那么如何查看自己电脑CPU的Cache信息呢?

    在windows下查看方式有多种方式,其中最直观的是,通过安装CPU-Z软件,直接显示Cache信息,如下图:

    图片描述

    此外,Windows下还有两种方法:

    ①Windows API调用GetLogicalProcessorInfo。 
    ②通过命令行系统内部工具CoreInfo。

    如果是Linux系统, 可以使用下面的命令查看Cache信息:

    ls /sys/devices/system/cpu/cpu0/cache/index0

    图片描述
    还有lscpu等命令也可以查看相关信息,如果是Mac系统,可以用sysctl machdep.cpu 命令查看cpu信息。

    如果我们用Java编程,还可以通过CacheSize API方式来获取Cache信息, CacheSize是一个谷歌的小项目,java语言通过它可以进行访问本机Cache的信息。示例代码如下:

    public static void main(String[] args) throws CacheNotFoundException {
            CacheInfo info = CacheInfo.getInstance(); 
            CacheLevelInfo l1Datainf = info.getCacheInformation(CacheLevel.L1, CacheType.DATA_CACHE);
            System.out.println("第一级数据缓存信息:"+l1Datainf.toString());
    
            CacheLevelInfo l1Instrinf = info.getCacheInformation(CacheLevel.L1, CacheType.INSTRUCTION_CACHE);
            System.out.println("第一级指令缓存信息:"+l1Instrinf.toString());
        }

    打印输出结果如下:

    第一级数据缓存信息:CacheLevelInfo [cacheLevel=L1, cacheType=DATA_CACHE, cacheSets=64, cacheCoherencyLineSize=64, cachePhysicalLinePartitions=1, cacheWaysOfAssociativity=8, isFullyAssociative=false, isSelfInitializing=true, totalSizeInBytes=32768]
    
    第一级指令缓存信息:CacheLevelInfo [cacheLevel=L1, cacheType=INSTRUCTION_CACHE, cacheSets=64, cacheCoherencyLineSize=64, cachePhysicalLinePartitions=1, cacheWaysOfAssociativity=8, isFullyAssociative=false, isSelfInitializing=true, totalSizeInBytes=32768]

    还可以查询L2、L3级缓存的信息,这里不做示例。从打印的信息和CPU-Z显示的信息可以看出,本机的Cache信息是一致的,L1数据/指令缓存大小都为:C=B×E×S=64×8×64=32768字节=32KB。

    Cache Line伪共享及解决方案

    Cache Line伪共享分析

    说伪共享前,先看看Cache Line 在java编程中使用的场景。如果CPU访问的内存数据不在Cache中(一级、二级、三级),这就产生了Cache Line miss问题,此时CPU不得不发出新的加载指令,从内存中获取数据。通过前面对Cache存储层次的理解,我们知道一旦CPU要从内存中访问数据就会产生一个较大的时延,程序性能显著降低,所谓远水救不了近火。为此我们不得不提高Cache命中率,也就是充分发挥局部性原理。

    局部性包括时间局部性、空间局部性。时间局部性:对于同一数据可能被多次使用,自第一次加载到Cache Line后,后面的访问就可以多次从Cache Line中命中,从而提高读取速度(而不是从下层缓存读取)。空间局部性:一个Cache Line有64字节块,我们可以充分利用一次加载64字节的空间,把程序后续会访问的数据,一次性全部加载进来,从而提高Cache Line命中率(而不是重新去寻址读取)。

    看个例子:内存地址是连续的数组(利用空间局部性),能一次被L1缓存加载完成。

    如下代码,长度为16的row和column数组,在Cache Line 64字节数据块上内存地址是连续的,能被一次加载到Cache Line中,所以在访问数组时,Cache Line命中率高,性能发挥到极致。

    public int run(int[] row, int[] column) {
        int sum = 0;
        for(int i = 0; i < 16; i++ ) {
            sum += row[i] * column[i];
        }
        return sum;
    }

    而上面例子中变量i则体现了时间局部性,i作为计数器被频繁操作,一直存放在寄存器中,每次从寄存器访问,而不是从主存甚至磁盘访问。虽然连续紧凑的内存分配带来高性能,但并不代表它一直都能带来高性能。如果把它放在多线程中将会发生什么呢?如图:

    图片描述

    数据X、Y、Z被加载到同一Cache Line中,线程A在Core1修改X,线程B在Core2上修改Y。根据MESI大法,假设是Core1是第一个发起操作的CPU核,Core1上的L1 Cache Line由S(共享)状态变成M(修改,脏数据)状态,然后告知其他的CPU核,图例则是Core2,引用同一地址的Cache Line已经无效了;当Core2发起写操作时,首先导致Core1将X写回主存,Cache Line状态由M变为I(无效),而后才是Core2从主存重新读取该地址内容,Cache Line状态由I变成E(独占),最后进行修改Y操作, Cache Line从E变成M。可见多个线程操作在同一Cache Line上的不同数据,相互竞争同一Cache Line,导致线程彼此牵制影响,变成了串行程序,降低了并发性。此时我们则需要将共享在多线程间的数据进行隔离,使他们不在同一个Cache Line上,从而提升多线程的性能。

    Cache Line伪共享处理方案

    处理伪共享的两种方式:

    1. 增大数组元素的间隔使得不同线程存取的元素位于不同的cache line上。典型的空间换时间。(Linux cache机制与之相关)
    2. 在每个线程中创建全局数组各个元素的本地拷贝,然后结束后再写回全局数组。

    在Java类中,最优化的设计是考虑清楚哪些变量是不变的,哪些是经常变化的,哪些变化是完全相互独立的,哪些属性一起变化。举个例子:

    public class Data{
        long modifyTime;
        boolean flag;
        long createTime;
        char key;
        int value;
    }

    假如业务场景中,上述的类满足以下几个特点:

    1. 当value变量改变时,modifyTime肯定会改变
    2. createTime变量和key变量在创建后,就不会再变化。
    3. flag也经常会变化,不过与modifyTime和value变量毫无关联。

    当上面的对象需要由多个线程同时的访问时,从Cache角度来说,就会有一些有趣的问题。当我们没有加任何措施时,Data对象所有的变量极有可能被加载在L1缓存的一行Cache Line中。在高并发访问下,会出现这种问题:

    图片描述

    如上图所示,每次value变更时,根据MESI协议,对象其他CPU上相关的Cache Line全部被设置为失效。其他的处理器想要访问未变化的数据(key 和 createTime)时,必须从内存中重新拉取数据,增大了数据访问的开销。

    Padding 方式

    正确的方式应该将该对象属性分组,将一起变化的放在一组,与其他属性无关的属性放到一组,将不变的属性放到一组。这样当每次对象变化时,不会带动所有的属性重新加载缓存,提升了读取效率。在JDK1.8以前,我们一般是在属性间增加长整型变量来分隔每一组属性。被操作的每一组属性占的字节数加上前后填充属性所占的字节数,不小于一个cache line的字节数就可以达到要求:

    public class DataPadding{
        long a1,a2,a3,a4,a5,a6,a7,a8;//防止与前一个对象产生伪共享
        int value;
        long modifyTime;
        long b1,b2,b3,b4,b5,b6,b7,b8;//防止不相关变量伪共享;
        boolean flag;
        long c1,c2,c3,c4,c5,c6,c7,c8;//
        long createTime;
        char key;
        long d1,d2,d3,d4,d5,d6,d7,d8;//防止与下一个对象产生伪共享
    }

    通过填充变量,使不相关的变量分开

    Contended注解方式

    在JDK1.8中,新增了一种注解@sun.misc.Contended,来使各个变量在Cache line中分隔开。注意,jvm需要添加参数-XX:-RestrictContended才能开启此功能 
    用时,可以在类前或属性前加上此注释:

    // 类前加上代表整个类的每个变量都会在单独的cache line中
    @sun.misc.Contended
    @SuppressWarnings("restriction")
    public class ContendedData {
        int value;
        long modifyTime;
        boolean flag;
        long createTime;
        char key;
    }
    或者这种:
    // 属性前加上时需要加上组标签
    @SuppressWarnings("restriction")
    public class ContendedGroupData {
        @sun.misc.Contended("group1")
        int value;
        @sun.misc.Contended("group1")
        long modifyTime;
        @sun.misc.Contended("group2")
        boolean flag;
        @sun.misc.Contended("group3")
        long createTime;
        @sun.misc.Contended("group3")
        char key;
    }

    采取上述措施图示:

    图片描述

    JDK1.8 ConcurrentHashMap的处理

    java.util.concurrent.ConcurrentHashMap在这个如雷贯耳的Map中,有一个很基本的操作问题,在并发条件下进行++操作。因为++这个操作并不是原子的,而且在连续的Atomic中,很容易产生伪共享(false sharing)。所以在其内部有专门的数据结构来保存long型的数据:

    
    (openjdk\jdk\src\share\classes\java\util\concurrent\ConcurrentHashMap.java line:2506):
    
        /* ---------------- Counter support -------------- */
    
        /**
         * A padded cell for distributing counts.  Adapted from LongAdder
         * and Striped64.  See their internal docs for explanation.
         */
        @sun.misc.Contended static final class CounterCell {
            volatile long value;
            CounterCell(long x) { value = x; }
        }

    我们看到该类中,是通过@sun.misc.Contended达到防止false sharing的目的

    JDK1.8 Thread 的处理

    java.lang.Thread在java中,生成随机数是和线程有着关联。而且在很多情况下,多线程下产生随机数的操作是很常见的,JDK为了确保产生随机数的操作不会产生false sharing ,把产生随机数的三个相关值设为独占cache line。

    (openjdk\jdk\src\share\classes\java\lang\Thread.java line:2023// The following three initially uninitialized fields are exclusively
        // managed by class java.util.concurrent.ThreadLocalRandom. These
        // fields are used to build the high-performance PRNGs in the
        // concurrent code, and we can not risk accidental false sharing.
        // Hence, the fields are isolated with @Contended.
    
        /** The current seed for a ThreadLocalRandom */
        @sun.misc.Contended("tlr")
        long threadLocalRandomSeed;
    
        /** Probe hash value; nonzero if threadLocalRandomSeed initialized */
        @sun.misc.Contended("tlr")
        int threadLocalRandomProbe;
    
        /** Secondary seed isolated from public ThreadLocalRandom sequence */
        @sun.misc.Contended("tlr")
        int threadLocalRandomSecondarySeed;
    

    Java中对Cache line经典设计

    Disruptor框架

    认识Disruptor

    LMAX是在英国注册并受到FCA监管的外汇黄金交易所。也是欧洲第一家也是唯一一家采用多边交易设施Multilateral Trading Facility(MTF)拥有交易所牌照和经纪商牌照的欧洲顶级金融公司。LMAX的零售金融交易平台,是建立在JVM平台上,核心是一个业务逻辑处理器,它能够在一个线程里每秒处理6百万订单。业务逻辑处理器的核心就是Disruptor(注,本文Disruptor基于当前最新3.3.6版本),这是一个Java实现的并发组件,能够在无锁的情况下实现网络的Queue并发操作,它确保任何数据只由一个线程拥有以进行写访问,从而消除写争用的设计, 这种设计被称作“破坏者”,也是这样命名这个框架的。

    Disruptor是一个线程内通信框架,用于线程里共享数据。与LinkedBlockingQueue类似,提供了一个高速的生产者消费者模型,广泛用于批量IO读写,在硬盘读写相关的程序中应用的十分广泛,Apache旗下的HBase、Hive、Storm等框架都有在使用Disruptor。LMAX 创建Disruptor作为可靠消息架构的一部分,并将它设计成一种在不同组件中共享数据非常快的方法。Disruptor运行大致流程入下图:

    图片描述

    图中左侧(Input Disruptor部分)可以看作多生产者单消费者模式。外部多个线程作为多生产者并发请求业务逻辑处理器(Business Logic Processor),这些请求的信息经过Receiver存放在粉红色的圆环中,业务处理器则作为消费者从圆环中取得数据进行处理。右侧(Output Disruptor部分)则可看作单生产者多消费者模式。业务逻辑处理器作为单生产者,发布数据到粉红色圆环中,Publisher作为多个消费者接受业务逻辑处理器的结果。这里两处地方的数据共享都是通过那个粉红色的圆环,它就是Disruptor的核心设计RingBuffer。

    Disruptor特点

    1. 无锁机制。
    2. 没有CAS操作,避免了内存屏障指令的耗时。
    3. 避开了Cache line伪共享的问题,也是Disruptor部分主要关注的主题。

    Disruptor对伪共享的处理

    RingBuffer类

    RingBuffer类(即上节中粉红色的圆环)的类关系图如下:

    图片描述

    通过源码分析,RingBuffer的父类,RingBufferFields采用数组来实现存放线程间的共享数据。下图,第57行,entries数组。

    图片描述

    前面分析过数组比链表、树更具有缓存友好性,此处不做细表。不使用LinkedBlockingQueue队列,是基于无锁机制的考虑。详细分析可参考,并发编程网的翻译。这里我们主要分析RingBuffer的继承关系中的填充,解决缓存伪共享问题。如下图: 
    图片描述

    图片描述

    依据JVM对象继承关系中父类属性与子类属性,内存地址连续排列布局,RingBufferPad的protected long p1,p2,p3,p4,p5,p6,p7;作为缓存前置填充,RingBuffer中的protected long p1,p2,p3,p4,p5,p6,p7;作为缓存后置填充。这样任意线程访问RingBuffer时,RingBuffer放在父类RingBufferFields的属性,都是独占一行Cache line不会产生伪共享问题。如图,RingBuffer的操作字段在RingBufferFields中,使用rbf标识:

    图片描述
    按照一行缓存64字节计算,前后填充56字节(7个long),中间大于等于8字节的内容都能独占一行Cache line,此处rbf是大于8字节的。

    Sequence类

    Sequence类用来跟踪RingBuffer和事件处理器的增长步数,支持多个并发操作包括CAS指令和写指令。同时使用了Padding方式来实现,如下为其类结构图及Padding的类。

    Sequence里在volatile long value前后放置了7个long padding,来解决伪共享的问题。示意如图,此处Value等于8字节:

    也许读者应该会认为这里的图示比上面RingBuffer的图示更好理解,这里的操作属性只有一个value,两个图相互结合就更能理解了。

    Sequencer的实现

    在RingBuffer构造函数里面存在一个Sequencer接口,用来遍历数据,在生产者和消费者之间传递数据。Sequencer有两个实现类,单生产者模式的实现SingleProducerSequencer与多生产者模式的实现MultiProducerSequencer。它们的类结构如图:

    单生产者是在Cache line中使用padding方式实现,源码如下:

    图片描述

    多生产者则是使用 sun.misc.Unsafe来实现的。如下图:

    图片描述

    总结与使用示例

    可见padding方式在Disruptor中是处理伪共享常见的方式,JDK1.8的@Contended很好的解决了这个问题,不知道Disruptor后面的版本是否会考虑使用它。

    Disruptor使用示例代码https://github.com/EasonFeng5870/disruptor_demo

    参考资料:

    7个示例科普CPU Cache:http://coolshell.cn/articles/10249.html 
    Linux Cache 机制:http://www.cnblogs.com/liloke/archive/2011/11/20/2255737.html 
    《深入理解计算机系统》:第六章部分 
    Disruptor官方文档:https://github.com/LMAX-Exchange/disruptor/tree/master/docs 
    Disruptor并发编程网文档翻译:http://ifeve.com/disruptor/

    作者简介:

    上海-周卫理、北京-杨珍琪、北京-冯英圣、深圳-姜寄羽 倾力合作,另外感谢惠普系统架构师吴治辉策划支持。

    周卫理:本科,从事Java开发7年,热爱研究术问题,喜欢运动。目前就职于上海一家互联网公司,担任Java后端小组组长,负责分布式系统框架搭建。正往Java高性能编程,大数据中间件方向靠拢。

    冯英胜:长期从事Java软件开发工作,善于复杂业务开发,6年工作经验,对大数据平台和分布式架构等有浓厚兴趣。目前就职于北京当当网。

    姜寄羽:四川大学软件工程学士。目前在深圳亚略特担任Java工程师一职。负责Java方面的开发和维护以及新技术预研,对软件工程、分布式系统和高性能编程有着深厚的理论基础。

    杨珍琪:硕士,会计学士,前HP工程师,参与中国移动BOSS系统开发,现为创业公司CTO。


    展开全文
  • 如果我们选择多个单核CPU,那么每一个CPU都需要有较为独立的电路支持,有自己的Cache,而他们之间通过板上的总线进行通信。假如在这样的架构上,我们要跑一个多线程的程序(常见典型情况),不考虑超线程,那么...
  • 从Java视角理解CPU缓存和伪共享

    万次阅读 2017-01-05 10:14:17
    转载自:http://ifeve.com/from-javaeye-cpu-cache/ ...  CPU是计算机的大脑,它负责执行程序的指令;内存负责存数据,包括程序自身数据。内存比CPU慢很多,现在获取内存中的一条数据大概需要200多个CPU周期(CPU cycle
  • iphone的cpu对于处理视频来说能力是非常有限的,所以在ios开发中,如果要进行视频处理,比如滤镜、美颜等,都会用到设备的GPU能力,也就是会用到opengl es的api,而CPU和GPU之间的数据传递效率十分低下,尤其是从GPU...
  • CPU-Freq开发指南 V1.0.1》 下载地址:http://developer.t-firefly.com/thread-12533-1-1.html更多开发资料请到社区精华系列“资源共享”专栏下载http://dev.t-firefly.com/forum-263-1.html——————————...
  • Java 中的伪共享详解及解决方案

    千次阅读 2020-01-09 22:05:44
    1. 什么是伪共享 CPU 缓存系统中是以缓存行(cache line)为单位存储的。目前主流的 CPU Cache 的 Cache Line 大小都是 64 Bytes...由于共享变量在 CPU 缓存中的存储是以缓存行为单位,一个缓存行可以存储多个变量...
  •   什么是伪共享 CPU缓存系统中是以缓存行(cache line)为...在多线程情况下,如果需要修改“共享同一个缓存行的变量”,就会无意中影响彼此的性能,这就是伪共享(False Sharing)。 CPU的三级缓存 由于...
  • Compile ffmpeg for Android Before Continuing this page, if u want to enable x264, please complete building x264 using android toolchain first.... this. Please put x264 folder at same level of
  • 文章目录1、 CPU缓存2、 总线锁和缓存锁3、 缓存行4、 缓存一致性协议(如:MESI)5、 伪共享(false sharing)问题6、 伪共享解决方案(如:缓存行填充)6.1 Disruptor为什么这么快?6.2 实验证明6.3 Jdk8中自带...
  • 单核CPU与多核CPU工作原理

    万次阅读 多人点赞 2018-06-21 16:43:43
    单核CPU与多核CPU工作原理今天要介绍一下单核CPU和单核(超时间轴)CPU以及多核CPU的工作原理 一.单核CPU起源:CPU的起源是1971年英特尔公司推出,由此正式宣告计算机的诞生,可以说是世界级的改变,并且严格准守冯...
  • 什么是缓存 一台电脑有两种内存 一种是在RAM模块中使用的DRAM(Dynamic RAM),使用电容器来存储数据的内存需要动态地被电流刷新才能存储数据 ...三级缓存也叫共享缓存,因为它的内存在所有CPU核心间共享
  • CPU卡/CPU的分类/CPU卡标准/CPU卡生产流程   CPU卡芯片通俗地讲就是指芯片内含有一个微处理器,它的功能相当于一台微型计算机。人们经常使用的集成电路卡(IC卡)上的金属片就是CPU卡芯片。CPU卡可适用于金融、...
  • 1. 什么是伪共享 CPU缓存系统中是以缓存行(cache line)为单位存储的。目前主流的CPU Cache的Cache Line大小都是64...由于共享变量在CPU缓存中的存储是以缓存行为单位,一个缓存行可以存储多个变量(存满当前缓存...
  • 单核CPU与多核CPU的区别

    万次阅读 2019-09-03 21:00:17
    1、单核CPU 1.1 工作原理 工作原理:单核CPU较为死脑,在通电时该CPU就会执行存储块中的指定地址的指令,如果你想要执行内存块其他地方的代码必须调整总线位置才可以让其执行。单核CPU就相当于阻塞程序,在工作...
  • cpu调度

    千次阅读 2015-10-30 18:59:53
    CPU调度 用于多道程序 以下先讨论对于单CPU的调度问题。 回顾多道程序,同时把多个进程导入内存,使得一个进程在CPU中执行I/O时,一个进程用来填补CPU的时间。 通常进程都是在CPU区间和I/O区间之间转换。 CPU...
  • 两个协程可能在不同的CPU上运行,假设第一个协程在CPU1上运行,第二个协程在CPU2上运行,但是由于每个CPU都会缓存,只有在必要的时候才会写入主内存,其他的CPU才能看到。 CPU1对 x 进行了修改,但是它只是在自己...
  • ㈠ 概念  ① 物理CPU   实际Server中插槽上的CPU个数  物理cpu数量,可以数不重复的 physical id 有几个   ② 逻辑CPU   Linux用户对 /proc/cpu

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 443,068
精华内容 177,227
关键字:

共享cpu