为什么 x86 没有实现直接的核心到核心消息传递 assembly/cpu 指令?

Why didn't x86 implement direct core-to-core messaging assembly/cpu instructions?

经过认真的发展,CPU 获得了很多核心,在多个小芯片、numa 系统等上获得了分布式核心块,但仍然有一段数据不仅要通过 L1 缓存(如果在同一个核心 SMT 上),还要通过一些 atomic/mutex 未被硬件加速的同步原语过程。

我想知道为什么英特尔或 IBM 没有想出类似的东西:

movcor 1 MX 5 <---- sends 5 to Messaging register of core 1
pipe 1 1 1 <---- pushes data=1 to pipe=1 of core=1 and core1 needs to pop it
bcast 1 <--- broadcasts 1 to all cores' pipe-0 

让它比其他一些方法快得多? GPU 支持块级快速同步点,如 barrier()__syncthreads()。 GPU 还支持本地数组的并行原子更新加速。

当 CPU 获得 256 个内核时,此功能不会为在内核到内核带宽(and/or 延迟)上存在瓶颈的各种算法实现严重扩展吗?

CPUs 进化为 非常 不同于 GPU 的编程模型,运行 多个独立的线程,可能是不同的进程,所以你会还需要软件和 OS 基础设施来让线程知道其他线程 运行 正在使用哪个其他核心(如果有的话)。或者他们必须将每个线程固定到一个特定的核心。但即便如此,它仍需要某种方法来虚拟化架构 message-passing 寄存器,就像上下文切换在每个内核上虚拟化 multi-tasking 的标准寄存器一样。

因此,在正常 OS 下甚至可以使用此类任何东西之前还有一个额外的障碍,其中单个进程不会完全拥有物理核心。 OS 仍然可能将其他进程的其他线程调度到内核上,并且 运行 宁中断处理程序,这与 GPU 不同,GPU 的内核没有任何其他事情要做,并且都是为了在一个大规模并行问题。

Intel did 在 Sapphire Rapids 中引入 user-space 中断。 包括用户 IPI(inter-processor 中断),但这不涉及在上下文切换时必须获得 saved/restored 的接收队列。 OS 仍然需要管理一些东西(比如用户中断目标 table),但我认为这对于上下文切换来说不是问题。它解决的问题与您所建议的不同,因为它是中断而不是消息队列。

在时通知另一个线程在共享内存中查找数据是需要解决的问题的难点,而不是在内核之间获取数据。共享内存对此仍然没问题(特别是像 cldemote 这样的新指令让编写器请求将 recently-stored 缓存行写回共享 L3,其他内核可以更有效地读取它)。请参阅下面有关 UIPI 的部分。


需要这样的任务通常最好在 GPU 上完成,而不是几个单独的深度流水线 OoO exec CPU 内核试图进行推测执行。与简单的 in-order 管道的 GPU 不同。

您实际上无法将结果推送到另一个核心,直到它在执行它的核心上退出。因为如果您发现 mis-speculation(例如在导致此问题的执行路径中较早的分支预测错误),您不希望也必须回滚另一个核心。可以想象,这仍然允许 lower-latency 而不是在共享内存的内核之间弹跳 cache-line,但它是一个非常狭窄的 class 可以使用它的应用程序。

然而,high-performance 计算是现代 CPU 的已知 use-case,因此如果它真的是 game-changer,那么作为设计选择值得考虑, 也许。

鉴于现有 CPU 的体系结构,有些事情不容易或不可能有效地完成。具有 fine-grained 线程间协作的小型工作单元是一个问题。如果实施可行,您的想法可能会有所帮助,但存在重大挑战。


Inter-processor 中断 (IPI),包括 user-IPI

为了OS使用,有IPI机制。但这会触发一个中断,因此当接收方的 out-of-order exec 管道到达它时它不会排列要读取的数据,因此它与您建议的机制非常不同,因为 use-case s.

相当low-performance 除了避免对方轮询。为了能够从 power-saving 睡眠状态唤醒一个核心,如果现在有更多线程准备 运行 那么它应该唤醒可以调用 schedule() 来确定要 运行.

任何内核都可以向任何其他内核发送 IPI,如果它 运行 在内核模式下。

New in Sapphire Rapids,硬件支持 OS 让 user-space 进程在 user-space.[=115= 中完全处理一些中断]

https://lwn.net/Articles/869140/ 是一个 LKML post 解释它以及 Linux 如何支持它。对于 ping-ponging 两个 user-space 线程之间的一条小消息,它显然比“eventfd”快 10 倍一百万次。或者比使用 POSIX 信号处理程序快 15 倍。

Kernel managed architectural data structures

  • UPID: User Posted Interrupt Descriptor - Holds receiver interrupt vector information and notification state (like an ongoing notification, suppressed notifications).
  • UITT: User Interrupt Target Table - Stores UPID pointer and vector information for interrupt routing on the sender side. Referred by the senduipi instruction.

The interrupt state of each task is referenced via MSRs which are saved and restored by the kernel during context switch.

Instructions

  • senduipi <index> - send a user IPI to a target task based on the UITT index.
  • clui - Mask user interrupts by clearing UIF (User Interrupt Flag).
  • stui - Unmask user interrupts by setting UIF.
  • testui - Test current value of UIF.
  • uiret - return from a user interrupt handler.

所以它在上下文切换时确实有新状态saved/restored。不过,我怀疑它比您想象的队列要小。关键是,对于不是 运行ning 的线程,任何地方都不需要接收队列,因为状态涉及内存中的 tables 并且没有数据,我猜只是一个挂起或未标记。所以 在 not-running 接收方的情况下,它可以在 table 中设置一个发送方无论如何都需要能够看到的位 ,以便 HW 知道在哪里指导 UIPI。不像需要找到内核保存的 register-state 或其他一些 space 并为您的想法附加到 variable-size(?) 缓冲区。

If the receiver is running (CPL=3), then the user interrupt is delivered directly without a kernel transition. If the receiver isn't running the interrupt is delivered when the receiver gets context switched back. If the receiver is blocked in the kernel, the user interrupt is delivered to the kernel which then unblocks the intended receiver to deliver the interrupt.

所以数据还是要经过内存的,这只是告诉另一个线程什么时候去看所以它没有轮询/旋转。
我认为 UIPI 对不同的人很有用use-case比您建议的消息队列少。

所以当接收线程知道特定数据即将到来时,您通常仍然不会使用它。除了可能让线程独立工作而不是 spin-waiting 或休眠。

如果线程不特别期待数据很快,它也可用,这与您的队列想法不同。所以你可以让它处理一些低优先级的事情,但是一旦更多的准备就绪,就开始作为关键路径一部分的工作。

它仍然是一个中断,因此仍然预计会有很大的开销,只是比通过内核处理信号处理程序或类似的开销要少得多。我认为像任何中断一样,它需要耗尽 out-of-order back-end。或者甚至没有那么糟糕,也许只是将其视为分支预测错误,因为它不必更改特权级别。丢弃 ROB 中的指令会降低中断延迟,但会降低吞吐量,并且只会 re-steering front-end 到 interrupt-handler 地址。


您问题中的可扩展性假设不正确

scaling for various algorithms that are bottlenecked on core-to-core bandwidth (and/or latency)?

Mesh 互连(如 Intel 自 Skylake Xeon 以来)允许内核之间相当大的 聚合 带宽。没有一条共享总线是他们都必须争夺的。即使是英特尔之前使用的环形总线 Skylake-Xeon,并且仍在客户端芯片中使用,也是流水线式的并且具有相当不错的总带宽。

数据可以同时在每对内核之间移动。 (我的意思是,128 对内核每个都可以在两个方向上传输数据。通过一些 memory-level 并行性,流水线互连可以让每个内核请求多个高速缓存行。)

这涉及共享 L3 缓存,但通常不涉及 DRAM,即使是跨套接字也是如此。 (或者在 AMD 上,核心集群在同一芯片内的 CCX 核心复合体中紧密连接)。

另请参阅一些具有良好 inter-core 延迟基准的 Anandtech 文章 (cache-line ping-pong)


GPUs also support parallel atomic update acceleration for local arrays.

我想我听说过一些 CPU(至少在理论上,也许是实践)通过将简单的 ALU 放入共享缓存来允许快速 memory_order_relaxed 原子。因此,一个核心可以向共享 L3 发送一个 atomic-increment 请求,它发生在那里的数据上,而不必暂时获得线路的独占所有权。 (旧值被 returned read-only 允许 fetch_addexchange return 值)。

这不容易订购。其他位置上的其他加载或存储由发送原子操作请求的核心完成,由缓存完成。

Anandtech 的 Graviton2 review 展示了一张幻灯片,其中提到 “动态近原子执行与远原子执行”。这可能是这个想法的一种形式!也许允许它远程执行(也许在拥有缓存行的核心?)如果内存排序要求足够弱以允许它执行此指令?这只是一个猜测,这是一个单独的问题,我不会在这里进一步深入。

(具有“大型系统扩展”的 ARMv8.1 提供 x86 风格的 single-instruction 原子 RMW,以及传统的 LL/SC,在出现虚假故障时需要重试循环,但可以像使用 CAS 重试循环一样合成任何原子 RMW 操作。)