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-2020-1 20175228 实验四 外设驱动程序设计
    2019-2020-1 20175228 实验三 实时系统
    2019-2020-1-20175332 20175323 20175228 实验一开发环境的熟悉
    2018-2019-2 20175228实验五《Java网络编程》实验报告
    2018-2019-2 20175228实验四《Android开发基础》实验报告
    2018-2019-2 20175228实验三《敏捷开发与XP实践》实验报告
    MyCP
    2018-2019-2 20175228实验二《面向对象程序设计》实验报告
    2018-2019-2 20175228实验一《Java开发环境的熟悉》实验报告
    转()析构函数
  • 原文地址:https://www.cnblogs.com/zhaoxinshanwei/p/8144531.html
Copyright © 2011-2022 走看看