zoukankan      html  css  js  c++  java
  • 伪共享(False Sharing)

    原文地址:http://ifeve.com/false-sharing/

    作者:Martin Thompson  译者:丁一

    缓存系统中是以缓存行(cache line)为单位存储的。缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。缓存行上的写竞争是运行在SMP系统中并行线程实现可伸缩性最重要的限制因素。有人将伪共享描述成无声的性能杀手,因为从代码中很难看清楚是否会出现伪共享。

    为了让可伸缩性与线程数呈线性关系,就必须确保不会有两个线程往同一个变量或缓存行中写。两个线程写同一个变量可以在代码中发现。为了确定互相独立的变量是否共享了同一个缓存行,就需要了解内存布局,或找个工具告诉我们。Intel VTune就是这样一个分析工具。本文中我将解释Java对象的内存布局以及我们该如何填充缓存行以避免伪共享。

    cache-line.png
    图 1.

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

    Java内存布局(Java Memory Layout)

    对于HotSpot JVM,所有对象都有两个字长的对象头。第一个字是由24位哈希码和8位标志位(如锁的状态或作为锁对象)组成的Mark Word。第二个字是对象所属类的引用。如果是数组对象还需要一个额外的字来存储数组的长度。每个对象的起始地址都对齐于8字节以提高性能。因此当封装对象的时候为了高效率,对象字段声明的顺序会被重排序成下列基于字节大小的顺序:

    1. doubles (8) 和 longs (8)
    2. ints (4) 和 floats (4)
    3. shorts (2) 和 chars (2)
    4. booleans (1) 和 bytes (1)
    5. references (4/8)
    6. <子类字段重复上述顺序>

    (译注:更多HotSpot虚拟机对象结构相关内容:http://www.infoq.com/cn/articles/jvm-hotspot

    了解这些之后就可以在任意字段间用7个long来填充缓存行。在Disruptor里我们对RingBuffer的cursor和BatchEventProcessor的序列进行了缓存行填充。

    为了展示其性能影响,我们启动几个线程,每个都更新它自己独立的计数器。计数器是volatile long类型的,所以其它线程能看到它们的进展。

    01 public final class FalseSharing
    02     implements Runnable
    03 {
    04     public final static int NUM_THREADS = 4; // change
    05     public final static long ITERATIONS = 500L * 1000L * 1000L;
    06     private final int arrayIndex;
    07   
    08     private static VolatileLong[] longs = new VolatileLong[NUM_THREADS];
    09     static
    10     {
    11         for (int i = 0; i < longs.length; i++)
    12         {
    13             longs[i] = new VolatileLong();
    14         }
    15     }
    16   
    17     public FalseSharing(final int arrayIndex)
    18     {
    19         this.arrayIndex = arrayIndex;
    20     }
    21   
    22     public static void main(final String[] args) throws Exception
    23     {
    24         final long start = System.nanoTime();
    25         runTest();
    26         System.out.println("duration = " + (System.nanoTime() - start));
    27     }
    28   
    29     private static void runTest() throws InterruptedException
    30     {
    31         Thread[] threads = new Thread[NUM_THREADS];
    32   
    33         for (int i = 0; i < threads.length; i++)
    34         {
    35             threads[i] = new Thread(new FalseSharing(i));
    36         }
    37   
    38         for (Thread t : threads)
    39         {
    40             t.start();
    41         }
    42   
    43         for (Thread t : threads)
    44         {
    45             t.join();
    46         }
    47     }
    48   
    49     public void run()
    50     {
    51         long i = ITERATIONS + 1;
    52         while (0 != --i)
    53         {
    54             longs[arrayIndex].value = i;
    55         }
    56     }
    57   
    58     public final static class VolatileLong
    59     {
    60         public volatile long value = 0L;
    61         public long p1, p2, p3, p4, p5, p6; // comment out
    62     }
    63 }

    结果(Results)

    运行上面的代码,增加线程数以及添加/移除缓存行的填充,下面的图2描述了我得到的结果。这是在我4核Nehalem上测得的运行时间。

    duration.png
    图 2.

    从不断上升的测试所需时间中能够明显看出伪共享的影响。没有缓存行竞争时,我们几近达到了随着线程数的线性扩展。

    这并不是个完美的测试,因为我们不能确定这些VolatileLong会布局在内存的什么位置。它们是独立的对象。但是经验告诉我们同一时间分配的对象趋向集中于一块。

    所以你也看到了,伪共享可能是无声的性能杀手。

    注意:更多伪共享相关的内容,请阅读我后续blog

  • 相关阅读:
    2019.6.20刷题统计
    36 线程 队列 守护线程 互斥锁 死锁 可重入锁 信号量
    35 守护进程 互斥锁 IPC 共享内存 的方式 生产者消费者模型
    34 进程 pid ppid 并发与并行,阻塞与非阻塞 join函数 process对象 孤儿进程与僵尸进程
    33 udp 域名 进程
    32 粘包 文件传输
    31 socket客户端. 服务器 异常 语法
    30 网络编程
    29 元类 异常
    26 封装 反射 常用内置函数
  • 原文地址:https://www.cnblogs.com/zhaoxinshanwei/p/8144531.html
Copyright © 2011-2022 走看看