提升 SC性能的技术

文章目录

  • **逻辑架构**
  • **总结要点**
  • ✅ **修正版总结(精炼版)**:
  • L->L例子
    • ✅ **设定目标**:
    • **例子:L1 → L2 场景中,L2 读取值改变**
      • 系统设定:
      • 程序:
        • 在 P1:
        • 在 P2:
      • ⏱️ 执行时序:
      • 最终结果:
    • ✅ 结论:
  • S->L 例子

1. 加快内存访问执行速度
	1. 一旦 load/store 的目标地址已知,就发起预取数据(prefetch)请求;
	2. 一旦 load/store 的目标地址能预测地址,就提前预取数据;
	然后 load/store 就 来 操作cache(而不是内存) , 从而减少延迟
2. 允许多个访问指令(S->L L->L)重叠进行 , 即 允许此种情况下的 后面的 load 提前投机执行
	但这样的重叠可能破坏访问的原子性,因此需要安全机制来检测出原子性被破坏时,进行恢复/补救。
		1. 需要跟踪原子性
		2. 原子性被破坏的意思是:原子性被破坏 = 执行顺序造成了程序“看见了不应该看到的值”
		3. 如何恢复:
	执行顺序可以乱,但提交顺序不能乱,从而仍然保持 S->L L->L

逻辑架构

  1. 背景:基本 SC 性能差 → 需要优化
  2. 目标:在不破坏 SC 语义的前提下,
    方法A:加预取,将 latency 隐藏
    方法B:投机执行,允许 overlap,失败则 rollback
  3. 对比:prefetch 不 overlap 语义,仅提前缓存;speculation 真 overlap load 指令
  4. 实现细节:投机执行时, 用 LL/SC 机制跟踪原子性; store 投机困难,故通常不做
  5. 总结观点:这两者都能显著提升 SC 性能,MIPS R10000 和 Pentium 都用了;但最强优化仍需 compiler relax reorder

总结要点

  • prefetch 能提升 cache hit、减少 load/store latency
  • speculation 能 overlap load 操作,从而减小延迟
  • 两者都不违背 SC,但需要做 conflict detection + rollback
  • 实际硬件(如 Pentium、R10000)已有实现,性能提升明显
  • 虽然如此,为了更高性能,通常仍允许更松的一致性模型(如 RC)让 compiler reorder memory

L/S->L : 
	将 前面的指令(L/S) 称为 A , 后面的 L 为 B
	当 A 被执行时,此时 B 已经投机执行过. 
 	如果 在 A  执行 过程中,  投机执行过的B 所涉及的块 会被从高速窃取或替换 (不一定是因为A)
 	则 B 必须 重新 执行(即在A执行后重新执行,此时就不是投机执行了)

修正版总结(精炼版)

  • 情形:L/S → L
  • A:前面的指令,可以是 Load 或 Store
  • B:后面的 Load 指令,被投机提前执行,先访问了 cache

  • 执行过程:
  1. B 投机访问了它要读的 cache block,读取到了值。

  2. 后面,A 被执行。

  3. 执行 A 的过程中,发现:

    • B 访问的 cache block 已被 invalidated(抢、替换)
    • 因此,B 之前读取的值可能已失效
  4. 为了保持 SC 语义(必须让执行结果就像所有处理器在某个全局顺序下运行),必须:

    • 让 B 重执行(在 A 之后)
    • 此时 B 不再是投机执行,它的值是可信的

L->L例子

这是个非常好的问题。我们现在专门来分析:在 L→L(Load followed by Load) 的情形下,是否可能发生第一次投机执行的 B(Load)读取的值和重新执行 B 时的值不一样 —— 答案是:是的,完全可能发生。


设定目标

你关心的是下面两个条件能否同时满足:

  1. B(第二个 Load)在 A(第一个 Load)之前投机执行,并读取了一个值;
  2. 在 A 执行期间,B 所访问的 cache block 被 invalidated 或 evicted
  3. 重新执行 B 后,得到的值和第一次不同。

我们马上给出一个完整的例子来说明这种情况真实可发生:


例子:L1 → L2 场景中,L2 读取值改变

系统设定:

  • 两个处理器:P1 和 P2

  • 初始内存值:

    • A = 0
    • B = 0

程序:

在 P1:
r1 = *A;   // L1:先读 A
r2 = *B;   // L2:后读 B(被提前投机执行)
在 P2:
*B = 1;    // 写 B(可能引发 P1 的 B block invalidation)
*A = 1;    // 写 A(此写影响 P1 的 L1)

⏱️ 执行时序:

  1. P1 投机执行 L2(读 B),当前 B = 0,所以 r2 = 0
  2. 然后 P2 执行 *B = 1:这个写操作对 P1 的 cache 发送了 invalidate,B 的 cache block 被踢掉
  3. P2 执行 *A = 1
  4. 然后 P1 执行 L1(读 A),发现 A 的值是 1
  5. 此时检查 L2(读 B)是否仍然有效。发现它的 block 在 A 执行时已被 invalidated ⇒ 投机失效
  6. 于是:L2 需要重新执行,现在 B = 1 ⇒ r2 = 1

最终结果:

  • 第一次执行 L2(投机)读取的是 B = 0
  • 重新执行 L2(非投机)读取的是 B = 1
  • 所以:值确实改变了

✅ 结论:

在 L→L 的场景中,确实存在 第二个 Load(B)投机执行获得的值,后来在第一个 Load(A)执行后检测失效、重执行并获得不同值 的真实可能性。

这种现象解释了为什么必须在投机执行后做原子性校验和 rollback,否则就可能违反 顺序一致性(SC) 的语义。

需要我把这个例子画成时序图吗?可以让流程更清晰。

S->L 例子

TODO

你可能感兴趣的:(verilog,内存一致性)