剖析Disruptor:为什么会这么快?(三)伪共享

开发 后端
缓存行上的写竞争是运行在SMP系统中并行线程实现可伸缩性最重要的限制因素。有 人将伪共享描述成无声的性能杀手,因为从代码中很难看清楚是否会出现伪共享。

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

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

cache-line.png

图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. <子类字段重复上述顺序>

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

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

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

结果(Results)

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

duration.png

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

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

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

原文链接:http://ifeve.com/false-sharing/

译文链接:http://ifeve.com/falsesharing/

责任编辑:陈四芳 来源: ifeve.com
相关推荐

2013-06-19 10:55:40

Disruptor并发框架

2013-06-17 14:41:10

Disruptor并发编程

2013-06-18 10:30:45

Disruptor框架

2020-03-30 15:05:46

Kafka消息数据

2020-02-27 15:44:41

Nginx服务器反向代理

2024-02-26 21:15:20

Kafka缓存参数

2020-02-27 21:03:30

调度器架构效率

2012-08-17 10:01:07

云计算

2023-08-29 07:46:08

Redis数据ReHash

2021-05-27 20:56:51

esbuild 工具JavaScript

2020-10-15 09:19:36

Elasticsear查询速度

2023-03-21 08:02:36

Redis6.0IO多线程

2020-10-21 09:17:52

Redis面试内存

2020-04-27 07:13:37

Nginx底层进程

2021-03-18 14:34:34

达达集团京东云电商

2022-01-04 08:54:32

Redis数据库数据类型

2023-11-02 10:22:29

gRPC后端通信

2017-06-06 16:30:55

戴尔交付保障

2021-06-27 22:48:28

Redis数据库内存

2019-06-17 14:20:51

Redis数据库Java
点赞
收藏

51CTO技术栈公众号