无声的性能杀手——伪共享(False Sharing)
性能杀手
缓存系统中是以缓存行(cache line)为单位存储的。缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。缓存行上的写竞争是运行在SMP系统中并行线程实现可伸缩性最重要的限制因素。有人将伪共享描述成无声的性能杀手,因为从代码中很难看清楚是否会出现伪共享。
为了让可伸缩性与线程数呈线性关系,就必须确保不会有两个线程往同一个变量或缓存行中写。两个线程写同一个变量可以在代码中发现。为了确定互相独立的变量是否共享了同一个缓存行,就需要了解内存布局,或找个工具告诉我们。Intel VTune就是这样一个分析工具。本文中我将解释Java对象的内存布局以及我们该如何填充缓存行以避免伪共享。
上图说明了伪共享的问题。在Core1上运行的线程想更新变量X,同时Core2上的线程想要更新变量Y。不幸的是,这两个变量在同一个缓存行中。每个线程都要去竞争缓存行的所有权来更新变量。如果Core1获得了所有权,缓存子系统将会使Core2中对应的缓存行失效。当Core2获得了所有权然后执行更新操作,Core1就要使自己对应的缓存行失效。这会来来回回的经过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)
- booleans (1) 和 bytes (1)
- references (4/8)
- <子类字段重复上述顺序>
了解这些之后就可以在任意字段间用7个long来填充缓存行。在Disruptor里我们对RingBuffer的cursor和BatchEventProcessor的序列进行了缓存行填充。
为了展示其性能影响,我们启动几个线程,每个都更新它自己独立的计数器。计数器是volatile long类型的,所以其它线程能看到它们的进展。
public final class FalseSharing implements Runnable { public final static int NUM_THREADS = 4; // 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 { 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 } }
结果(Results)
运行上面的代码,增加线程数以及添加/移除缓存行的填充,下面的图描述了我得到的结果。这是在我4核Nehalem上测得的运行时间。
从不断上升的测试所需时间中能够明显看出伪共享的影响。没有缓存行竞争时,我们几近达到了随着线程数的线性扩展。
这并不是个完美的测试,因为我们不能确定这些VolatileLong会布局在内存的什么位置。它们是独立的对象。但是经验告诉我们同一时间分配的对象趋向集中于一块。
所以你也看到了,伪共享可能是无声的性能杀手。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Android | 教你如何用三十分钟在安卓上开发一个微笑抓拍神器
前言 前段时间Richard Yu在发布会上给大家介绍了华为HMS Core4.0,回顾发布会信息请戳: 华为面向全球发布HMS Core 4.0意味着什么? 其中有一个重点被介绍的服务,机器学习服务(Machine Learning Kit 简称 MLKit)。 那机器学习服务能干什么呢?能帮助开发者解决应用开发过程中的哪些问题? 今天就抛砖引玉一下,以人脸检测为例,给大家出一个实战小样例,让大家感受下机器学习服务所提供的强大功能以及给开发者提供的便捷性。 机器学习服务人脸检测所提供的能力 先给大家看一下华为机器学习服务人脸检测能力的展示: 从这个动图里面可以看到,人脸识别可以支持识别人脸的朝向,支持检测人脸的表情(高兴、厌恶、惊讶、伤心、愤怒、生气),支持检测人脸属性(性别、年龄、穿戴),支持检测是否睁眼闭眼,支持人脸以及鼻子、眼睛、嘴唇、眉毛等特征的坐标检测,另外还支持多人脸同时检测,是不是很强大! 核心提示:此功能免费,安卓全机型覆盖! 多人脸微笑拍照功能开发实战 今天就用机器学习服务的多人脸识别+表情检测能力写一个微笑抓拍的小demo,做一次实...
- 下一篇
spring cloud系列教程第六篇-Eureka集群版
spring cloud系列教程第六篇-Eureka集群版 本文主要内容: 本文来源:本文由凯哥Java(kaigejava)发布在龙果博客的。转载请注明 1:Eureka执行步骤理解 2:集群原理 3:Eureka集群搭建 4:修改payment和order项目注册到集群中 本文是由凯哥(凯哥Java:kagejava)发布的《spring cloud系列教程》教程的总第六篇:《spring cloud系列教程第六篇-Eureka集群版》。 本文是几个维度中的第一个维度:注册与发现维度配置中心管理之Eureka相关教程第三篇。 一:eureka注册与发现步骤 服务注册:将服务信息注册到注册中心 服务发现:从注册中心上获取到服务信息 其实质就是:key-value形式的。Key:服务的名字 value:服务调用地址 执行步骤: 1:先启动eureka注册中心 2:启动服务提供者(我们这里的服务提供者就是payment支付服务) 3:服务提供者在启动后会把自身的信息(如服务地址,以别名方式注册到)注册到eureka中 4:消费者(我们这里是order服务)在需要调用接口的时候,使用服务别...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- CentOS7安装Docker,走上虚拟化容器引擎之路
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- CentOS8安装Docker,最新的服务器搭配容器使用
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- Red5直播服务器,属于Java语言的直播服务器
- CentOS6,CentOS7官方镜像安装Oracle11G
- Hadoop3单机部署,实现最简伪集群
- SpringBoot2整合Thymeleaf,官方推荐html解决方案