介绍
在 TSO 这一篇中谈到过,给内存模型松绑,有助于硬件设计上实现更多的优化,TSO 对 SC 的一种顺序进行了松绑。如果我们进一步松绑,有机会带来更多的优化。比如,在分析 TSO 的优化的时候有一个场景,写指令发生在读指令之前,但生效却在之后,因为中间加了一层缓存。对于写指令来说,这个缓存是先进先出的(FIFO),因为对于 TSO 来说,写之间的顺序是强制的。如果我们松绑了写之间的顺序,那么这个缓存就没必要是先进先出,这样会带来更多的优化机会。本篇我们介绍宽松的内存模型,有不少处理器都采用了这种模型,为了叙述方便,我们就把它统称为 RMC(Relaxed Memory Model)。
场景分析
考虑以下代码:
1// Core C1
2S1: data1 = NEW;
3S2: data2 = NEW;
4S3: flag = SET;1// Core C2
2L1: r1 = flag
3B1: if (r1 != SET) goto L1;
4L2: r2 = data1;
5L3: r3 = data2;如果你熟悉操作系统,那么很可能已经看出,flag就是一个自旋锁(spinlock)。这个锁的作用,就是确保r2和r3都能得到NEW值。
要使r2得到NEW,那么以下执行序列必须得到保证:S1 -> S3 -> L1 -> L2。同理,要使r3得到NEW,则S2 -> S3 -> L1 -> L3。在 SC 和 TSO 两种内存模型下,这都可以得到保证。但是,SC 和 TSO 还额外保证了S1 -> S2和L2 -> L3,这种保证是没有必要的,所以我们有了第三种内存模型 RMC。
在 RMC 下,除了对同一地址的读写,其他的操作之间都是没有顺序保证的,所以上面的场景是无法产生预期效果的,我们还是需要借助 FENCE 来保证顺序:
1// Core C1
2S1: data1 = NEW;
3S2: data2 = NEW;
4F2: FENCE
5S3: flag = SET;1// Core C2
2L1: r1 = flag
3B1: if (r1 != SET) goto L1;
4F2: FENCE
5L2: r2 = data1;
6L3: r3 = data2;形式化定义
先来定义FENCE,和 TSO 是一样的:
- 如果
L(a) <p FENCE,则L(a) <m FENCE(Load -> FENCE) - 如果
S(a) <p FENCE,则S(a) <m FENCE(Store -> FENCE) - 如果
FENCE <p FENCE,则FENCE <m FENCE(FENCE -> FENCE) - 如果
FENCE <p L(a),则FENCE <m L(a)(FENCE -> Load) - 如果
FENCE <p S(a),则FENCE <m S(a)(FENCE -> Store)
RMC 要求保证对同一地址的操作顺序:
- 如果
L1(a) <p L2(a),则L1(a) <m L2(a)(同一地址Load -> Load) - 如果
L(a) <p S(a),则L(a) <m S(a)(同一地址Load -> Store) - 如果
S1(a) <p S2(a),则S1(a) <m S2(a)(同一地址Store->Store)
至于同一地址Store->Load的规则,和 TSO 是一样的:
针对同一内存地址,读(Load)会得到 <m 中最近一次写(Store)的值,或者同一处理器上一个尚位于 Write Buffer 中的值:
$$ {L(a) = MAX_{<m} { S(a)\ |\ S(a) \text{ <m } L(a)} \text{ or } S(a) \text{ <p } L(a)} $$
除此以外,其他的顺序没有保证。
| Load 2 | Store 2 | RMW 2 | FENCE 2 | |
|---|---|---|---|---|
| Load 1 | A | A | A | Yes |
| Store 1 | B | A | A | Yes |
| RMW 1 | A | A | A | Yes |
| FENCE 1 | Yes | Yes | Yes | Yes |
这里引入了一个新的符号 A,代表了仅保证对相同地址的操作的顺序。
总结
在这种内存模型下,编程变得更困难了。
程序员需要想清楚很多事情,人为地把一个个FENCE码放在代码的合适位置,确保实现出来的逻辑真正符合设计意图。同时,FENCE是有成本的,所以又要反过来思考哪些地方不需要FENCE,避免滥用。总结起来一句话——用最少的FENCE实现完全正确的代码逻辑。
在嵌入式领域中最流行的 ARM 处理器就属于这类模型。
