伪共享(false sharing)

并发编程无声的性能杀手

数据在内存中以 page 为单位存在;在cache 里以 cache line 为单位

伪共享:缓存系统以 cache line为单位存储,多线程修改互相独立的变量时,如果这些变量共享同一个cache line,就会无意中影响彼此的性能

CPU 取数逻辑

CPU 执行运算的时,经 L1 > L2 > L3 > 主存(物理内存)

缓存未命中的消耗

cpu到 周期约(cycle) 耗时约(ns)
主存 60-80
QPI 20
L3 40-45 15
L2 10 3
L1 3-4 1
寄存器 1

Cache Line

cacheLine 最常见 64 字节(2 的整数幂,32-256)

java long 8 字节,一个 cache line 可放 8 个 long 变量

访问 long[0] ,0-7 元素都被会加载到 cache 里,即便此后并不需要访问 1-7 的元素

好处:

恰好访问完 0 元素就像访问 1 的话(遍历数组),赚到

副作用:

整体加载,同一 cache line 上的数据是绑在一根绳上的蚂蚱

如果数据结构中的项在内存中不是彼此相邻的(如链表),将得不到免费缓存加载所带来的优势,并且在这些数据结构中的每一个项都可能会出现缓存未命中

False Sharing

多个线程操作不同的成员变量,但是相同的缓存行

1
2
volatile long head;
volatile long tail;

head 和 tail 会被加载到一个 cache line 中,但多线程的情况

head 被 Thread A 写入,tail 被 Thread B 读取。A 和 B 运行于不同 CPU 核心上

假设 head 被 A 更新了,cache 和主存中 head 的值都会被更改。导致所有 head 所在的 cache line 都失效,包括 B 使用的 cache line——即便它其实并不关心 head 的值

而 B 此时如果需要读取 tail,就必须重新从主存中加载 head 和 tail 到一个 cache line,即便 tail 没有被更改过

甚的情况是如果 B 也是写入操作,那么 A 和 B 任意一方的写操作都会让对方的 cache line 整体失效从而导致重新从主存加载

问题在于这一切都在幕后悄然发生,没有任何编译告警提示你说你这段代码并发性能会非常低下


core1 线程想要更新 X ,同时 core2 线程想要更新 Y 。但这两个频繁改动的变量都处于同一条缓存行

两个线程会轮番发送 RFO 消息,占得此缓存行的拥有权

core1 取得拥有权开始更新 X,则 core2 对应的缓存行要设为 I 状态

core2 取得拥有权开始更新 Y,则 core1 对应的缓存行要设为 I 状态 (失效态)

轮番夺取拥有权不但带来大量的 RFO 消息,而且如果某个线程需要读此行数据时,L1 和 L2 缓存上都是失效数据,只有 L3 缓存上是同步好的数据

更坏的情况是跨槽读取,L3 都要 miss,只能从内存上加载

表面上 X 和 Y 都是被独立线程操作的,而且两操作之间也没有任何关系。只不过它们共享了一个缓存行,但所有竞争冲突都是来源于共享


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class FalseShareTest implements Runnable {

	public static int NUM_THREADS = 4;

	public final static long ITERATIONS = 500L * 100L * 100L;

	private final int arrayIndex;

	private static VolatileLong[] longs;

	public static long SUM_TIME = 0L;

	public FalseShareTest(final int arrayIndex) {

		this.arrayIndex = arrayIndex;
	}

	public static void main(final String[] args) throws Exception {

		for (int j = 0; j < 10; j++) {
			System.out.println(j);
			if (args.length == 1) {
				NUM_THREADS = Integer.parseInt(args[0]);
			}
			longs = new VolatileLong[NUM_THREADS];
			for (int i = 0; i < longs.length; i++) {
				longs[i] = new VolatileLong();
			}
			final long start = System.nanoTime();
			runTest();
			final long end = System.nanoTime();
			SUM_TIME += end - start;
		}
		System.out.println("平均耗时:" + SUM_TIME / 10);
	}

	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 FalseShareTest(i));
		}
		for (Thread t : threads) {
			t.start();
		}
		for (Thread t : threads) {
			t.join();
		}
	}

	@Override
	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;     //屏蔽此行
	}
}

https://www.cnblogs.com/cyfonly/p/5800758.html