文档章节

内存障碍: 软件黑客的硬件视图

Romane
 Romane
发布于 06/22 12:36
字数 19967
阅读 9
收藏 0

此文为笔者近日有幸看到的一则关于计算机底层内存障碍的学术论文,并翻译(机译)而来[自认为翻译的还行],若读者想要英文原版的论文话,给我留言,我发给你。

内存障碍: 软件黑客的硬件视图

保罗 E. 麦肯尼

2009年4月5日

 

那么, 是什么让 CPU 设计者使他们对穷人不知情的 SMP 软件设计者造成内存障碍呢?

简而言之, 因为重新排序内存引用允许更好的性能, 因此需要内存屏障来强制排序, 如同步基元, 其正确操作依赖于有序内存引用。

要获得这个问题的更详细的答案, 需要对 CPU 缓存的工作方式有一个很好的理解, 特别是使缓存真正工作的需要。以下各节:

  1. 呈现缓存的结构,
  2. 描述缓存一致性协议确保 cpu 如何同意内存中每个位置的值, 最后,
  3. 概述存储缓冲区和无效 queueshelp 缓存和缓存一致性协议如何实现高性能。

我们将看到, 内存屏障是一个必要的邪恶, 这是必须的, 以实现良好的性能和可伸缩性, 这一邪恶的根源在于, cpu 的数量级比两者之间的互联和他们试图的内存访问。

1          缓存结构

现代的 cpu 比现代的内存系统快得多。2006 CPU 可能能够执行每纳秒十指令, 但需要许多纳秒才能从主内存中提取数据项。这种速度的差异--超过两个数量级--导致了在现代 cpu 上发现的 multimegabyte 缓存。这些缓存与 cpu 相关联, 如图1所示, 通常可以在几个周期内进行访问。[1]

 

CPU 0

CPU 1

覆盖

覆盖

记忆

互 连

 

图 1: 现代计算机系统缓存结构

数据在固定长度块 (称为 "高速缓存线") 中的 cpu 缓存和内存之间流动, 通常是两个大小的幂, 范围从16到256字节不等。当给定的数据项首次被给定的 cpu 访问时, 它将从该 cpu 的缓存中消失, 这意味着出现 "缓存遗漏" (或者更确切地说是 "启动" 或 "预热" 缓存缺失)。缓存错过意味着 CPU 将不得不等待 (或被 "停滞") 数以百计的周期, 而该项目是从内存中提取。但是, 该项将加载到该 CPU 的缓存中, 以便随后的访问将在缓存中找到它, 因此以全速运行。

一段时间后, CPU 的缓存将会填满, 随后的遗漏可能需要从缓存中弹出一个项目, 以便为新获取的项目腾出空间。这样的缓存错过被称为 "容量错过", 因为它是由缓存的有限容量造成的。但是, 大多数缓存可以强制弹出旧项目, 以便为新项目腾出空间, 即使它们尚未满。这是因为, 大型缓存是作为硬件哈希表实现的, fixedsize 哈希桶 (或 "集合", CPU 设计者调用它们), 没有链接, 如图2所示。

此缓存有十六个 "集合" 和两个 "方法", 总共有32个 "行", 每个条目包含一个 256byte "缓存行", 它是一个256字节对齐的内存块。这种高速缓存线的大小在大尺寸上有点小, 但使十六进制算术变得简单得多。在硬件术语中, 这是一个双向 setassociative 缓存, 它类似于一个带有十六个桶的软件哈希表, 其中每个桶的哈希链最多只能有两个元素。大小 (在本例中为32个缓存行) 和相关性 (在本例中为两个) 统称为缓存的 "几何图形"。由于此缓存是在硬件中实现的, 因此哈希函数非常简单: 从内存地址提取四位。

在图2中, 每个框对应一个缓存项, 其中可以包含256字节的缓存行。但是, 缓存项可以为空, 如图中的空框所示。其余的框都用它们所包含的缓存线的内存地址进行标记。由于缓存行必须是256字节对齐的, 所以每个地址的低八位为零, 而硬件哈希函数的选择意味着下一个较高的四位与哈希行号匹配。

如果程序的代码位于地址上, 则可能会出现图中描述的情况。

                          方式0                  方式1

0x12345000

 

0x12345100

 

0x12345200

 

0x12345300

 

0x12345400

 

0x12345500

 

0x12345600

 

0x12345700

 

0x12345800

 

0x12345900

 

0x12345A00

 

0x12345B00

 

0x12345C00

 

0x12345D00

 

0x12345E00

0x43210E00

 

 

0x0

0x1

0x2

0x3

0x4

0x5

0x6

0x7

0x8

0x9

0xA

0xB

0xC

0xD

0cars

0xF

图 2: CPU 缓存结构

0x43210E00 通过 0x43210EFF, 该程序通过0x12345EFF 从0x12345000 依次访问数据。假设程序现在访问位置0x12345F00。此位置散列到0xF 行, 此行的两种方式都为空, 因此可以容纳相应的256字节行。如果程序要访问位置 0x1233000, 将其哈希为行 0x0, 则相应的256字节缓存行可以以1方式容纳。但是, 如果程序要访问位置 0x1233E00 (哈希为0xE 行), 则必须从缓存中弹出一个现有行, 以便为新的缓存行腾出空间。如果以后访问此弹出的行, 则会导致缓存错过。这样的缓存小姐被称为 "结合小姐"。

迄今为止, 我们只考虑 CPU 读取数据项的情况。当它写的时候会发生什么?因为重要的是所有 cpu 都同意给定数据项的值, 在给定的 CPU 写入该数据项之前, 它必须首先使其从其他 cpu 缓存中删除或 "失效"。一旦此失效完成, CPU 可以安全地修改数据项。如果该数据项存在于此 CPU 的缓存中, 但为只读, 则此过程称为 "写入遗漏"。一旦给定的 cpu 已完成将给定数据项从其他 cpu 缓存中作废, 该 cpu 可能会重复写入 (并读取) 该数据项。

以后, 如果另一个 cpu 尝试访问该数据项, 它将招致缓存错过, 这一次是因为第一个 cpu 使该项无效, 以便写入它。这种类型的缓存错过被称为 "通信小姐", 因为通常是由于几个 cpu 使用数据项进行通信 (例如, 锁是一个数据项, 用于在 cpu 之间使用互斥算法进行通信)。

显然, 必须非常小心, 以确保所有 cpu 保持一致的数据视图。由于所有这些获取、无效和写入, 很容易想象数据丢失或 (也许更糟) 不同的 cpu 在各自的缓存中具有与同一数据项有冲突的值。下一节中介绍的 "缓存一致性协议" 防止了这些问题。

2              缓存一致性协议

缓存一致性协议管理缓存行状态, 以防止数据不一致或丢失。这些协议可能相当复杂, 有许多国家,[2] 但为了我们的目的, 我们只需要关注四状态 MESI 缓存一致性协议。

2。1     月份状态

MESI 代表 "修改"、"独占"、"共享" 和 "无效", 给定缓存行可以使用此协议的四状态。因此, 使用此协议的缓存除了该行的物理地址和数据外, 还在每个缓存行上维护两位状态 "标记"。

"修改" 状态中的一行已受来自相应 CPU 的最近内存存储区的规限, 相应的内存保证不会出现在任何其他 cpu 的缓存中。因此, "修改" 状态中的缓存行可以说是由 CPU "拥有" 的。由于此缓存包含数据的唯一 todate 副本, 因此此缓存最终负责将其写入内存或将其分发到其他缓存中, 并且必须在重用此行以保存其他数据之前进行。

"独占" 状态与 "修改" 状态非常相似, 唯一的例外是缓存行尚未被相应的 CPU 修改, 这反过来意味着驻留在内存中的缓存行数据的副本是 todate 的。但是, 由于 cpu 可以随时存储到此行, 而无需咨询其他 cpu, "独占" 状态下的一行仍然可以说是由相应的 CPU 拥有的。也就是说, 由于内存中相应的值是最新的, 所以这个缓存可以丢弃这些数据, 而不会将其写回内存或将其交给其他 CPU。

"共享" 状态中的行可能至少在另一个 CPU 的缓存中复制, 因此不允许此 CPU 存储到该行, 而无需先与其他 cpu 进行协商。与 "独占" 状态一样, 因为内存中对应的值是最新的, 所以这个缓存可以丢弃这些数据, 而不必将其写回内存或将其交给其他 CPU。

"无效" 状态中的行为空, 换言之, 它不包含任何数据。当新数据进入缓存时, 如果可能, 它将被放置在 "无效" 状态的缓存行中。此方法是首选, 因为替换任何其他状态中的行可能会导致昂贵的缓存错过将来是否引用替换行。

由于所有 cpu 都必须保持对缓存行中数据的一致视图, cachecoherence 协议提供了通过系统协调缓存线移动的消息。

2。2          MESI 协议消息

上一节中描述的许多转换都要求 cpu 之间进行通信。如果 cpu 在单个共享总线上, 则以下消息足够:

读:"读取" 消息包含要读取的缓存行的物理地址。

读取响应:"读取响应" 消息包含较早的 "读取" 消息所请求的数据。此 "读取响应" 消息可能由内存或其他缓存提供。例如, 如果其中一个缓存在 "已修改" 状态下具有所需的数据, 则该缓存必须提供 "读取响应" 消息。

无效:"无效" 消息包含要失效的缓存行的物理地址。所有其他缓存都必须从缓存中删除相应的数据并作出响应。

无效确认:接收 "无效" 消息的 CPU 在从其缓存中删除指定数据后, 必须响应 "无效确认" 消息。

读取无效:"读取无效" 消息包含要读取的缓存行的物理地址, 同时指示其他缓存删除数据。因此, 它是一个 "读" 和 "无效" 的组合, 如其名称所示。"读取无效" 消息同时需要 "读取响应" 和一组 "无效确认" 消息。

"写回" 消息包含要写回内存的地址和数据 (可能会 "窥探" 到其他 cpu 的缓存中)。此消息允许缓存根据需要在 "已修改" 状态下弹出行, 以便为其他数据腾出空间。

有趣的是, 共享内存多处理器系统确实是在封面下传递消息的计算机。这意味着使用分布式共享内存的 SMP 计算机群集使用消息传递在系统体系结构的两个不同级别实现共享内存。

快速测验 1:如果两个 cpu 试图同时使同一高速缓存行无效, 会发生什么情况?

 

快速测验 2:当大型多处理器中出现 "无效" 消息时, 每个 CPU 都必须发出 "无效确认" 响应。导致 "无效确认" 响应的 "风暴" 不会完全饱和系统总线吗?

快速测验 3:如果 smp 计算机真的使用消息传递, 那么为什么还要用 smp 呢?

2。3          月份状态图

给定的缓存行状态随着协议消息的发送和接收而更改, 如图3所示。

 

M

E

S

I

a

c

e

d

f

g

h

j

k

l

b

i

 

图 3: MESI 缓存-相干态图

此图中的过渡弧如下所示:

过渡 (a):缓存行被写回内存, 但 CPU 将其保留在缓存中, 并进一步保留修改它的权限。此转换需要 "回写" 消息。

过渡 (b):CPU 写入它已经独占访问的缓存行。此转换不需要发送或接收任何消息。

过渡 (c):CPU 收到已修改的缓存行的 "读取无效" 消息。CPU 必须使其本地副本无效, 然后响应 "读取响应" 和 "无效确认" 消息, 既将数据发送到请求的 CPU, 又表示它不再具有本地副本。

过渡 (d):CPU 对其缓存中未存在的数据项执行原子 readmodify 写入操作。它传输一个 "读取无效", 通过 "读取响应" 接收数据。当 CPU 还收到一整套 "无效确认" 响应时, 它就可以完成转换。

过渡 (e):CPU 对以前在其缓存中只读的数据项执行原子 readmodify 写入操作。它必须传输 "无效" 消息, 并且在完成转换之前必须等待一整套 "无效确认" 响应。

过渡 (f):另一些 cpu 读取缓存行, 它是从该 CPU 的缓存中提供的, 它保留一个只读副本。此转换是通过接收 "读取" 消息启动的, 而此 CPU 用包含所请求数据的 "读取响应" 消息进行响应。

过渡 (g):另一些 cpu 读取此缓存行中的数据项, 并从该 cpu 的缓存或内存中提供。无论哪种情况, 此 CPU 都保留一个只读副本。此转换是通过接收 "读取" 消息启动的, 而此 CPU 用包含所请求数据的 "读取响应" 消息进行响应。

过渡 (h):这个 CPU 意识到它很快就需要写入这个缓存行中的一些数据项, 从而传输一个 "无效" 消息。CPU 无法完成转换, 直到收到一整套 "无效确认" 响应。或者, 所有其他 cpu 都通过 "回写" 消息 (想必是为其他缓存行腾出空间) 从缓存中弹出此缓存行, 这样 cpu 就是最后一个缓存它的 cpu。

过渡 ():另一些 CPU 对缓存行中的数据项执行原子读-修改-写入操作, 该运算仅在该 cpu 的缓存中保存, 因此此 cpu 将其从缓存中无效。此转换是通过接收 "读取无效" 消息启动的, 而此 CPU 同时响应 "读取响应" 和 "无效确认" 消息。

过渡 (j):此 CPU 对缓存行中不在其缓存中的数据项进行存储, 从而传输 "读取无效" 消息。CPU 无法完成转换, 直到接收到 "读取响应" 和一整套 "无效确认" 消息。当实际存储完成后, 缓存行大概会通过过渡 (b) 转换为 "修改" 状态。

转换 (k):此 CPU 在缓存行中加载不在其缓存中的数据项。CPU 传输 "读取" 消息, 并在接收相应的 "读取响应" 时完成转换。

过渡 (l):另一些 CPU 将存储区放到此缓存行中的数据项, 但由于该缓存行保存在其他 cpu 缓存中 (如当前 cpu 的缓存), 因此在只读状态下将其保留。此转换是通过接收 "无效" 消息启动的, 并且此 CPU 响应 "无效确认" 消息。

快速测验 4:硬件如何处理上面描述的延迟过渡?

2。4         MESI 协议示例

现在让我们从缓存行的数据的角度来看这一点, 最初驻留在地址0的内存中, 因为它通过四 CPU 系统中的各种单行直接映射缓存进行传输。表1显示了此数据流, 其中第一列显示了操作顺序、第二个执行操作的 CPU、第三个执行的操作、下一个四个 cpu 缓存行的状态 (后跟 MESI 状态的内存地址) 和最后两列是否对应的内存内容是最新的 ("V") 还是不 ("I")。

最初, 数据驻留的 CPU 缓存行处于 "无效" 状态, 并且数据在内存中有效。当 cpu 0 加载地址0上的数据时, 它进入 cpu 0 的缓存中的 "共享" 状态, 并且在内存中仍然有效。CPU 3 还加载地址0中的数据, 以便它处于两个 cpu 缓存中的 "共享" 状态, 并且在内存中仍然有效。下一个 CPU 3 加载一些其他缓存行 (地址 8), 它通过回写将地址0中的数据强制从缓存中取出, 并用地址8中的数据替换它。现在 CPU 2 从地址0进行负载, 但是这个 CPU 意识到它很快就需要存储到它, 所以它使用一个 "读取无效" 的消息, 以获得一个独占拷贝, 使它从 CPU 3 的缓存无效 (虽然在内存中的副本仍然是最新的)。下一个 CPU 2 会进行预期的存储, 将状态更改为 "已修改"。内存中的数据副本现在已过期。cpu 1 使用 "读取无效" 来窥探 cpu 2 的缓存中的数据并使其失效, 因此 cpu 1 的缓存中的副本处于 "已修改" 状态 (并且内存中的副本仍然过期)。最后, CPU 1 读取地址8上的高速缓存行, 它使用 "回写" 消息将地址0的数据推送回内存。

请注意, 我们以某些 CPU 缓存中的数据结尾。

快速测验 5:什么操作顺序会将 cpu 的缓存全部恢复到 "无效" 状态?

3   商店导致不必要的摊位

尽管图1所示的缓存结构为从给定的 CPU 重复读取和写入给定的数据项提供了良好的性能, 但它对给定缓存行的第一次写入的性能相当差。要看到这一点, 请考虑图 4, 它显示了一个由 cpu 0 写入到 cpu 1 缓存中的 cacheline 的时间线。由于 cpu 0 必须等待缓存行到达它才能写入它, 所以 cpu 0 必须在一段较长的时间内失速。[3]

 

CPU 0

CPU 1

确认

无效

失速

 

图 4: 写查看不必要的摊位

但是, 没有真正的理由迫使 cpu 0 拖延这么长的时间-毕竟, 不管是什么数据发生在缓存线, cpu 1 发送它, cpu 0 将无条件地覆盖它。

3。1   存储缓冲区

防止这种不必要的写入延迟的一种方法是在每个 CPU 和它的缓存之间添加 "存储缓冲区", 如图5所示。随着这些存储缓冲区的增加, CPU 0 可以简单地在其存储缓冲区中记录其写入并继续执行。当高速缓存线最终从 cpu 1 转到 cpu 0 时, 数据将从存储缓冲区移动到缓存行。

然而, 有些并发症必须解决, 这在接下来的两个部分中得到了讨论。

3。2        商店转发

要查看第一个复杂度 (违反 selfconsistency), 请考虑以下代码, 其中包含变量 "a" 和 "b" (最初为零), 以及具有最初由 cpu 1 所拥有的变量 "a" 的缓存行, 其中包含 "b" (最初由 cpu 0 拥有):

 

CPU 0

CPU 1

缓冲区

商店

缓冲区

商店

缓存

缓存

记忆

互 连

 

序列

CPU

操作

 

CPU 缓存

 

0

内存8

0

1

2

3

0

 

初始状态

-/我

-/我

-/我

-/我

V

V

1

0

负荷

0/秒

-/我

-/我

-/我

V

V

2

3

负荷

0/秒

-/我

-/我

0/秒

V

V

3

0

8/秒

-/我

-/我

0/秒

V

V

4

2

RMW

8/秒

-/我

0/E

-/我

V

V

5

2

商店

8/秒

-/我

0/米

-/我

I

V

6

1

原子公司

8/秒

0/米

-/我

-/我

I

V

7

1

8/秒

8/秒

-/我

-/我

V

V

表 1: 缓存一致性示例

图 5: 带有存储缓冲区的缓存

  1. a = 1;
  2. b = 1;
  3. 断言 (b = = 2);

人们不会期望断言失败。然而, 如果一个人愚蠢到使用图5所示的非常简单的体系结构, 你会感到惊讶。这样的系统可能会看到以下一系列事件:

  1. CPU 0 开始执行a=1.
  2. CPU 0 在缓存中看起来是 "a", 并发现

它不见了。

  1. 因此, CPU 0 发送 "读取无效" 消息, 以便获得包含 "a" 的缓存行的独占所有权。
  2. CPU 0 将存储区记录到其存储缓冲区中的 "a"。
  3. CPU 1 接收到 "读取无效" 消息, 并通过传输缓存行并从其缓存中删除该 cacheline 来响应。
  4. CPU 0 开始执行b=a+1.
  5. cpu 0 从 cpu 1 接收缓存行, whichstill 的值为 "a" 的零。
  6. CPU 0 从它的缓存中加载 "a", 找到 valuezero。
  7. CPU 0 将该项从其存储队列应用到新到达的缓存行, 将其缓存中的 "a" 值设置为一个。
  8. cpu 0 为上面的 "a" 加载了一个零值, 并将其存储到包含 "b" 的缓存行中 (我们假定它已经由 cpu 0 拥有)。
  9. CPU 0 执行断言 (b = 2), 但失败。

问题是, 我们有两个 "a" 副本, 一个在缓存中, 另一个在存储缓冲区中。

本示例打破了一个非常重要的保证, 即每个 CPU 将始终看到其自己的操作, 就好像它们是在程序顺序中发生的一样。

这种保证对软件类型是猛烈的反直觉的, 因此, 硬件的人很同情并实现了 "存储转发", 在那里每个 CPU 都指 (或 "窥探") 它的存储缓冲区以及在执行负载时的缓存, 如图6所示。换言之, 给定 CPU 的存储直接转发到其后续负载, 而无需通过缓存。

 

CPU 0

CPU 1

缓冲区

商店

缓冲区

商店

缓存

缓存

记忆

互 连

 

图 6: 带有存储转发的缓存

随着存储转发到位, 上述序列中的项目8将在存储缓冲区中找到 "a" 的正确值 1, 以便 "b" 的最终值将是 2, 正如人们所希望的那样。

3。3 存储缓冲区和内存障碍

要查看第二个并发症, 即违反全局内存排序, 请考虑以下代码序列, 变量 "a" 和 "b" 最初为零:

  1. 无效 foo (空)
  2. {
  3. a = 1;
  4. b = 1;
  5. }

6

  1. 空隙杆 (空)
  2. {
  3. 当 (b = = 0) 继续;
  4. 断言 (a = = 1);
  5. }

假设 cpu 0 执行 foo () 和 cpu 1 执行条 ()。进一步假设包含 "a" 的缓存行仅驻留在 cpu 1 的缓存中, 并且包含 "b" 的缓存行由 cpu 0 拥有。然后, 操作序列可能如下所示:

  1. CPU 0 执行a=1.缓存行不在 cpu 0 的缓存中, 因此 cpu 0 将 "a" 的新值放在其存储缓冲区中, 并传输 "读取无效" 消息。
  2. CPU 1 执行而 (b = = 0) 继续, 但包含 "b" 的缓存行不在其缓存中。因此, 它传送一个 "读取" 信息。
  3. CPU 0 执行b=1.它已经拥有了此缓存行 (换言之, 缓存行已经处于 "已修改" 或 "独占" 状态), 因此它将 "b" 的新值存储在其缓存行中。
  4. cpu 0 接收到 "读取" 消息, 并将包含 "b" 的当前更新值的缓存行传输到 CPU 1, 也将该行标记为 "共享" 在其自己的缓存中。
  5. CPU 1 接收到包含 "b" 的缓存行, andinstalls 它在其缓存中。
  6. CPU 1 现在可以完成执行而 (b = = 0) 继续, 并且因为它发现 "b" 的值是 1, 它继续到下一条语句。
  7. CPU 1 执行断言 (a = = 1), 而且, 由于 CPU 1 使用的是 "a" 的旧值, 因此此断言失败。
  8. cpu 1 接收到 "读取无效" 消息, 并将包含 "a" 的缓存行传输到 CPU 0, 并使此缓存行从其自己的缓存中无效。但为时已晚。
  9. cpu 0 接收到缓存线, 其中包含 "a" andapplies 缓冲的存储, 正好是 CPU 1 的失败断言的牺牲品。

硬件设计者在这里不能直接帮助, 因为 cpu 不知道哪些变量是相关的, 更不用说它们是如何相关的了。因此, 硬件设计者提供内存屏障指令, 允许软件告诉 CPU 有关这种关系。必须更新程序片段以包含内存屏障:

    1. 无效 foo (空)
    2. {
    3. a = 1;
    4. smp_mb ();
    5. b = 1;
    6. }

7

    1. 空隙杆 (空)
    2. {
    3. 当 (b = = 0) 继续;
    4. 断言 (a = = 1);
    5. }

记忆障碍smp_mb ()将导致 CPU 在将后续存储应用到其缓存行之前刷新其存储缓冲区。在继续之前, CPU 可以简单地失速直到存储缓冲区为空, 或者它可以使用存储缓冲区来保存后续的存储, 直到应用了存储缓冲区中的所有前一项。

使用后一种方法, 操作序列可能如下所示:

  1. CPU 0 执行a=1.缓存行不在 cpu 0 的缓存中, 因此 cpu 0 将 "a" 的新值放在其存储缓冲区中, 并传输 "读取无效" 消息。
  2. CPU 1 执行而 (b = = 0) 继续, 但包含 "b" 的缓存行不在其缓存中。因此, 它传送一个 "读取" 信息。
  3. CPU 0 执行smp_mp (), 并标记所有当前存储缓冲区项 (即,a=1).
  4. CPU 0 执行b=1.它已经拥有此缓存行 (换言之, 缓存行已经处于 "已修改" 或 "独占" 状态), 但存储缓冲区中有一个标记的条目。因此, 与其将 "b" 的新值存储在缓存行中, 不如将其放在存储缓冲区中 (但在无名项)。
  5. cpu 0 接收到 "读取" 消息, 并将包含 "b" 的原始值的缓存行传输到 CPU 1。它还将其自己的此缓存行的副本标记为 "共享"。
  6. CPU 1 接收到包含 "b" 的缓存行, andinstalls 它在其缓存中。
  7. CPU 1 现在可以完成执行而 (b = = 0) 继续, 但由于它发现 "b" 的值仍然是 0, 它重复了而声明。"b" 的新值安全地隐藏在 CPU 0 的存储缓冲区中。
  8. cpu 1 接收到 "读取无效" 消息, 并将包含 "a" 的缓存行传输到 CPU 0, 并使此缓存行从其自己的缓存中无效。
  9. CPU 0 接收到缓冲存储区中包含 "a" andapplies 的缓存行。
  10. 因为商店到 "a" 是唯一的条目在 thestore 缓冲区, 是标记的smp_mb (), CPU 0 还可以存储 "b" 的新值--除了包含 "b" 的高速缓存行现在处于 "共享" 状态的情况之外。
  11. 因此, cpu 0 发送一个 "无效" messageto CPU 1。
  12. cpu 1 接收到 "无效" 消息, 使包含 "b" 的缓存行从其缓存中无效, 并向 CPU 0 发送 "确认" 消息。
  13. CPU 1 执行而 (b = = 0) 继续, 但包含 "b" 的缓存行不在其缓存中。因此, 它将 "读取" 消息传送到 CPU 0。
  14. CPU 0 接收到 "确认" 消息, 并将包含 "b" 的缓存行放入 "独占" 状态。CPU 0 现在将 "b" 的新值存储到缓存行中。
  15. cpu 0 接收到 "读取" 消息, 并将包含 "b" 的原始值的缓存行传输到 CPU 1。它还将其自己的此缓存行的副本标记为 "共享"。
  16. CPU 1 接收到包含 "b" 的缓存行, andinstalls 它在其缓存中。
  17. CPU 1 现在可以完成执行而 (b = = 0) 继续, 并且因为它发现 "b" 的值是 1, 它继续到下一条语句。
  18. CPU 1 执行断言 (a = = 1), 但包含 "a" 的缓存行不再位于其缓存中。当它从 CPU 0 获取此缓存时, 它将使用 "a" 的最新值, 因此断言通过。

如您所见, 此过程不涉及少量簿记。即使是直觉简单的东西, 比如 "负载一个值", 也可能涉及到许多复杂的硅步骤。

4   商店序列导致不必要的摊位

不幸的是, 每个存储缓冲区都必须相对较小, 这意味着执行适度的存储序列的 CPU 可以填充其存储缓冲区 (例如, 如果所有这些缓存都导致内存丢失)。此时, CPU 必须再次等待失效完成以耗尽其存储缓冲区, 然后才能继续执行。这种情况可能会立即出现在内存障碍后, 当所有后续存储指令必须等待失效完成, 而不管这些存储是否导致缓存丢失。

这种情况可以通过使无效确认消息更快到达而得到改进。实现此目的的一种方法是使用每 CPU 队列中的无效消息, 或者 "使队列无效"。

4。1       使队列无效

使确认消息无效的原因之一是, 它们必须确保相应的缓存行实际上无效, 如果缓存忙, 则可能会延迟此无效, 例如, 如果 CPU 正在密集地加载和存储数据都驻留在缓存中。此外, 如果大量无效消息在短时间内到达, 则给定的 CPU 在处理它们时可能落后, 因此可能会阻塞所有其他 cpu。

但是, 在发送确认之前, CPU 不需要实际上使缓存行无效。它可以对无效消息进行排队, 并了解在 CPU 发送有关该缓存行的任何进一步消息之前将处理该消息。

4。2 使队列无效并使确认无效

图7显示了具有无效队列的系统。具有无效队列的 CPU 可能会在队列中立即确认无效消息, 而不必等到相应的行实际失效。当然, 在准备传输无效消息时, cpu 必须引用其无效队列-如果对应缓存行的项在无效队列中, 则 cpu 无法立即传输无效消息;它必须等待, 直到已处理无效队列项。

将条目放入无效队列实质上是 CPU 在传输有关该缓存行的任何 MESI 协议消息之前处理该项的承诺。只要对应的数据结构没有受到高度的争辩, CPU 就很少会受到这样的承诺的不便。

但是, 使消息失效的事实可以在无效队列中缓冲, 这为内存 misordering 提供了额外的机会, 如下一节所述。

 

CPU 0

CPU 1

缓冲区

商店

缓冲区

商店

缓存

缓存

无效

队列

记忆

互 连

无效

队列

 

图 7: 带有无效队列的缓存

4。3    使队列和内存障碍无效

假设 "a" 和 "b" 的值最初为零, 则 "a" 被复制为只读 (MESI "共享" 状态), 并且 "b" 由 CPU 0 (MESI "独占" 或 "修改" 状态) 拥有。然后假设 CPU 0 执行foo ()CPU 1 执行函数时栏 ()在下面的代码片断中:

  1. 无效 foo (空)
  2. {
  3. a = 1;
  4. smp_mb ();
  5. b = 1;
  6. }

7

  1. 空隙杆 (空)
  2. {
  3. 当 (b = = 0) 继续;
  4. 断言 (a = = 1);
  5. }

然后, 操作序列可能如下所示:

  1. CPU 0 执行a=1.对应的缓存行在 cpu 0 的缓存中是只读的, 因此 cpu 0 将 "a" 的新值放在其存储缓冲区中, 并传输 "无效" 消息, 以便从 CPU 1 的缓存中刷新相应的缓存行。
  2. CPU 1 执行而 (b = = 0) 继续, 但包含 "b" 的缓存行不在其缓存中。因此, 它传送一个 "读取" 信息。
  3. CPU 0 执行b=1.它已经拥有了此缓存行 (换言之, 缓存行已经处于 "已修改" 或 "独占" 状态), 因此它将 "b" 的新值存储在其缓存行中。
  4. cpu 0 接收到 "读取" 消息, 并将包含 "b" 的当前更新值的缓存行传输到 CPU 1, 也将该行标记为 "共享" 在其自己的缓存中。
  5. cpu 1 接收到 "a" 的 "无效" 消息, 将其置于无效队列中, 并将 "无效确认" 消息传递给 CPU 0。请注意, "a" 的旧值仍然保留在 CPU 1 的缓存中。
  6. CPU 1 接收到包含 "b" 的缓存行, andinstalls 它在其缓存中。
  7. CPU 1 现在可以完成执行而 (b = = 0) 继续, 并且因为它发现 "b" 的值是 1, 它继续到下一条语句。
  8. CPU 1 执行断言 (a = = 1), 而且, 由于 "a" 的旧值仍然位于 CPU 1 的缓存中, 因此此断言失败。
  9. CPU 1 处理排队的 "无效" 消息, 并使包含 "a" 的缓存行从其自己的缓存中无效。但为时已晚。
  10. cpu 0 从 cpu 0 接收到 "a" 的 "无效确认" 消息, 因此, 及时将缓冲存储应用于 cpu 1 的失败断言。

再一次, cpu 设计者对这种情况不能做太多的事情, 因为硬件不知道在 cpu 中有什么关系只是不同的比特。但是, 内存屏障指令可以与无效队列交互, 因此, 当给定的 CPU 执行内存屏障时, 它将标记当前处于无效队列中的所有项, 并强制任何后续负载等待, 直到所有标记的项都已应用于 CPU 的缓存。因此, 我们可以添加内存屏障, 如下所示:

    1. 无效 foo (空)
    2. {
    3. a = 1;
    4. smp_mb ();
    5. b = 1;
    6. }

7

    1. 空隙杆 (空)
    2. {
    3. 当 (b = = 0) 继续;
    4. smp_mb ();
    5. 断言 (a = = 1);
    6. }

通过此更改, 操作序列可能如下所示:

  1. CPU 0 执行a=1.对应的缓存行在 cpu 0 的缓存中是只读的, 因此 cpu 0 将 "a" 的新值放在其存储缓冲区中, 并传输 "无效" 消息, 以便从 CPU 1 的缓存中刷新相应的缓存行。
  2. CPU 1 执行而 (b = = 0) 继续, 但包含 "b" 的缓存行不在其缓存中。因此, 它传送一个 "读取" 信息。
  3. CPU 0 执行b=1.它已经拥有了此缓存行 (换言之, 缓存行已经处于 "已修改" 或 "独占" 状态), 因此它将 "b" 的新值存储在其缓存行中。
  4. cpu 0 接收到 "读取" 消息, 并将包含 "b" 的当前更新值的缓存行传输到 CPU 1, 也将该行标记为 "共享" 在其自己的缓存中。
  5. cpu 1 接收到 "a" 的 "无效" 消息, 将其置于无效队列中, 并将 "无效确认" 消息传递给 CPU 0。请注意, "a" 的旧值仍然保留在 CPU 1 的缓存中。
  6. CPU 1 接收到包含 "b" 的缓存行, andinstalls 它在其缓存中。
  7. CPU 1 现在可以完成执行而 (b = = 0) 继续, 并且因为它发现 "b" 的值是 1, 它继续到下一条语句。
  8. CPU 1 执行smp_mb (), 标记其无效队列中的项。
  9. CPU 1 执行断言 (a = = 1), 但由于在无效队列中包含 "a" 的缓存行有标记的条目, 因此 CPU 1 必须停止此负载, 直到已应用无效队列中的项为止。
  10. CPU 1 处理 "无效" 消息, 从其缓存中删除包含 "a" 的 cacheline。
  11. 现在 CPU 1 可以自由加载 "a" 的值, butsince 这会导致缓存错过, 它必须发送一个 "读取" 消息来获取相应的缓存行。
  12. cpu 0 从 cpu 0 接收到 "a" 的 "无效确认" 消息, 因此应用缓冲存储, 将相应缓存行的 MESI 状态更改为 "已修改"。
  13. cpu 0 接收到 "a" fromCPU 1 的 "读取" 消息, 因此将相应缓存行的状态更改为 "共享", 并将缓存线传输到 CPU 1。
  14. CPU 1 接收到包含 "a" 的缓存行, 因此可以执行负载。由于此负载返回 "a" 的更新值, 断言通过。

随着 MESI 消息的大量传递, cpu 到达了正确的答案。

5   读  和  写  记忆障碍

在上一节中, 内存屏障用于标记存储缓冲区和无效队列中的条目。但在我们的代码片段中,foo ()没有理由做任何与无效队列, 并栏 ()simlarly 没有理由对商店排队做任何事。

因此, 许多 CPU 体系结构提供了较弱的内存屏障指令, 仅执行这两个操作中的一个或另一个。粗略地说, "读取内存屏障" 仅标记无效队列, 而 "写入内存屏障" 仅标记存储缓冲区。而一个完全成熟的记忆屏障也能做到这一点。

这样做的效果是, 读取内存屏障只在执行它的 CPU 上加载, 因此读取内存屏障之前的所有负载在读取内存屏障之后的任何负载之前都将显示完成。同样, 写内存屏障命令只存储, 再次在执行它的 CPU 上, 再次使写内存屏障之前的所有存储在写入内存屏障之后的任何存储区中都已完成。一个完整的内存屏障命令加载和存储, 但再次只在 CPU 执行内存屏障。

如果我们更新Foo和酒吧要使用读写内存屏障, 它们的显示方式如下:

  1. 无效 foo (空)
  2. {
  3. a = 1;
  4. smp_wmb ();
  5. b = 1;
  6. }

7

  1. 空隙杆 (空)
  2. {
  3. 当 (b = = 0) 继续;
  4. smp_rmb ();
  5. 断言 (a = = 1);
  6. }

有些电脑有更多的记忆障碍, 但了解这三种变体将为一般的记忆障碍提供一个很好的介绍。

6   例子   记忆屏障序列

本节介绍一些诱人但巧妙地打破记忆障碍的使用。虽然其中许多将在大多数情况下工作, 有些将一直工作在某些特定的 cpu 上, 但如果目标是生成在所有 cpu 上可靠运行的代码, 则必须避免使用这些用途。为了帮助我们更好地看到微妙的破损, 我们首先需要关注的是一个有序敌对的体系结构。

6。1            排序敌意体系结构

保罗已经遇到了一些命令敌意的计算机系统, 但敌意的性质一直非常微妙, 并理解它需要详细的知识, 具体的硬件。与其挑选一个特定的硬件供应商, 并作为一个大概有吸引力的替代, 以拖动读者通过详细的技术规格, 让我们取而代之的设计一个神话般的, 但最大的内存排序恶意的计算机体系结构。[4]

此硬件必须服从以下排序约束 [16、17]:

  1. 每个 CPU 都会在程序顺序中感知到自己的 memoryaccesses。
  2. 如果两个操作引用不同的位置, cpu 将用 storeonly 重新排序给定的操作。
  3. 在读取内存屏障之前给定 CPU 的所有负载 (smp_rmb ()) 将被所有 cpu 视为在读取内存屏障之后的任何负载之前。

 

CPU 0

CPU 1

CPU 2

 

a=1;smp_wmb ();

而 (b = = 0);

 

b=1;

c=1;

z = c;smp_rmb ();x = a;断言 (z = = 0 | |x==1);

表 2: 内存屏障示例1

  1. 所有给定 CPU 的存储在 writememory 屏障之前 (smp_wmb ()) 将被所有的 cpu 认为在任何存储之后, 写内存屏障。
  2. 所有给定 CPU 的访问 (负载和存储) 之前的完整内存屏障 (smp_mb ()) 将被所有 cpu 视为在内存屏障之后的任何访问之前。

快速测验 6:是否保证每个 CPU 都能看到自己的内存访问, 以保证每个用户级线程都能按顺序看到自己的内存访问?为什么?

设想一个大型的非统一缓存体系结构 (NUCA) 系统, 以便为给定节点中的 cpu 提供公平的互连带宽分配, 并在每个节点的互连接口中提供每个 cpu 的队列, 如图8所示。虽然给定 cpu 的访问是按该 cpu 执行的内存障碍指定的, 但是, 给定的一对 cpu 访问的相对顺序可能会被严重重新排序, 正如我们将看到的那样。[5]

6。2     示例1

Figure2 显示三个代码片段, 由 cpu 1、2和3并发执行。每个 "a"、"b" 和 "c" 都是初始为零。

 

U 0

Cp

明智

U 1

Cp

e

凹陷

CPU 0

C

交流

CPU 1

节点0

Cp

U 2

明智

Cp

U 3

Mes

圣人

CPU 3

CPU 2

C

交流

节点1

互 连

记忆

 

图 8: 示例排序-恶意体系结构

假设 cpu 0 最近经历了许多缓存丢失, 因此其消息队列已满, 但 cpu 1 已在缓存中独占运行, 因此其消息队列为空。然后, cpu 0 对 "a" 和 "b" 的赋值将立即出现在节点0的缓存中 (因此对 cpu 1 可见), 但将被阻塞在 cpu 0 的先前通信量之后。相比之下, cpu 1 对 "c" 的赋值将通过 cpu 1 以前的空队列进行航行。因此, cpu 2 在将 cpu 0 的任务分配到 "a" 之前, 很可能会将 cpu 1 的任务分配到 "c", 从而导致断言发生火灾, 尽管存在内存障碍。

理论上, 便携式代码不能在这个示例编码技术上, 然而, 实际上它确实在所有主流计算机系统上工作。

快速测验 7:是否可以通过在 CPU 1 的 "同时" 和 "c" 任务之间插入内存屏障来修复此代码?为什么?

6。3     示例2

Figure3 显示三个代码片段, 由 cpu 1、2和3并发执行。"a" 和 "b" 最初都是零。

同样, 假设 cpu 0 最近经历了许多缓存缺失, 因此它的消息队列已满, 但 cpu 1 已完全运行在

      CPU 0         CPU 1                   CPU 2

a=1;

而 (a = = 0);smp_mb ();

y = b;

 

b=1;

smp_rmb ();x = a;断言 (y = = 0 | |x==1);

表 3: 内存屏障示例2

 

CPU0

CPU1

CPU2

 

1

a=1;

 

 

2

smb_wmb ();

 

 

3

b=1;

而 (b = = 0);

而 (b = = 0);

4

 

smp_mb ();

smb_mb ();

5

 

c=1;

D = 1;

6

而 (c = = 0);

 

 

7

而 (d = = 0);

 

 

8

smp_mb ();

 

 

9

e = 1;

 

断言 (e = = 0 | |a==1);

表 4: 内存屏障示例3

缓存, 使其消息队列为空。然后, cpu 0 对 "a" 和 "b" 的赋值将立即出现在节点0的缓存中 (因此对 cpu 1 可见), 但将被阻塞在 cpu 0 的先前通信量之后。相比之下, cpu 1 对 "b" 的分配将通过 cpu 1 以前的空队列进行航行。因此, cpu 2 在将 cpu 0 的任务分配到 "a" 之前, 很可能会将 cpu 1 的任务分配到 "b", 从而导致断言发生火灾, 尽管存在内存障碍。

理论上, 便携式代码不能在这个例子编码技术, 但像以前一样, 实际上它确实在所有主流计算机系统上工作。

6。4     示例3

Figure4 显示三个代码片段, 由 cpu 1、2和3并发执行。所有变量初始为零。

请注意, 无论是 cpu 1 还是 cpu 2 都不能继续到4行, 直到他们看到 cpu 0 的工作分配到了3行的 "b"。一旦 cpu 1 和2在3行上执行了它们的内存屏障, 它们都可以保证在2行的内存屏障之前, 通过 cpu 0 查看所有分配。同样, cpu 0 的内存屏障在8号线与 cpu 1 和2行 4, 使 cpu 0 不会执行分配到 "e" 在行 9, 直到它的分配到 "a" 是可见的, 对两个其他 cpu。因此, CPU 2 在9行上的断言是有保证的开火。

快速测验 8:假设 cpu 1 和2的行3-5 在中断处理程序中, cpu 2 的行9在进程级别运行。为了使代码能够正确工作, 是否需要进行哪些更改, 换言之, 以防止断言被触发?

Linux 内核的synchronize_rcu ()基元使用类似于本示例中所示的算法。

7内存屏障指令为特定的 cpu

每个 CPU 都有它自己特有的内存屏障指令, 这可以使可移植性成为一个挑战, 如表5所示。事实上, 许多软件环境, 包括 pthreads 和 Java, 都只是禁止直接使用内存屏障, 将程序员限制在相互排斥的原语中, 将它们合并到需要的范围内。在表中, 前四列指示给定的 CPU 是否允许重新排序负载和存储的四可能组合。接下来的两列指示给定的 CPU 是否允许使用原子指令重新排序负载和存储。只有六个 cpu, 我们有五不同的负载存储 reorderings 组合, 三的四可能的原子指令 reorderings。

第七栏, 依赖读取重新排序, 需要一些解释, 这是在下一节涉及 Alpha cpu。简短的版本是阿尔法要求读者记忆障碍以及链接数据结构的 updaters。是的, 这意味着 Alpha 可以实际上获取数据指向之前它提取指针本身, 奇怪但真实。请参阅:http://www.openvms。compaq.com/wizard/wiz_2637.html如果你认为我是在做这个。这种极弱的内存模型的好处是 Alpha 可以

 

LoadsReorderedAfterLoads?

LoadsReorderedAfterStores?

StoresReorderedAfterStores?

StoresReorderedAfterLoads?

AtomicInstructionsReorderedWithLoads?

AtomicInstructionsReorderedWithStores?

DependentLoadsReordered?

IncoherentInstructionCache/管道?

 

阿 尔 法

Y

Y

Y

Y

Y

Y

Y

Y

AMD64

 

 

 

Y

 

 

 

 

IA64

Y

Y

Y

Y

Y

Y

 

Y

(PA-RISC)

Y

Y

Y

Y

 

 

 

 

cpu

 

 

 

 

 

 

 

 

权力Tm

Y

Y

Y

Y

Y

Y

 

Y

(SPARC RMO)

Y

Y

Y

Y

Y

Y

 

Y

(SPARC PSO)

 

 

Y

Y

 

Y

 

Y

SPARC

 

 

 

Y

 

 

 

Y

x86

 

 

 

Y

 

 

 

Y

(x86 OOStore)

Y

Y

Y

Y

 

 

 

Y

zSeries°R

 

 

 

Y

 

 

 

Y

表 5: 内存排序摘要

使用更简单的缓存硬件, 这反过来又允许在 Alpha 的全盛时期更高的时钟频率。

最后一列指示给定的 CPU 是否具有不连贯的指令缓存和管线。此类 cpu 需要为自修改代码执行特殊指令。

带圆括号的 CPU 名称表示在架构上允许的模式, 但在实践中很少使用。

常见的 "只是说不" 的方法, 内存障碍可以非常合理的地方, 它适用, 但有环境, 如 Linux 内核, 在那里直接使用内存障碍是必需的。因此, Linux 提供了一个精心选择的 leastcommon 分母的内存屏障基元集, 如下所示:

  • smp mb (): "内存屏障", 它命令装载和存储。这意味着内存屏障之前的负载和存储将在内存屏障之后的任何负载和存储之前提交给内存。
  • smp 人民币 (): "读取内存屏障", 命令只加载。
  • smp wmb (): "写内存屏障", 只订购商店。
  • smp 读取 障碍 取决于 ()这将强制后续操作依赖于要订购的先前操作。这个原语是除 Alpha 之外的所有平台上的无运算。
  • mmiowb ()这迫使 MMIO 写的命令受到全球自旋锁的保护。在所有平台上, 自旋锁的内存屏障都已强制 MMIO 排序, 因此此原始元素是不操作的。nonno 的平台mmiowb ()定义包括一些 (但不是全部) IA64、FRV、MIPS 和 SH 系统。这个原始的是相对地新的, 因此相对地少量司机利用它。

的smp mb (),smp 人民币 ()和smp wmb ()基元还强制编译器避免任何优化, 这将使内存优化在各个障碍之间重新排序。的smp 读取障碍取决于 ()基元具有类似的效果, 但仅在 Alpha cpu 上。

这些基元只在 SMP 内核中生成代码, 但是每个元素都有一个向上版本 (Smp mb (),smp wmb ()和smp 读取障碍取决于 (), 分别) 产生内存屏障, 甚至在内核。的Smp 大多数情况下都应该使用版本。但是, 这些后基元在编写驱动程序时很有用, 因为即使在内核中, MMIO 访问也必须保持有序。在没有内存屏障指令的情况下, cpu 和编译器都将愉快地重新排列这些访问, 这充其量会使设备发生异常, 并可能导致您的内核崩溃, 甚至会损坏您的硬件。

因此, 大多数内核程序员不必担心每个 CPU 的内存屏障特性, 只要它们粘在这些接口上。如果你在一个给定的 CPU 的 architecturespecific 代码中工作深入, 当然, 所有的赌注都是假的。

此外, Linux 的所有锁定基元 (自旋锁、读写器锁、信号量、区域协调程序、...) 包括任何需要的屏障基元。所以, 如果你正在使用这些基元的代码, 你甚至不需要担心 Linux 的 memoryordering 原语。

也就是说, 对每个 CPU 的 memoryconsistency 模型的深入了解可以在调试时非常有用, 在编写 architecturespecific 代码或同步基元时不说什么。

此外, 他们说, 一点点知识是一件非常危险的事情。试想一下, 你可以用大量的知识来破坏你的一切!对于那些希望更多地了解单个 cpu 的内存一致性模型的人, 接下来的部分将描述那些最流行和最突出的 cpu。虽然没有什么可以代替实际读取给定 CPU 的文档, 但这些部分提供了一个很好的概述。

7。1 阿 尔 法

对于一个已经宣布生命终结的 CPU 来说, 这可能看起来有些奇怪, 但是 Alpha 很有趣, 因为使用最弱的内存排序模型, 它排序内存操作最积极。因此, 它定义了 Linuxkernel 内存排序基元, 它必须在所有 cpu (包括 Alpha) 上工作。因此, 理解 Alpha 对于 Linux 内核黑客来说是非常重要的。

Alpha 和其他 cpu 之间的区别由图9所示的代码来说明。这smp wmb ()在该图的9行保证在将元素添加到10行的列表之前执行行6-8 中的元素初始化, 这样就可以使无锁搜索正常工作。即, 它可以保证所有 cpu除了阿 尔 法。

Alpha 具有非常弱的内存排序, 因此图9行20中的代码可以看到在6-8 行初始化之前存在的旧垃圾值。

  1. 结构 el * 插入 (长密钥、长数据)
  2. {
  3. 结构 * p;
  4. p = kmalloc (sizeof (p), GPF_ATOMIC);
  5. spin_lock (互斥);
  6. 下一个 = 头。
  7. 钥匙 = 钥匙;
  8. 数据 = 数据;
  9. smp_wmb ();
  10. 头. 下一页 = p;
  11. spin_unlock (互斥);
  12. }

13

  1. 结构 el 搜索 (长键)
  2. {
  3. 结构 * p;
  4. p = 头. 下一页;
  5. 而 (p! = 和头) {
  6. 在 ALPHA!!! 上的错误*/
  7. 如果 (p-键 = = 键) {
  8. 返回 (p);
  9. }
  10. p = 下一个;
  11. };
  12. 返回 (NULL);
  13. }

图 9: 插入和无锁定搜索

图10显示了如何在具有分区缓存的积极并行计算机上发生这种情况, 以便交替缓存线由缓存的不同分区处理。假定列表头头将由缓存库0处理, 并且新元素将由缓存库1处理。在阿尔法,smp wmb ()将保证图9行6-8 所执行的缓存无效将在10行之前达到互连, 但绝对不能保证新值到达读取 CPU 核心的顺序。例如, 读取 CPU 的缓存库1非常忙, 但缓存库0处于空闲状态。这可能导致延迟的新元素缓存无效, 因此读取 CPU 获取指针的新值, 但会看到新元素的旧缓存值。请参阅前面的网站, 了解更多信息, 或者, 如果您认为我只是在做这些。[6]

人们可以将一个smp 人民币 ()原始的是-

编写 CPU 内核

互 连

读取 CPU 内核

(r) mb 排序

(r) mb 排序

缓存

银行0

缓存

银行1

缓存

银行0

缓存

银行1

(w) mb 排序

(w) mb 排序

 

 

 

6

图 10: 为什么 smp 读取障碍取决于 () 是必需的

将指针提取和取消引用补间。但是, 这在尊重读取端的数据依赖项的系统 (如 i386、IA64、PPC 和 SPARC) 上强加了不必要的开销。一个smp 读取障碍取决于 ()已将原始元素添加到 Linux 2.6 内核中, 以消除这些系统的开销。此原语可用如图 11 19 行所示。

还可以实施软件屏障, 以代替smp wmb (), 这将强制所有读取 cpu 查看写入 cpu 的写入顺序。然而, 这种方法被 Linux 社区认为在极弱有序的 cpu (如 Alpha) 上施加过多的开销。通过将处理器间中断 (IPIs) 发送到所有其他 cpu, 可以实现此软件屏障。在收到此类新闻学会后, CPU 将执行内存屏障指令, 实现内存屏障 shootdown。为了避免死锁, 需要额外的逻辑。当然, 尊重数据依赖关系的 cpu 将定义这样一个屏障, 以便简单地smp wmb ().也许这一决定应该在未来重新考虑, 因为阿尔法消失在日落。

Linux 内存屏障基元从 Alpha 指令中取了他们的名字, 所以smp mb ()是M b,smp 人民币 ()是人民币和smp wmb ()是wmb.Alpha 是

  1. 结构 el * 插入 (长密钥、长数据)
  2. {
  3. 结构 * p;
  4. p = kmalloc (sizeof (p), GPF_ATOMIC);
  5. spin_lock (互斥);
  6. 下一个 = 头。
  7. 钥匙 = 钥匙;
  8. 数据 = 数据;
  9. smp_wmb ();
  10. 头. 下一页 = p;
  11. spin_unlock (互斥);
  12. }

13

  1. 结构 el 搜索 (长键)
  2. {
  3. 结构 * p;
  4. p = 头. 下一页;
  5. 而 (p! = 和头) {
  6. smp_read_barrier_depends ();
  7. 如果 (p-键 = = 键) {
  8. 返回 (p);
  9. }
  10. p = 下一个;
  11. };
  12. 返回 (NULL);
  13. }

图 11: 安全插入和无锁搜索

只有 CPU 的地方smp 读取障碍取决于 ()是一个smp mb ()而不是一个不做的事

有关 Alpha 的详细信息, 请参阅参考手册 [19]。

7。2 AMD64

AMD64 与 x86 兼容, 最近更新了其内存模型 [2], 以强制执行实际实现所提供的更严格的顺序。Linux 的 AMD64 实现smp mb ()原始是mfence,Smp 人民币 ()是lfence和smp wmb ()是sfence.理论上, 这些可能是放松的, 但任何这样的放松必须考虑到 SSE 和3DNOW 指令。

7。3 IA64

IA64 提供了一个弱一致性模型, 因此在没有显式内存屏障指令的情况下, IA64 是在它的权利任意重新排序内存引用 [8]。IA64 有一个内存围栏指令, 名为Mf, 但也有 "半内存围栏" 修饰符加载, 存储, 并对其一些原子

 

图 12: 半内存屏障

指示 [7]。的acq修饰符防止随后的内存引用指令重新排序。acq, 但允许以前的 memoryreference 指令重新排序后,acq, 如图12所示. 地那些鲜肉。同样,相对修饰符防止前面的内存引用指令重新排序。相对, 但允许随后的内存引用指令重新排序。相对.

这些半内存围栏对于关键部分很有用, 因为将操作推入关键部分是安全的, 但这可能是致命的, 允许它们流血。但是, 作为具有此属性的唯一 cpu 之一, IA64 定义了 Linux 与锁定获取和释放相关的内存排序的语义。

IA64Mf指令用于smp 人民币 (),smp mb ()和smp wmb ()Linux 内核中的基元。哦, 尽管有相反的谣言, "mf" mneumonic 确实支持 "记忆栅栏"。

最后, IA64 提供了 "发布" 操作的全局总订单, 包括 "mf" 指令。这提供了传递性的概念, 如果给定的代码片段在发生时看到给定的访问权限, 则以后的代码片段也会看到以前的访问已经发生。假设, 即所有涉及的代码片段都正确地使用了内存屏障。

7。4  PA-RISC

尽管 PA RISC 体系结构允许对负载和存储进行完全重新排序, 但实际的 cpu 运行完全有序 [14]。这意味着 Linux 内核的内存排序基元不会生成任何代码, 但是, 它们确实使用 gcc记忆属性禁用编译器优化, 以便在内存屏障上重新排序代码。

7。5     权力

电源和电源 PC°RCPU 家庭有各种各样的记忆障碍指示 [6, 15]:

  1. 同步导致所有前面的操作都似乎有在任何后续操作开始之前完成。因此, 这项指示相当昂贵。
  2. lwsync(轻量同步) 对后续的装载和存储以及订单存储进行装载。然而, 它确实对后续负载订购存储。有趣的是,lwsync指令强制执行与 zSeries 相同的排序, 巧合的是 SPARC。
  3. eieio(强制按顺序执行 i/o, 以防您想知道) 导致所有前面的可缓存存储在所有后续存储之前都已完成。但是, 可缓存内存的存储从存储区单独订购到不可缓存内存, 这意味着eieio不会强制 MMIO 存储在自旋锁版本之前。
  4. isync在任何后续指令开始执行之前, 强制所有前面的指令看起来已完成。这意味着前面的指令必须已经取得了足够的进展, 以至于它们可能生成的任何陷阱都已发生或保证不发生, 并且这些指令的任何副作用 (例如, 页表更改) 都由随后的说明。

不幸的是, 这些指令中没有一个完全符合 Linux 的wmb ()原始, 这需要所有要订购的存储区, 但不需要其他高开销的操作同步教学。但没有选择: ppc64 版本的wmb ()和mb ()被定义为重量级同步教学。然而, Linux 的smp wmb ()指令从不用于 MMIO (因为驱动程序必须仔细地订购 MMIOs 和 SMP 内核, 毕竟), 所以它被定义为较轻的重量eieio教学。这个指令很可能是独特的, 有一个五元音 mneumonic, 这代表了 "强制执行的 i/o"。的smp mb ()指令也被定义为同步指令, 但两者都smp 人民币 ()和人民币 ()被定义为 lighterweightlwsync教学。

功率特征为 "cumulativity", 可用于获取传递性。当正确使用时, 查看早期代码片段结果的任何代码也将看到此早期代码片段本身所看到的访问。更多细节从麦肯尼和 Silvera 是可利用的 [18]。

电源结构的许多成员都有不连贯的指令缓存, 因此存储到内存不一定会反映在指令缓存中。谢天谢地, 现在很少有人编写自修改代码, 但是 JITs 和编译器一直都在这样做。此外, 重新编译最近运行的程序看起来就像从 CPU 的角度自修改代码。的icbi指令 (指令缓存块无效) 使指定的缓存行从指令缓存中无效, 并可在这些情况下使用。

7。6      SPARC RMO、PSO 和祖

sparc 上的 Solaris 使用草 (总存储订单), Linux 在为 "SPARC" 32 位体系结构构建时也是如此。但是, 64 位 Linux 内核 ("sparc64" 体系结构) 在 RMO (轻松内存顺序) 模式下运行 SPARC [20]。SPARC 体系结构还提供了中间 PSO (部分存储顺序)。在 RMO 中运行的任何程序也将在 pso 或左宗棠中运行, 类似地, 在 pso 中运行的程序也将在曹中运行。将共享内存并行程序移动到另一个方向可能需要小心插入内存屏障, 但如前所述, 使同步基元标准使用的程序不必担心内存障碍。

SPARC 具有非常灵活的内存屏障指令 [20], 允许对顺序进行细粒度控制:

StoreStore: 在后续商店之前订购前面的商店。(此选项由 Linux 使用smp wmb ()原始。

LoadStore: 在随后的商店之前订购前装运。

StoreLoad: 在后续装载之前订购前商店。

LoadLoad: 在随后的加载之前先订购之前的负载。(此选项由 Linux 使用smp 人民币 ()原始。

同步: 在开始任何后续操作之前完全完成前面的所有操作。

MemIssue: 在后续内存操作之前完成前面的内存操作, 对于内存映射 i/o 的某些实例很重要。

旁路: 与 MemIssue 相同, 但只适用于前面的存储和随后的加载, 甚至仅用于访问相同内存位置的存储和加载。

的 Linuxsmp mb ()原始使用前四个选项在一起, 如在membar #LoadLoad

|#LoadStore |#StoreStore |#StoreLoad, 从而完全订购内存操作。

那么, 为什么membar #MemIssue需要?因为一个membar #StoreLoad可以允许随后的负载从写入缓冲区获取其值, 如果写入的 MMIO 寄存器导致对要读取的值产生副作用, 则这将是灾难性的。相比之下,membar #MemIssue将等到写入缓冲区刷新后才允许加载执行, 从而确保负载实际上从 MMIO 寄存器获得其值。驱动程序可以改用membar #Sync, 但重量轻membar #MemIssue是首选的情况下, 额外的功能更昂贵的membar #Sync不是必需的。

的membar #Lookaside是一个重量较轻的版本membar #MemIssue, 当写入给定的 MMIO 寄存器时, 它会影响下一个从该寄存器读取的值。然而, 重的重量membar #MemIssue当对给定的 MMIO 寄存器的写入影响将从下面读取的值时, 必须使用其他一些MMIO 注册。

不清楚 SPARC 为什么不定义wmb ()要被membar #MemIssue和smb wmb ()要被membar #StoreStore, 因为当前的定义似乎容易受到某些驱动程序中的 bug 的影响。Linux 运行的所有 SPARC cpu 都有可能实现比体系结构更保守的内存订购模型。

SPARC 需要一个冲洗指令在存储和执行指令的时间之间使用 [20]。需要从 SPARC 的指令缓存中刷新该位置的任何先前值。注意,冲洗获取地址, 并且只从指令缓存中刷新该地址。在 SMP 系统上, 所有 cpu 的缓存都被刷新, 但是没有任何方便的方法来确定 cpu 的刷新何时完成, 尽管有一个对实现说明的引用。

7。7 x86

由于 x86 cpu 提供 "进程排序", 因此所有 cpu 都同意给定 CPU 写入内存的顺序, 则smp wmb ()原始是 noop 为 CPU [10]。但是, 需要编译器指令来防止编译器执行优化, 从而导致在整个smp wmb ()原始。

另一方面, x86 cpu 传统上没有给出负载的排序保证, 因此smp mb ()和smp 人民币 ()基元扩展到锁; addl.这种原子指令对负载和存储都是一个障碍。

最近, 英特尔发布了 x86 [11] 的内存模型。事实证明, 英特尔的实际 cpu 比以前的规范中要求的要严格, 所以这个模型实际上只是授权先前的实际行为。最近, 英特尔为 x86 [12] 发布了一个更新的内存模型, 它为存储提供了一个全局订单, 尽管单个 cpu 仍被允许看到它们自己的存储, 这比全球订单之前的情况更早。为了允许涉及存储缓冲区的重要硬件优化, 需要对总订单进行此异常。软件可以使用原子操作来覆盖这些硬件优化, 这也是原子操作比非原子对应更昂贵的一个原因。此总存储订单是在较旧的处理器上得到保证。

但是, 请注意, 一些 SSE 指令是弱有序的 (clflush和非世俗移动指示 [9])。具有 SSE 的 cpu 可以使用mfence为smp mb (),lfence为smp 人民币 ()和sfence为smp wmb ().

x86 CPU 的几个版本有一个模式位, 它启用了顺序存储, 对于这些 cpu,smp wmb ()也必须定义为锁; addl.

尽管许多旧的 x86 实现都满足了自修改代码, 而不需要任何特殊的指令, 但 x86 体系结构的更新修改不再要求 x86 cpu 如此适合。有趣的是, 这种放松来得正是时候, 给 JIT 实现者带来不便。

7。8 zSeries

TmzSeries 机组成了 IBM 大型机家族, 以前称为360、370和 390 [13]。zSeries 的并行性来得晚, 但考虑到这些大型机最早在二十世纪六十年代中旬出货, 这并不是说得太多。的bcr 150指令用于 Linuxsmp mb (),smp 人民币 ()和smp wmb ()原。它还具有相对强的内存排序语义, 如表5所示, 它应允许smp wmb ()原始的是一个nop(当你读到这个时, 这种变化很可能发生了)。该表实际上低估了这种情况, 因为 zSeries 内存模型在其他顺序上是一致的, 这意味着所有 cpu 都将同意来自不同 cpu 的无关存储的顺序。

与大多数 cpu 一样, zSeries 体系结构不保证缓存连贯的指令流, 因此, 自修改代码必须在更新指令和执行它们之间执行序列化指令。换句话说, 许多实际的 zSeries 机器实际上是在不序列化指令的情况下适应自修改代码的。zSeries 指令集提供了一大组序列化指令, 包括比较和交换、某些类型的分支 (例如, 上述bcr 150指令), 以及测试和设置等。

8   记忆障碍是否永远存在?

最近的一些系统在一般的顺序执行方面明显不那么激进, 特别是重新订购内存引用。这种趋势会持续到记忆障碍是过去的问题吗?

赞成的论据将引用建议的大规模多线程硬件体系结构, 以便每个线程都将等待, 直到内存准备就绪, 十个, 数以百计, 甚至数以千计的其他线程在此期间取得进展。在这样的体系结构中, 不需要内存屏障, 因为给定的线程在继续下一个指令之前, 只需等待所有未完成的操作。由于可能会有数以千计的其他线程, cpu 将被完全利用, 因此不会浪费 cpu 时间。反对的论据将引用数量极有限的应用程序, 能够扩展多达1000个线程, 以及日益严重的实时需求, 对于某些应用程序来说, 这是一个微秒。由于大量多线程方案所隐含的极低的单线程吞吐量, realtimeresponse 的要求非常困难, 难以满足。

另一种赞成的论据将会提到越来越复杂的滞后时间隐藏硬件实现技术, 这很可能允许 CPU 提供完全按顺序一致执行的错觉, 同时仍然提供几乎所有的顺序执行的性能优势。

相反的论点将会提到电池操作设备和环境责任所带来的越来越严重的电力效率要求。

谁是对的?我们没有头绪, 所以准备住在任何一种情况下。

9   对硬件设计人员的建议

硬件设计者可以做许多事情来使软件人的生活变得困难。下面是我们过去遇到过的一些这样的事情的清单, 在这里提出, 希望它可能有助于防止未来的这些问题:

  1. 忽略缓存一致性的 i/o 设备。

这个迷人的 misfeature 会导致 DMAs 从内存中丢失最近对输出缓冲区所做的更改, 或者同样糟糕的是, 在 DMA 完成后, CPU 缓存的内容会覆盖输入缓冲区。要使您的系统能够正常工作, 您必须仔细刷新任何 DMA 缓冲区中任何位置的 CPU 缓存, 然后再将该缓冲区呈现给 i/o 设备。即使这样, 你也需要非常小心避免指针错误, 因为即使是误读到输入缓冲区也会导致数据输入损坏!

  1. 忽略缓存一致性的设备中断。

这听起来可能不够天真-毕竟, 中断不是内存引用, 是吗?但是想象一下, 一个带有拆分缓存的 CPU, 其中一个银行非常忙, 因此保持到输入缓冲区的最后一个 cacheline。如果相应的 i/o 完全中断到达此 cpu, 则 cpu 对缓冲区最后缓存行的内存引用可能会返回旧数据, 再次导致数据损坏, 但在以后的崩溃转储中将不可见的窗体中。当系统绕过有冲突的输入缓冲区时, DMA 很可能已经完成。

  1. 处理器间中断 (IPIs), ignorecache 一致性。

如果在相应消息缓冲区中的所有缓存行都已提交内存之前, 新闻学会到达其目标, 这可能会有问题。

  1. 提前到达缓存一致性的上下文切换。

如果内存访问可以完成过于疯狂的顺序, 那么上下文切换可能是相当悲惨的。如果任务在源 cpu 可见的所有内存访问之前掠过到另一个 cpu, 那么任务就可以很容易地看到相应的变量恢复到以前的值, 这会使大多数算法产生致命的混淆。

  1. 过于亲切的模拟器和仿真器。

很难编写模拟器或仿真程序来强制内存重新排序, 因此在这些环境中运行正常的软件会在第一次在真正的硬件上运行时得到令人不快的惊喜。不幸的是, 它仍然是规则, 硬件是比模拟器和仿真器更狡猾, 但我们希望这种情况改变。

再次, 我们鼓励硬件设计者避免这些做法!

确认

我感谢许多 CPU 架构师耐心地解释他们的 cpu 的指令和 memoyr 的功能, 特别是韦恩 Cardoza, Ed Silha, 安东. ·弗雷, 凯茜, 德里克. 威廉斯, 蒂姆 Slegel, 史塔克 Probst, Ingo Adlung, 和拉维 Arimilli韦恩值得特别感谢他的耐心解释字母重新排序的从属负载, 一个教训, 我抗拒相当 strenu-显然!

法律声明

此工作代表作者的视图, 不一定代表 IBM 的视图。

IBM、zSeries 和电源 PC 是美国、其他国家或两者的国际商用机器公司的商标或注册商标。

Linux 是莱纳斯托瓦尔兹的注册商标。i386 是英特尔公司或其子公司在美国、其他国家或两者的商标。其他公司、产品和服务名称可能是此类公司的商标或服务标志。版权所有°c 2005 IBM 公司。

快速测验的答案

快速测验 1:

回答:如果两个 cpu 试图同时使同一高速缓存行无效, 会发生什么情况?

其中一个 cpu 首先获得共享总线的访问权限, cpu "获胜"。另一个 cpu 必须使其缓存行的副本无效, 并向另一个 cpu 发送 "无效确认" 消息。当然, 丢失的 cpu 可以被期望立即发出 "读取无效" 的交易, 因此获胜的 cpu 的胜利将是相当短暂的。

快速测验 2:

回答:当大型多处理器中出现 "无效" 消息时, 每个 CPU 都必须发出 "无效确认" 响应。导致 "无效确认" 响应的 "风暴" 不会完全饱和系统总线吗? 如果大规模多处理器实际上是以这种方式实现的, 它可能。较大的多处理器, 特别是 NUMA 机器, 倾向于使用所谓的 "基于目录的" 缓存一致性协议, 以避免出现此问题和其他难题。

快速测验 3:

回答:如果 smp 计算机真的使用消息传递, 那么为什么还要用 smp 呢? 在过去的几十年里, 关于这个问题有相当多的争议。一个答案是, 缓存一致性协议非常简单, 因此可以直接在硬件中实现, 从而获得软件消息传递无法达到的带宽和延迟。另一个答案是, 由于大型 smp 机器的相对价格, 以及小型 smp 机器的集群, 在经济学中发现了真正的真理。第三个答案是 SMP 编程模型比分布式系统更容易使用, 但反驳可能会注意到 HPC 集群和 MPI 的出现。所以争论还在继续。

快速测验 4:

回答:硬件如何处理上面描述的延迟过渡?

通常是通过添加其他状态, 尽管这些附加的状态不必实际存储在缓存行中, 这是因为每次只有几行会转换。延迟转换的需要只是一个问题, 导致现实世界的缓存一致性协议比本附录中描述的超简化 MESI 协议复杂得多。轩尼诗和帕特森对计算机体系结构的经典介绍 [5] 涵盖了许多这些问题。

快速测验 5:

回答:什么操作顺序会将 cpu 的缓存全部恢复到 "无效" 状态?

 

没有这样的序列, 至少在没有特殊的 "刷新我的缓存" 指令在 CPU 的指令集。大多数 cpu 都有这样的指令。

快速测验 6:

回答:是否保证每个 CPU 都能看到自己的内存访问, 以保证每个用户级线程都能按顺序看到自己的内存访问?为什么?

不。请考虑线程从一个 CPU 迁移到另一个中央处理器的情况, 以及目标 cpu 对源 cpu 最近的内存操作的感知不有序的情况。为了保持用户模式的健全, 内核黑客必须在上下文切换路径中使用内存屏障。但是, 安全执行上下文切换所需的锁定将自动提供导致用户级任务按顺序查看其自身访问所需的内存障碍。如果您正在设计一个超级优化的调度程序, 无论是在内核还是在用户级别, 请牢记此方案!

快速测验 7:

回答:是否可以通过在 CPU 1 的 "同时" 和 "c" 任务之间插入内存屏障来修复此代码?为什么?

不。这样的内存屏障只会强制命令本地 CPU 1。它不会对 cpu 0 和 cpu 1 访问的相对排序产生影响, 因此断言仍可能失败。然而, 所有主流计算机系统提供一种或另一个机制来提供 "传递性", 这提供了直观的因果排序: 如果 b 看到了 a 的访问效果, c 看到了 b 的访问效果, 那么 c 也必须看到 a 的访问效果。.

快速测验 8:

回答:假设 cpu 1 和2的行3-5 在中断处理程序中, cpu 2 的行9在进程级别运行。为了使代码能够正确工作, 是否需要进行哪些更改, 换言之, 以防止断言被触发? 断言将需要编码, 以确保 "e" 的负载在 "a" 之前。在 Linux 内核中, 屏障 () 原语可用于实现这一点, 这与前面示例中断言中使用内存屏障的方式非常相似。

引用

  1. 先进的微型设备.AMD x86-64 体系结构程序员手册卷1-5, 2002。
  2. 先进的微型设备.AMD x86-64 体系结构程序员手册卷 2: 系统编程, 2007。
  3. Culler, d.e., 辛格, jp 和古普塔, A。并行计算机体系结构: 硬件/软件方法.摩根考夫曼,

1999。

  1. Gharachorloo, K。共享内存多处理器的内存一致性模型。斯坦福大学电子工程与计算机科学系 CSL-TR-95-685, 计算机系统实验室, 斯坦福, 加州, 1995年12月。可用:http://www.hpl.hp.com/techreports/康柏-东汽/WRL-95-9. pdf[查看: 2004年10月11日]。
  2. 轩尼诗, j.l. 和帕特森, 检察官计算机体系结构: 一种定量方法.摩根考夫曼, 1995。
  3. IBM 微电子与摩托罗拉.PowerPC 微处理器系列: 编程环境, 1994。
  4. 英特尔公司.英特尔安腾体系结构软件开发商手册卷 3: 指令集参考, 2002。
  5. 英特尔公司.英特尔安腾体系结构软件开发人员手册卷 3: 系统体系结构, 2002。
  6. 英特尔公司.IA-32 英特尔体系结构软件开发商手册卷 2B: 指令集参考, N Z, 2004。可用:ftp://download.intel.com/design/Pentium4/手册/25366714. pdf[查看: 2005年2月16日]。
  7. 英特尔公司.IA-32 英特尔体系结构软件开发商手册卷 3: 系统编程指南, 2004。可用:ftp://download.intel.com/design/Pentium4/手册/25366814. pdf[查看: 2005年2月16日]。
  8. 英特尔公司.英特尔64体系结构内存订购白皮书, 2007。可用:http://developer.intel.com/产品/处理器/手册/318147. pdf[查看: 2007年9月7日]。
  9. 英特尔公司.英特尔64 IA-32 体系结构软件开发人员手册, 3A: 系统编程指南, 1 部分, 2009。可用:http://download.intel。com/设计/处理器/手册/253668. pdf[查看: 2007年9月7日]。
  10. 国际商用机器公司.z/体系结构原理的操作。可用:http://publibz.boulder。ibm.com/epubs/pdf/dz9zr003.pdf[查看: 2005年2月16日], 2004年5月。
  11. 凯恩湾RISC 2.0 体系结构.HewlettPackard 专业书1996。
  12. 里昂, m, Silha, E 和干草, B。PowerPC 存储模型和 AIX 编程。可用:http://www-106.ibm.com/developerworks/eserver/文章/powerpc. html[查看: 2005年1月31日], 2002年8月。
  13. 麦肯尼, 体育现代微处理器中的内存排序, 第一部分。Linux 杂志1, 136 (2005年8月), 52–57。可用:http

www.linuxjournal.com/article/8211 http://www.rdrop.com/users/paulmck/可伸缩性/纸张/订购2007.09.19a。Pdf[查看 2007年11月30日]。

  1. 麦肯尼, 体育现代微处理器中的内存排序, 第二部分。Linux 杂志1, 137 (2005年9月), 78–82。可用:http

www.linuxjournal.com/article/8212 http://www.rdrop.com/users/paulmck/可伸缩性/纸张/订购2007.09.19a。Pdf[查看 2007年11月30日]。

  1. 麦肯尼, 体育和 Silvera, R。c++ 内存模型的电源实现示例。可用:http://www.rdrop.com/users/paulmck/scalability/paper/N2745r 2009.02.27a. html[查看: 2009年4月5日], 2009年2月。
  2. 站点、r.l. 和 Witek, 室温阿尔法 AXP

建筑, 第二版数字媒体, 1995。

  1. SPARC 国际.SPARC 体系结构手册, 1994。

 

[1]使用多个级别的高速缓存是标准的做法, 一个小型的高速缓存接近 CPU, 具有单周期访问时间, 并且具有更长的访问时间 (大约为十时钟周期) 的更大级别两个缓存。高性能 cpu 通常具有三甚至四级缓存。

[2]请参见 Culler et. [3] 页670和671为 SGI Origin2000 和相继 (现在 IBM) NUMA Q 分别的九状态和26状态图。两个图都比实际生活简单得多。

[3]将高速缓存线从一个 CPU 的缓存传输到另一台处理器所需的时间通常比执行简单寄存器到寄存器指令所需的数量多一些。

[4]读者更倾向于详细查看真正的硬件体系结构, 以咨询 CPU 供应商手册 [19、1、8、6、15、20、10、9、13] 或 Gharachorloo 的论文 [4]。

[5]任何真正的硬件架构师或设计师无疑会大声呼吁拉尔夫在瓷对讲机, 因为他们可能只是有点心烦的前景的工作, 其中一个队列应该处理的消息涉及缓存线, 这两个 cpu 访问, 对这个例子所构成的许多种族没有任何发言权。我只能说 "给我一个更好的例子"。

[6]当然, 精明的读者将已经认识到, 阿尔法几乎没有任何卑鄙和肮脏的, 因为它可能是, (谢天谢地) 神话建筑在6.1 节是一个例子。

© 著作权归作者所有

共有 人打赏支持
Romane
粉丝 0
博文 46
码字总数 35352
作品 0
什么是网络安全?网络安全的主要类型

网络安全 网络安全指的是可以保障数据和网络的不被黑客入侵攻击,可以安全的运作。有效的网络安全管理对网络的访问。它针对各种威胁,并阻止危险进入或传播到您的网络。网络安全员都会对网络...

网络安全
2017/12/14
0
0
DDR3 内存存在硬件漏洞:可被修改数据

在通常,黑客都是研究系统和软件等的漏洞。不过这次黑客把目标瞄准了硬件。根据外媒报道,黑客发现在某些类型的DDR芯片上存在设计缺陷,可以用来提升Linux系统权限。谷歌的Project Zero博客周...

oschina
2015/03/11
5.1K
38
15岁少年黑了比特币钱包后,奉上了这篇诚意满满的破译教程

近期,一名英国的15岁黑客Saleem Rashid爆出,Ledger Nano S这款比特币硬件钱包有严重的安全性漏洞,它可能会被黑客控制,成为黑客安插在你身边的“间谍”,偷走你的比特币。 什么是Ledger ...

雪花又一年
04/18
0
0
Oracle安全攻防,你可能不知道自己一直在裸奔

作者介绍 刘思成,安华金和安全攻防实验室。 主题简介: 一场策划有序的入侵行动中,黑客组织常常会找到网站、操作系统、系统软件上的Vulnerability(弱点),有针对性地开展一系列组合攻击,...

刘思成
2017/04/21
0
0
Android 新漏洞泄露敏感信息

IBM的研究人员发现,大约有86%的Android手机中都存在一个漏洞。黑客可能借此获取用户的敏感信息,包括银行服务和虚拟专用网络(VPN)的密钥,以及用于解锁设备的PIN码或图形。 该漏洞位于Andro...

oschina
2014/07/01
2.7K
12

没有更多内容

加载失败,请刷新页面

加载更多

你为什么在Redis里读到了本应过期的数据

一个事故的故事 晚上睡的正香突然被电话吵醒,对面是开发焦急的声音:我们的程序在访问redis的时候读到了本应过期的key导致整个业务逻辑出了问题,需要马上解决。 看到这里你可能会想:这是不...

IT--小哥
今天
2
0
祝大家节日快乐,阖家幸福! centos GnuTLS 漏洞

yum update -y gnutls 修复了GnuTLS 漏洞。更新到最新 gnutls.x86_64 0:2.12.23-22.el6 版本

yizhichao
昨天
5
0
Scrapy 1.5.0之选择器

构造选择器 Scrapy选择器是通过文本(Text)或 TextResponse 对象构造的 Selector 类的实例。 它根据输入类型自动选择最佳的解析规则(XML vs HTML): >>> from scrapy.selector import Sele...

Eappo_Geng
昨天
4
0
Windows下Git多账号配置,同一电脑多个ssh-key的管理

Windows下Git多账号配置,同一电脑多个ssh-key的管理   这一篇文章是对上一篇文章《Git-TortoiseGit完整配置流程》的拓展,所以需要对上一篇文章有所了解,当然直接往下看也可以,其中也有...

morpheusWB
昨天
5
0
中秋快乐!!!

HiBlock
昨天
3
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部