False share 伪共享-Cache line Padding 缓存行填充 缓存行对齐

False share翻译成伪共享确实有点令人苦恼

 

Cache Line Padding

  • 缓存是以缓存行为最小单位
  • 一个缓存行可以存储多个不同的数据

        这些不同数据数据被不同线程中的任意一个修改之后,会导致整个缓存行失效,会导致其他线程更新缓存行,这就是低效的原因.

  • Cache line Padding技术就是保证每个缓存行只缓存一个数据

        这样虽然使缓存利用变低,但是会减少缓存频繁失效的问题

 

cache line (typical) size is 64 bytes

        L1,L2,L3

        Java中一个long8字节,所以一个缓存行可以存8个long类型

 

缓存行填充具体办法(假设Cache line 64Bytes):

  •     1. Java8提供了@Contentded注解(java8)进行缓存行填充

       

 @sun.misc.Contended

        class MyLong {

            volatile long value;

        }
  •     2. 比如两个long类型之间使用额外的6个long进行填充(具体要填充多少根据实际情况来定)

        Java 程序的对象头固定占 8 字节(32位系统)或 12 字节( 64 位系统默认开启压缩, 不开压缩为 16 字节),所以我们只需要填 6 个无用的长整型补上6*8=48字节,让不同的 VolatileLong 对象处于不同的缓存行,就避免了伪共享( 64 位系统超过缓存行的 64 字节也无所谓,只要保证不同线程不操作同一缓存行就可以)。

       

 class Pointer {

            volatile long x;

            long p1, p2, p3, p4, p5, p6, p7;

            volatile long y;

        }
  •     3. 自定义某些类型达到填充目的
  •     一旦跨平台,可能一切就无意义了

    

    

How to detect false sharing?

 无法从系统层面上通过工具来探测伪共享事件。其次,不同类型的计算机具有不同的微架构(如 32 位系统和 64 位系统的 java 对象所占自己数就不一样),如果设计到跨平台的设计,那就更难以把握了,一个确切的填充方案只适用于一个特定的操作系统。

 

Cross core 跨核访问缓存行

  •     需要Memeory controller支持,总线带宽有限,性能瓶颈
  •     RFO(Request For Owner) request:跨核访问同一个缓存行当需要写权限就会发送这个请求
  •     MESI协议

        貌似是intel的一个协议

        代表四个状态Modifyied,Exclusive,Shared,Invalid

        Exclusive:所有处理的缓存行都没有此行数据,从内存加载到此缓存行后设置成E状态,表示只此一家

        Shared:其他处理器有此行数据,则设置为S

        Invalid:缓存行没有加载任何数据

        Modified:写数据到I状态的缓存行,则置为M状态(处于M态的缓存行,本地处理器继续读写状态不会改变)

        远程读写(跨核访问缓存行),如果读就把此缓存行拷贝过去,然后置为S态,如果远程写需要RFO request,此时其他处理器缓存行设为I态,除了自己谁也不许动这行数据,RFO和I态设置会有大消耗

 

应用案例

    LinkedBlockingQueue它的last和head经常被不同线程修改,但却可能在同一个缓存行,某些java编译器就会补齐数据

    GC可能导致数据在内存和对应的CPU缓存行的位置发生变化所以padding时候要注意

    netty和grizzly的代码中的LinkedTransferQueue中都使用了PaddedAtomicReference来代替原来的Node, 使用了补齐的办法解决了队列伪共享的问题

    ConcurrentHashMap 里面的 size() 方法使用的是分段的思想来构造的,每个段使用的类是 CounterCell,它的类上就有 @sun.misc.Contended 注解。

    

    

扩展知识

    

    计算机的cpu物理核数是同时可以并行的线程数量(cpu只能看到线程,线程是cpu调度分配的最小单位),由于超线程技术,实际上可以并行的线程数量通常是物理核数的两倍,这也是操作系统看到的核数。我们只care可以并行的线程数量,所以之后所说的核数是操作系统看到的核数,所指的核也是超线程技术之后的那个核(不是物理核)。

    进程是操作系统资源分配(内存,显卡,磁盘)的最小单位,线程是执行调度(即cpu调度)的最小单位(cpu看到的都是线程而不是进程),一个进程可以有一个或多个线程,线程之间共享进程的资源,通过这样的范式,就可以减少进程的创建和销毁带来的代价,可以让进程少一点,保持相对稳定,不断去调度线程就好。如果计算机有多个cpu核,且计算机中的总的线程数量小于核数,那线程就可以并行运行在不同的核中,如果是单核多线程,那多线程之间就不是并行,而是并发,即为了均衡负载,cpu调度器会不断的在单核上切换不同的线程执行,但是我们说过,一个核只能运行一个线程,所以并发虽然让我们看起来不同线程之间的任务是并行执行的,但是实际上却由于增加了线程切换的开销使得代价更大了。如果是多核多线程,且线程数量大于核数,其中有些线程就会不断切换,并发执行,但实际上最大的并行数量还是当前这个进程中的核的数量,所以盲目增加线程数不仅不会让你的程序更快,反而会给你的程序增加额外的开销。

    from https://zhuanlan.zhihu.com/p/82123111

    

    

你可能感兴趣的:(计算机基础,缓存,并发,java)