2019独角兽企业重金招聘Python工程师标准>>>
我了解伪分享是在看Disruptor源码时开始的。
1. @Contented注解
JDK8中引入了@Contented,不过这个注解在sun包中,如下List-1
List-1
package sun.misc;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface Contended {
String value() default "";
}
这个注解只能在类属性、类上使用。
2. 我机器上CentOS的缓存行大小
下面的Centos版本是Centos7 64位,运行在我的虚拟机上,可用的cpu核数是4,可用的运行内存是8192M,即8G。
为了高效地存取缓存, 不是简单随意地将单条数据写入缓存的. 缓存是由缓存行组成的, 典型的一行是64字节,CentOS上可以查看到,如下:
List-2
[dmj@localhost ~]$ cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
64
来看下cache/index3到cache/index0的size变化,如下:
List-3 从index3到index0是逐渐变小(index0表示越接近CPU,index3表示离CPU越远)
[dmj@localhost ~]$ cat /sys/devices/system/cpu/cpu0/cache/index3/size
8192K
[dmj@localhost ~]$ cat /sys/devices/system/cpu/cpu0/cache/index2/size
256K
[dmj@localhost ~]$ cat /sys/devices/system/cpu/cpu0/cache/index1/size
32K
[dmj@localhost ~]$ cat /sys/devices/system/cpu/cpu0/cache/index0/size
32K
来看下cache/index0到cache/index3的level变化,如下:
List-4 index3的level大于index0的
[dmj@localhost ~]$ cat /sys/devices/system/cpu/cpu0/cache/index0/level
1
[dmj@localhost ~]$ cat /sys/devices/system/cpu/cpu0/cache/index1/level
1
[dmj@localhost ~]$ cat /sys/devices/system/cpu/cpu0/cache/index2/level
2
[dmj@localhost ~]$ cat /sys/devices/system/cpu/cpu0/cache/index3/level
3
来看下cache/index0到cache/index3的coherency_line_size值变化
List-5 值都是64
[dmj@localhost ~]$ cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
64
[dmj@localhost ~]$ cat /sys/devices/system/cpu/cpu0/cache/index1/coherency_line_size
64
[dmj@localhost ~]$ cat /sys/devices/system/cpu/cpu0/cache/index2/coherency_line_size
64
[dmj@localhost ~]$ cat /sys/devices/system/cpu/cpu0/cache/index3/coherency_line_size
64
来看下cache/index0到cache/index3的type
List-6 index0是Data、index1是Instruction、index2和index3都是Unified
[dmj@localhost ~]$ cat /sys/devices/system/cpu/cpu0/cache/index0/type
Data
[dmj@localhost ~]$ cat /sys/devices/system/cpu/cpu0/cache/index1/type
Instruction
[dmj@localhost ~]$ cat /sys/devices/system/cpu/cpu0/cache/index2/type
Unified
[dmj@localhost ~]$ cat /sys/devices/system/cpu/cpu0/cache/index3/type
Unified
根据上面的数据,我猜测我的CentOS7上的CPU和缓存架构如下:
图1 猜测的我Centos上的CPU、缓存结构图
注:图1仅仅是我猜测,很可能是错误的,建议读者自己去查看自己Linux系统中上述的值。
mac系统查看"L1 data cache line size"的命令如下,参考自google论坛:
List-7 我mac系统上,得到的值也是64
mjduan@mjduandeMacBook-Pro:/tmp % sysctl hw.cachelinesize
hw.cachelinesize: 64
CentOS上查看"L1 data cache line size"的命令如下,参考自google论坛:
List-8 我CentOS上,得到的值也是64
[dmj@localhost ~]$ getconf LEVEL1_DCACHE_LINESIZE
64
上面做的基本是铺垫,由我们自己验证得缓存行是64bytes,怎么确定单位是bytes,参考国外博客,之后我们进一步进入伪共享。我发现这篇51CTO博客讲的挺好的,这里就不再重复描述。关键点在于:假设俩个独立的变量x、y内存上位于同一个缓存行中,CPU0要对x进行操作,CPU1要对y进行操作。当CPU0加载换成到自己的L0缓存中,CPU1加载缓存行到自己的L0缓存中,之后CPU0修改自己L0缓存中的x,此时CPU1的L0缓存中的缓存行是失效了,所以此时CPU1不能对自己缓存行进行操作,CPU1不得不重新从L3或者主存加载该缓存行。所以CPU0和CPU1得不到并行执行。
经过上述讨论,引出一个问题,CPU1的L0中缓存行怎么知道,CPU0该缓存行中的某些数据,这个要了解下MESI协议,可以了解下这篇博客。
3.怎么避免伪分享
3.1 填充(不是很建议使用填充,用@Contented,可以参考这个)
"对于HotSpot JVM,所有对象都有两个字长的对象头。第一个字是由24位哈希码和8位标志位(如锁的状态或作为锁对象)组成的Mark Word。第二个字是对象所属类的引用。如果是数组对象还需要一个额外的字来存储数组的长度。每个对象的起始地址都对齐于8字节以提高性能",这段话来自于51CTO开发频道博客。这让我想起来《深入理解计算机》这本书中讲过变量声明顺序对内存的优化。
通常下给出的是填充缓存行,如下List-9。
List-9 "public long p1, p2, p3, p4, p5, p6; // comment out"这行就是填充使用的
public final static class VolatileLong {
public volatile long value = 0L;
public long p1, p2, p3, p4, p5, p6; // comment out
}
但是这个要求开发人员对计算机底层和JVM内存有深的了解,要优化,但是也要慎重:
- 没有达到优化的效果,反过来让程序代码可读性、可维护性变差。
- Java编译器会不会在编译时对代码进行优化?比如编译器在编译期间将"public long p1, p2, p3, p4, p5, p6; "移除导致伪分享又发生,因为这几个变量没有被使用到。这个我想可以通过Javap命令来查看,不了解Javap命令的同学可以去google/bing.com搜索下,这是java自带的命令。
- JVM的JustInTime,即JIT,在Java代码执行时会不会对代码进行优化?它在执行期间检测到"public long p1, p2, p3, p4, p5, p6; "这个变量没有被使用到,所以将这些移除,使得伪分享又发生。Stackoverflow上有人使用openJDK的jol分析了,结果说"JIT can't figure that some fields are unused and doesn't optimize them out."。不过这个我没有自己试验过,后面有时间再去验证。
个人主观的认为,没有被使用的局部变量很有可能被优化,但是没有被使用到的类属性被优化的可能性不是很大,注意,这个仅仅是主观的认为,没有试验验证过。
3.2 JDK的@Contented注解
JDK中的@Contented注解,这个的作用我没有仔细验证过,且这个注解是在sun.misc包中的,这个在开头给出了。Oracle的博客给出说这个注解可以一定程度上减少False sharing的发生。
注意经过本人验证,JDK8上加上@Contented注解是不会生效的,除非加上List-10中的JVM参数。我是怎么知道不加List-10的参数,@Contented不会生效的呢,请参考这里,链接里面的是个OpenJDK jol的例子,在运行是测试下加上和不加上List-10JVM参数的结果,运行结果可以看到占用内存bits位数的变化。
List-10
-XX:-RestrictContended
除了填充和@Contented外,很有可能有其它方法,建议读者多google/bing.com,也许能找到更充分的证据,最后是要自己验证。
最好是自己测试下,如果测试发现优化后确实效率高了,可以运行在生产上,特别是注意测试数据量大的情况和长时间运行的情况。