消除线程本地内存的缓存侦听

Eliding cache snooping for thread-local memory

现代多核 CPUs 通过窥探同步内核之间的缓存,即每个内核广播它在内存访问方面正在做什么,并观察其他内核生成的广播,以合作确保写入核心A被核心B看到。

这样做的好处在于,如果您有确实需要在线程之间共享的数据,它可以最大限度地减少您必须编写的代码量以确保它确实得到共享。

糟糕的是,如果您只有一个线程的本地数据,窥探仍然会发生,不断地消耗能量而没有任何目的。

如果声明相关变量thread_local,是否仍然会发生窥探?不幸的是,根据 Can other threads modify thread-local memory?

的公认答案,答案是肯定的

当前存在的任何平台(CPU 和操作系统的组合)是否提供任何方法来关闭对线程本地数据的侦听?不一定是便携的方式;如果它需要发出 OS-特定的 API 调用,或者甚至进入汇编,我仍然感兴趣。

有一个基于失效的基本协议 MESI,它有点基础。它还有其他扩展,但它用于最小化读取或写入时的总线事务数。 MESI 对缓存行可能处于的状态进行编码:修改、独占、共享、无效。 MESI 的基本示意图涉及两个视图。破折号 (-) 表示内部状态可能发生变化,但不需要外部操作。从 CPU 到它的缓存:

           M   E  S   I
Read       -   -  -   2
Write      -   -  1   3

其中:

  1. 发出总线无效,将状态更改为M。
  2. 发出总线读取,将状态更改为 S。
  3. 发出总线读取+总线无效,将状态更改为M。

此外,这些状态“监听”外部总线,因此从总线到缓存:

           M   E  S  I
Read       4   -  -  -
Write      5   -  -  -
  1. 从缓存中刷新,更改为 S。
  2. 从缓存中刷新,更改为 I.

所以总线代理合作只生成最少的必要事务。

许多 CPU 的,特别是嵌入式控制器,有 cpu-private-memory,这可能是线程本地存储的一个很好的候选者;然而,要将一个线程从一个核心迁移到另一个核心,需要追踪其所有线程本地存储变量,并将它们(以某种方式)复制到新核心的私有内存中。

根据工作负载,这可能是可行的,但对于一般工作负载,最小化总线流量和放松亲和力是一个胜利。

大多数现代处理器使用目录一致性协议来维护同一 NUMA 节点中所有内核之间的一致性,并使用另一个目录一致性协议来维护同一一致性域中的所有 NUMA 节点和 IO 集线器之间的一致性,其中每个NUMA 节点可以是活动套接字、活动套接字的一部分或节点控制器。对真实处理器中一致性的简要介绍可以在以下位置找到:Cache coherency(MESI protocol) between different levels of cache namely L1, L2 and L3.

目录一致性协议显着减少了广播侦听的需要,因为它们为每个缓存行提供了额外的一致性状态,以便从根本上跟踪谁可能拥有该行的副本。在以下情况下仍然会发生不必要的窥探:

  • 在不通知目录控制器的情况下,一条线从核心或 NUMA 节点中被悄无声息地逐出。
  • 目录状态可能受到错误检测代码的保护。如果状态被认为已损坏,则需要进行广播。
  • 根据微体系结构,内存中目录可能无法跟踪每个 NUMA 节点的缓存行,而是具有“任何其他 NUMA 节点”的粒度。

不必要的窥探的代价不仅仅是额外的能源消耗,还有延迟,因为除非所有一致性事务都已完成,否则不能认为请求已经非推测性地完成。这会显着增加完成请求的时间,从而限制带宽,因为每个未完成的请求都会消耗一定的硬件资源。

只要真正用作线程局部变量并且拥有这些变量的线程很少在物理内核之间迁移,您就不必担心对存储线程局部变量的缓存行进行不必要的窥探。