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

问题描述

现代多核 cpu 通过窥探同步内核之间的缓存,即每个内核广播它在内存访问方面所做的事情,并观察其他内核生成的广播,以合作确保内核 B 看到来自内核 A 的写入.

这很好,因为如果您确实需要在线程之间共享数据,它可以最大限度地减少您必须编写的代码量以确保它确实被共享。

不好的是,如果您的数据应该只存在于一个线程的本地,那么窥探仍然会发生,不断地无谓地耗散能量。

如果你声明了相关的变量thread_local,监听还会发生吗?不幸的是,根据对 Can other threads modify thread-local memory?

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

是否有任何当前现存的平台(cpu 和操作系统的组合)提供任何方法关闭对线程本地数据的监听?不必是可移植的方式;如果它需要发出特定于操作系统的 API 调用,甚至进入程序集,我仍然感兴趣。

解决方法

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

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

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

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

您不必担心不必要的窥探来缓存存储线程局部变量的行,只要它们确实被用作线程局部变量并且拥有这些变量的线程很少在物理内核之间迁移。

,

有一个基于失效的基本协议 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,这可能是线程本地存储的理想选择;但是,要将线程从一个内核迁移到另一个内核,则需要追踪其所有线程本地存储变量,并将它们(以某种方式)复制到新内核的私有内存中。

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