写Java也得了解CPU--伪共享
第一次接触伪共享的概念,是在马丁的博客上;而ifeve也把这一系列博文翻译整理好了。概读了几次,感觉到此概念的重要。因此有了这个系列的第二篇读后总结。
1. 什么是伪共享(False sharing)
在上一篇博文知道,缓存的存储方式,是以缓存行(Cache Line)为单位的。一般缓存行的大小是64字节。这意味着,小于64字节的变量,是有可能存在于同一条缓存行的。例如变量X大小32字节,变量Y大小32字节,那么他们有可能会存在于一条缓存行上。
根据马丁博客上的定义,伪共享,就是多个线程同时修改共享在同一个缓存行里的独立变量,无意中影响了性能。
2.伪共享是怎么发生的
借助马丁的图,我们可以窥知伪共享发生的过程。
当核心1上的线程想更新X,而核心2上的线程想更新Y,而X变量和Y变量在同一个缓存行中时;每个线程都要去竞争缓存行的所有权来更新变量。如果核心1获得所缓存行的所有权,那么缓存子系统将会使核心2中对应的缓存行失效,反之亦然。这会来来回回的经过L3缓存,大大影响了性能。这种情况,就像多个线程同事竞争锁的所有权一样。如果互相竞争的核心位于不同的插槽,就要额外横跨插槽连接,问题可能更加严重。
3. 怎么发现伪共享
很遗憾,没有特别直接有效的方法。马丁自己也承认,伪共享相当难发现,因此有“无声性能杀手”之称。但这不意味着无法发现。通过观察L2和L3的缓存命中和丢失的情况,可以从侧面发现是否有伪共享的发生。
4. 怎么解决伪共享
对于伪共享这种影响性能的问题,解决是关键。解决伪共享的方法是通过补齐(Padding),使得每一条缓存行只存一个多线程变量。请看下面的代码:
- public final class FalseSharing implements Runnable {
- public final static int NUM_THREADS = 2; // change
- public final static long ITERATIONS = 500L * 1000L * 1000L;
- private final int arrayIndex;
- private static VolatileLong[] longs = new VolatileLong[NUM_THREADS];
- static {
- for (int i = 0; i < longs.length; i++) {
- longs[i] = new VolatileLong();
- }
- }
- public FalseSharing(final int arrayIndex) {
- this.arrayIndex = arrayIndex;
- }
- public static void main(final String[] args) throws Exception {
- //test the size of VolatileLong
- System.out.println(ClassLayout.parseClass(VolatileLong.class).toPrintable());
- final long start = System.nanoTime();
- runTest();
- System.out.println("duration = " + (System.nanoTime() - start));
- }
- private static void runTest() throws InterruptedException {
- Thread[] threads = new Thread[NUM_THREADS];
- for (int i = 0; i < threads.length; i++) {
- threads[i] = new Thread(new FalseSharing(i));
- }
- for (Thread t : threads) {
- t.start();
- }
- for (Thread t : threads) {
- t.join();
- }
- }
- public void run() {
- long i = ITERATIONS + 1;
- while (0 != --i) {
- longs[arrayIndex].value = i;
- }
- }
- public final static class VolatileLong {
- public volatile long value = 0L;
- public long p1, p2, p3, p4, p5, p6; // comment out in order to trigger false sharing
- }
- }
改变线程的数量以及运行此程序(基于Intel Xeon E31270 8GB/64bit Win7/64bit jdk 6),会得到以下的结果:
这个结果没有马丁的结果那么惊人,伪共享在3-4线程的时候会比较明显地影响性能。这个结果后面还会继续分析。然而这并不是个完美的测试,因为我们不能确定这些VolatileLong会布局在内存的什么位置。它们是独立的对象。但是经验告诉我们同一时间分配的对象趋向集中于一块。
5.一些思考
上面是马丁对伪共享的初步解释。说实话,解释得略微简略了一点。读了几次,还是有不太明白的地方,因此厚着脸皮在这里发了个帖子问点疑惑,结果得到马丁本人,Nitsan和Peter等众大神的回答,收益匪浅。下面摘录点我的疑惑和他们的解答,更好的理解伪共享:
5.1 上文得知,L1缓存和L2缓存是核心私有的缓存,不同的核心并不共享L1和L2缓存。为什么核心1更新它自己L1上的X,而核心2更新它自己L1上的Y,会发生伪共享?
要回答这个问题,首先得稍微了解CPU缓存工作的协议,MESI。这套协议是用来保证CPU缓存的一致性的(cache coherency)。简单来说,这协议定义了多级缓存下的同一个变量改变后,该怎么办。这套协议相当复杂,这里只是介绍伪共享相关的知识点,来回答我们的问题。
我们知道,缓存的最小使用单位,是缓存行。如上面所假设,变量X和变量Y不幸在同一个缓存行里,而核心1需要X,核心2需要Y。这时候,核心1就会拷贝这条缓存行到自己的L1,核心2也一样。所以这条缓存行在L3,核心1的L1和核心2的L1里,正如上图所示。
假设核心1修改变量X,那么根据MESI协议,这个缓存行的状态就会变成M(Modified),表面这一行数据和内存数据不一致,得回写(write back)此缓存行到L3里。而这时,需要发送一个Request For Ownership (RFO),来获得L3的这条缓存行的所有权。由于X和Y在同一条缓存行,虽然核心2修改的变量是Y,但也需要做同样的事情-发送RFO获得L3同一条缓存行的所有权。因此,伪共享就这样在L3里发生了。
5.2 补齐(Padding)会不会有失效的时候?
会的。Java 7淘汰或是重新排列了无用的字段,因此上述的补齐在Java 7里已经失效了,伪共享还会发生。要避免伪共享,需要改变补齐的方式如下:
- public static long sumPaddingToPreventOptimisation(final int index)
- {
- PaddedAtomicLong v = longs[index];
- return v.p1 + v.p2 + v.p3 + v.p4 + v.p5 + v.p6;
- }
- public static class PaddedAtomicLong extends AtomicLong
- {
- public volatile long p1, p2, p3, p4, p5, p6 = 7L;
- }
这个方法的由来在这里,并不打算深究。要注意的是,这个方法sumPaddingToPreventOptimisation只是用来防止JVM7消除无用的字段。
5.3 怎么计算VolatileLong的大小?为什么这样补齐可以使它符合缓存行64字节大小?
理论上,我们知道在64bit Hotspot JVM下,一个long占8字节之类的知识。但实际的对象占多少字节,怎么分布,得依靠这个工具--JOL来测量。下面是补齐前和补齐后,VolatileLong的输出:
VolatileLong object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 (alignment/padding gap) N/A
16 8 long VolatileLong.value N/A
Instance size: 24 bytes (estimated, the sample instance is not available)
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
VolatileLong object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 (alignment/padding gap) N/A
16 8 long VolatileLong.value N/A
24 8 long VolatileLong.p1 N/A
32 8 long VolatileLong.p2 N/A
40 8 long VolatileLong.p3 N/A
48 8 long VolatileLong.p4 N/A
56 8 long VolatileLong.p5 N/A
64 8 long VolatileLong.p6 N/A
Instance size: 72 bytes (estimated, the sample instance is not available)
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
这样,我们可以看到补齐前,VolatileLong只有24字节小于缓存行大小,补齐后就超过缓存行大小。
5.4 补齐的对象超过了缓存行,有没有影响会不会和接下来的变量发生潜在的伪共享?
假设补齐后,VolatileLong是72字节,紧接着刚好有一个变量Z是刚好56个字节,那么第二个缓存行存放着VolatileLong的8字节那一部分,以及变量Z。那么同时访问VolatileLong和Z,会不会发生伪共享呢?是不是一定要补齐到缓存行大小才完全避免伪共享呢?
答案是否定的,补齐超过缓存行,最多浪费点珍贵的缓存,但不会产生伪共享。请看下面的图:
| 8b | 16 | 24 | 32 | 40 | 48 | 56 | 64 |
5.5 为何上述测试8线程的结果回比4线程的结果要好?
6. 最后
本文完
写Java也得了解CPU--伪共享的更多相关文章
- 写Java也得了解CPU–CPU缓存
CPU,一般认为写C/C++的才需要了解,写高级语言的(Java/C#/pathon…)并不需要了解那么底层的东西.我一开始也是这么想的,但直到碰到LMAX的Disruptor,以及马丁的博文,才发现 ...
- 从Java视角理解CPU缓存和伪共享
转载自:http://ifeve.com/from-javaeye-cpu-cache/ http://ifeve.com/from-javaeye-false-shari ...
- java高并发核心要点|系列5|CPU内存伪共享
上节提到的:伪共享,今天我们来说说. 那什么是伪共享呢? 这得从CPU的缓存结构说起.以下如图,CPU一般来说是有三级缓存,1 级,2级,3级,越上面的,越靠近CPU的,速度越快,成本也越高.也就是说 ...
- java 伪共享
MESI协议及RFO请求典型的CPU微架构有3级缓存, 每个核都有自己私有的L1, L2缓存. 那么多线程编程时, 另外一个核的线程想要访问当前核内L1, L2 缓存行的数据, 该怎么办呢?有人说可以 ...
- Java 中的伪共享详解及解决方案
1. 什么是伪共享 CPU 缓存系统中是以缓存行(cache line)为单位存储的.目前主流的 CPU Cache 的 Cache Line 大小都是 64 Bytes.在多线程情况下,如果需要修改 ...
- 伪共享和缓存行填充,从Java 6, Java 7 到Java 8
关于伪共享的文章已经很多了,对于多线程编程来说,特别是多线程处理列表和数组的时候,要非常注意伪共享的问题.否则不仅无法发挥多线程的优势,还可能比单线程性能还差.随着JAVA版本的更新,再各个版本上减少 ...
- java中伪共享问题
伪共享(False Sharing) 原文地址:http://ifeve.com/false-sharing/ 作者:Martin Thompson 译者:丁一 缓存系统中是以缓存行(cache l ...
- 关于java中的伪共享的认识和解决
在并发编程过程中,我们大部分的焦点都放在如何控制共享变量的访问控制上(代码层面),但是很少人会关注系统硬件及 JVM 底层相关的影响因素: CPU缓存 网页浏览器为了加快速度,会在本机存缓存以前浏览过 ...
- 伪共享 FalseSharing (CacheLine,MESI) 浅析以及Java里的解决方案
起因 在阅读百度的发号器 uid-generator 源码的过程中,发现了一段很奇怪的代码: /** * Represents a padded {@link AtomicLong} to preve ...
随机推荐
- 利用SCORE法则来总结一次偷懒的单元测试过程
最近遇到一个单元测试的问题,本周正好学个了一个SCORE法则,这里正好练练手应用此法则将问题的前因后果分享给大家. S:背景 代码要有单元测试,检测的标准就是统计代码的单元测试覆盖率,程序员需要达到 ...
- 断言与异常(Assertion Vs Exception)
在日常编程实践中,断言与异常的界限不是很明显,这也使得它们常常没有被正确的使用.我也在不断的与这个模糊的怪兽搏斗,仅写此文和大家分享一下我的个人看法.我想我们还可以从很多角度来区别断言和异常的使用场景 ...
- 兼容 IE input button 左右内边距的增加
IE 下的按钮标签(input.button)会随着文字的增加导致文本左右两侧的间距越来越大.该问题存在于 IE6/IE7,IE8~IE10 没发现类似问题.一般情况下,只要给按钮标签设置 overf ...
- 配置 Oracle 11g侦听器来使用SQL操作ST_Geometry(DLL路径问题)
注:http://resources.arcgis.com/zh-cn/help/main/10.2/index.html#/na/00qn0000001p000000/ (ArcGIS 帮助库) 1 ...
- [Animatable Properties](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreAnimation_guide/AnimatableProperties/AnimatableProperties.html)
 
- iOS RESideMenu 侧滑 第三方类库
下载地址:https://github.com/romaonthego/RESideMenu 效果如下:官方案例 自己的实现效果 具体代码下: AppDelegate.m文件中 - (BOOL)app ...
- js(javascript)与OC(Objective-C)交互
实质上oc与js的通信交互就是发送消息,也即函数调用,iOS7以后官方公布JavaScriptCore framework中很方便我们对他们之间的相互调用.在以前我们只能通过UIWebView的UIW ...
- <<你的灯亮着吗?>>读书笔记
本书是美国计算机传奇人物杰拉尔德.温伯格和唐纳德.高斯所著,我在网上买到的2003年版的本书,发现本书用20则幽默的现代寓言故事,60幅精美插图,以及一系列的适当提问和建议,让我们的思考方式慢慢得以扩 ...
- ZooKeeper:Quick Start
下载.安装与配置 下载地址 安装 配置 ZooKeeper bin目录下脚本说明 Server 启动.停止 客户端操作 使用简易客户端访问 使用Java客户端访问 使用开源客户端ZkClient访问 ...
- 使用TortoiseGit 来进行日常操作
TortoiseSVN大家应该都很熟悉,相应的,git也有一个工具是TortoiseGit. 下载地址: http://download.tortoisegit.org/tgit/previews/ ...