标签:word nis logical 处理器 nal time iss file 总线
维基百科中对伪共享的定义如下:
In computer science, false sharing is a performance-degrading usage pattern that can arise in systems with distributed,
coherent caches at the size of the smallest resource block managed by the caching mechanism. When a system participant attempts
to periodically access data that will never be altered by another party, but those data shares a cache block with data that are
altered, the caching protocol may force the first participant to reload the whole unit despite a lack of logical necessity. The
caching system is unaware of activity within this block and forces the first participant to bear the caching system overhead
required by true shared access of a resource.
其大致意思是:
CPU的缓存是以缓存行(cache line)为单位进行缓存的,当多个线程修改不同变量,而这些变量又处于同一个缓存行时就会影响彼此的性能。例如:线程1和线程2共享一个缓存行,线程1只读取缓存行中的变量1,线程2修改缓存行中的变量2,虽然线程1和线程2操作的是不同的变量,由于变量1和变量2同处于一个缓存行中,当变量2被修改后,缓存行失效,线程1要重新从主存中读取,因此导致缓存失效,从而产生性能问题。为了更深入一步理解伪共享,我们先看一下CPU缓存。
CPU的速度要远远大于内存的速度,为了解决这个问题,CPU引入了三级缓存:L1,L2和L3三个级别,L1最靠近CPU,L2次之,L3离CPU最远,L3之后才是主存。速度是L1>L2>L3>主存。越靠近CPU的容量越小。CPU获取数据会依次从三级缓存中查找,如果找不到再从主存中加载。示意图如下:
当CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。一般来说,每级缓存的命中率大概都在80%左右,也就是说全部数据量的80%都可以在一级缓存中找到,只剩下20%的总数据量才需要从二级缓存、三级缓存或内存中读取,由此可见一级缓存是整个CPU缓存架构中最为重要的部分。
下表是一些缓存未命中的消耗数据:
从CPU到 | 大约需要的CPU周期 | 大约需要的时间 |
---|---|---|
主存 | 约60-80ns | |
QPI总线 | 约20ns | |
L3 cache | 约40-45cycles | 约15ns |
L2 cache | 约10cycles | 约3ns |
L1 cache | 约3-4cycles | 约1ns |
寄存器 | 1cycle |
CPU的缓存是以缓存行(cache line)为单位的,MESI协议描述了多核处理器中一个缓存行的状态。在MESI协议中,每个缓存行有4个状态,分别是:
缓存行的E状态如下图:
此时只有core1访问缓存行,它的缓存行的状态为E,表示core1独占。
缓存行的S状态如下图:
此时core1和core2都会访问缓存行,他们的缓存行状态为S,表示缓存行处于共享状态。
缓存行的M和I状态如下图:
此时core1修改了缓存行,因此core1的缓存行状态为M,代表已经修改,而core2的缓存行状态为I,代表已经失效,需要从主存中读取。
在MESI协议中,每个Cache的Cache控制器不仅知道自己的读写操作,而且也监听(snoop)其它Cache的读写操作。每个Cache line所处的状态根据本核和其它核的读写操作在4个状态间进行迁移。MESI协议状态迁移图如下:
CPU缓存是以缓存行(cache line)为单位存储的。缓存行通常是 64 字节,并且它有效地引用主内存中的一块地址。一个 Java 的 long 类型是 8 字节,因此在一个缓存行中可以存 8 个 long 类型的变量。所以,如果你访问一个 long 数组,当数组中的一个值被加载到缓存中,它会额外加载另外 7 个,以致你能非常快地遍历这个数组。事实上,你可以非常快速的遍历在连续的内存块中分配的任意数据结构。而如果你在数据结构中的项在内存中不是彼此相邻的(如链表),你将得不到免费缓存加载所带来的优势,并且在这些数据结构中的每一个项都可能会出现缓存未命中。下图是一个CPU缓存行的示意图:
上图中,一个运行在处理器 core1上的线程想要更新变量 X 的值,同时另外一个运行在处理器 core2 上的线程想要更新变量 Y 的值。但是,这两个频繁改动的变量都处于同一条缓存行。两个线程就会轮番发送 RFO 消息,占得此缓存行的拥有权。当 core1 取得了拥有权开始更新 X,则 core2 对应的缓存行需要设为 I 状态。当 core2 取得了拥有权开始更新 Y,则 core1 对应的缓存行需要设为 I 状态(失效态)。轮番夺取拥有权不但带来大量的 RFO 消息,而且如果某个线程需要读此行数据时,L1 和 L2 缓存上都是失效数据,只有 L3 缓存上是同步好的数据。从前一篇我们知道,读 L3 的数据非常影响性能。更坏的情况是跨槽读取,L3 都要 miss,只能从内存上加载。
表面上 X 和 Y 都是被独立线程操作的,而且两操作之间也没有任何关系。只不过它们共享了一个缓存行,但所有竞争冲突都是来源于共享。
解决伪共享最直接的方法就是填充(padding),例如下面的VolatileLong,一个long占8个字节,Java的对象头占用8个字节(32位系统)或者12字节(64位系统,默认开启对象头压缩,不开启占16字节)。一个缓存行64字节,那么我们可以填充6个long(6 * 8 = 48 个字节)。这样就能避免多个VolatileLong共享缓存行。
public class VolatileLong {
private volatile long v;
// private long v0, v1, v2, v3, v4, v5 // 去掉注释,开启填充,避免缓存行共享
}
这是最简单直接的方法,Java 8中引入了一个更加简单的解决方案:@Contended
注解:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface Contended {
String value() default "";
}
Contended注解可以用于类型上和属性上,加上这个注解之后虚拟机会自动进行填充,从而避免伪共享。这个注解在Java8 ConcurrentHashMap、ForkJoinPool和Thread等类中都有应用。我们来看一下Java8中ConcurrentHashMap中如何运用Contended这个注解来解决伪共享问题。以下说的ConcurrentHashMap都是Java8版本。
ConcurrentHashMap的size操作通过CounterCell来计算,哈希表中的每个节点都对用了一个CounterCell,每个CounterCell记录了对应Node的键值对数目。这样每次计算size时累加各个CounterCell就可以了。<font color="#FF0000">ConcurrentHashMap中CounterCell以数组形式保存,而数组在内存中是连续存储的,CounterCell中只有一个long类型的value属性,这样CPU会缓存CounterCell临近的CounterCell,于是就形成了伪共享。</font>
ConcurrentHashMap中用Contended注解自动对CounterCell来进行填充:
/**
* Table of counter cells. When non-null, size is a power of 2.
*/
private transient volatile CounterCell[] counterCells; // CounterCell数组,CounterCell在内存中连续
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
// 计算size时直接对各个CounterCell的value进行累加
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
// 使用Contended注解自动进行填充避免伪共享
@sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
<font color="##FF0000">需要注意的是@sun.misc.Contended
注解在user classpath中是不起作用的,需要通过一个虚拟机参数来开启:-XX:-RestrictContended。</font>
@sun.misc.Contended
注解来自动填充。标签:word nis logical 处理器 nal time iss file 总线
原文地址:https://www.cnblogs.com/zdcsmart/p/12576464.html