管道中的软件中断会发生什么?

What happens to software interrupts in the pipeline?

读完后:

When an interrupt occurs, what happens to instructions in the pipeline?

关于软件中断会发生什么的信息不多,但我们确实了解到以下内容:

Conversely, exceptions, things like page faults, mark the instruction affected. When that instruction is about to commit, at that point all later instructions after the exception are flushed, and instruction fetch is redirected.

我想知道管道中的软件中断 (INT 0xX) 会发生什么情况,首先,它们是什么时候检测到的?它们是否可能在预解码阶段被检测到?在指令队列中?在解码阶段?还是到后台立马完成(不​​进入保留站),依次退役,退赛阶段捡到是INT指令(好像很浪费)。

假设它是在预解码时拾取的,必须有一种方法向 IFU 发出信号以停止获取指令或确实 clock/power 对其进行门控,或者如果它是在指令队列中拾取的,一种方法刷新队列中它之前的指令。然后必须有一种向某种逻辑('control unit')发送信号的方式,例如为软件中断生成微指令(索引到 IDT,检查 DPL >=CPL >= 段 RPL,等等),幼稚的建议,但如果有人对这个过程有更好的了解,那就太好了。

我也想知道当这个过程受到干扰时它是如何处理的,即发生硬件中断(记住陷阱不清除 EFLAGS 中的 IF),现在必须开始一个全新的中断处理和 uop 过程一代,它如何回到处理软件中断的状态。

Andy @Krazy Glew 的引述是关于在执行“正常”指令期间发现的同步异常,例如 mov eax, [rdi] 如果发现 RDI 指向未映射的页面,则引发 #PF。1 你希望这不会出错,所以你推迟做 任何事情 直到退休,以防它处于分支预测错误或早期异常的阴影中。


但是,是的,他的回答没有详细说明管道如何针对同步 int 陷阱指令进行优化,我们知道在解码时陷阱指令总是会导致异常。陷阱指令在整个指令组合中也很少见,因此针对它们进行优化并不能为您节省很多电量;只有容易的事才值得做。

正如 Andy 所说,当前的 CPU 不会重命名特权级别,因此无法推测到 interrupt/exception 处理程序,因此在看到 intsyscall 绝对是明智的事情。我只是打算写 int 或“陷阱指令”,但同样适用于 syscall/sysenter/sysret/iret 和其他特权-更改“分支”指令。而 1-byte versions of int 就像 int3 (0xcc) 和 int1 (0xf1)。溢出时的条件陷阱 into 很有趣;对于在无陷阱情况下的非可怕表现,可能假设不陷阱。 (当然还有 vmcall 和 VMX 扩展的东西,可能还有 SGX EENTER,可能还有其他东西。但就停止管道而言,我猜所有陷阱指令都是相同的除了条件 into)


我假设像 lfence 一样,CPU 没有推测陷阱指令。 你是对的,有在管道中使用这些 uops 是没有意义的,因为 int 之后的任何东西肯定会被刷新。

IDK if anything would fetch from the IVT (real-mode interrupt vector table) or IDT (interrupt descriptor table) to get address of a int handler before int 指令在后端变为非推测性。可能吧。 (一些陷阱指令,如 syscall,使用 MSR 来设置处理程序地址,因此从那里开始取代码可能会有用,特别是如果它提前触发 L1i 未命中。必须权衡在分支未命中后看到 int 和错误路径上的其他陷阱指令。)

错误推测命中陷阱指令的情况可能很少见,一旦前端看到陷阱指令就开始从 IDT 加载或预取 syscall 入口点是值得的,如果前端足够聪明来处理所有这些。但它可能不是。将花哨的东西留给微码对于限制前端的复杂性是有意义的。陷阱很少见,即使在 syscall 繁重的工作负载中也是如此。跨 user/kernel 障碍以更大的块进行批处理工作是一件好事,因为廉价 syscall 非常非常困难 post Spectre...


所以最迟会在 issue/rename 中检测到一个陷阱(它已经知道如何停止(部分)序列化指令),并且没有进一步的 uops将被分配到乱序后端,直到 int 被淘汰并且异常被处理。

但似乎有可能在解码中检测到它,并且不会进一步解码超过肯定会出现异常的指令。 (而且我们不知道下一步要从哪里获取。)解码器阶段确实知道如何停止,例如对于非法指令陷阱。

Let's say it is picked up at predecode

这可能不切实际,在完全解码之前您不知道它是 int。预解码只是在 Intel CPUs 上寻找指令长度。我假设 intsyscall 的操作码只是许多长度相同的操作码中的两个。

在 HW 中构建以更深入地搜索陷阱指令会花费比预解码更多的功率。 (请记住,陷阱非常罕见,尽早检测到它们大多只会节省电量,因此在将陷阱传递给解码器后停止预解码所节省的电量不能超过寻找它们所节省的电量。

您需要解码 int 以便它的微代码可以执行并让 CPU 再次启动 运行 中断处理程序,但是理论上您可以进行预解码在循环通过后停止。

例如,这是识别分支预测错过的跳转指令的常规解码器,因此主解码阶段通过不再继续处理陷阱更有意义。


超线程

当您发现停顿时,您不只是对前端进行电源门控。你让另一个逻辑线程拥有所有的周期。

超线程使得前端在没有后端帮助的情况下开始从 IDT 指向的内存中获取的价值降低。如果另一个线程没有停止,并且可以在该线程解决其陷阱时从额外的前端带宽中受益,则 CPU 正在做有用的工作。

我当然不会排除从 SYSCALL 入口点获取代码的可能性,因为该地址位于 MSR 中,并且它是在某些工作负载中与性能相关的少数几个陷阱之一。

我很好奇的另一件事是,一个逻辑核心切换特权级别对另一个核心的性能有多大影响(如果有的话)。为了对此进行测试,您将构建一些工作负载,这些工作负载会在您选择的前端问题带宽、后端端口、后端 dep 链延迟或后端在中长距离上找到 ILP 的能力方面遇到瓶颈(RS 尺寸或 ROB 尺寸)。或组合或其他东西。然后比较 cycles/iteration 的测试工作负载 运行ning 在一个核心上与它自己共享一个核心,一个紧密的 dec/jnz 线程,一个 4x pause / dec/jnz 工作负载,和一个 syscall 在 Linux 下进行 ENOSYS 系统调用的工作负载。也许还有 int 0x80 工作量来比较不同的陷阱。


脚注 1:异常处理,如正常负载上的#PF。

(题外话,回复:无辜的错误指令,而不是陷阱指令,可以在解码器中检测到引发异常)。

你等到提交(退休),因为你不想立即开始昂贵的管道刷新,只是发现这条指令处于分支未命中(或更早的错误指令)的阴影中,应该首先没有 运行(那个错误的地址)。让快速分支恢复机制捕获它。

这种等待直到退出的策略(以及危险的 L1d 缓存不会将 L1d 的加载值压缩为 0,TLB 表示它有效但没有读取权限)是导致 Meltdown 和 L1TF exploit works on some Intel CPUs. (http://blog.stuffedcow.net/2018/05/meltdown-microarchitecture/).理解 Meltdown 对理解高性能 CPUs 中的同步异常处理策略非常有帮助:标记指令并且只做任何事情 if 它到达退休是一个很好的廉价策略,因为异常非常罕见。

如果后端中的任何 uop 检测到待处理的 #PF 或其他异常。 (大概是因为这会更紧密地耦合 CPU 中原本相距很远的部分。)

并且因为在从分支未命中的快速恢复期间来自错误路径的指令可能仍在运行,并且确保您仅针对我们认为当前正确的执行路径上的预期故障停止前端需要更多的跟踪。后端中的任何 uop 曾一度被认为是在正确的路径上,但当它到达执行单元的末尾时可能不再正确。

如果您没有进行快速恢复,那么也许值得让后端发送“出现问题”信号来暂停前端,直到后端实际发生异常,或发现正确的路径。

使用 SMT(超线程),当一个线程检测到它当前正在推测导致错误的(可能正确的)路径时,这可以为其他线程留出更多的前端带宽。

所以这个想法可能有一些优点;我想知道是否有 CPU 这样做?

我同意彼得在他的回答中所说的一切。虽然它们可以通过多种方式来实现 INTn 指令,但很可能会针对 CPU 设计简单性而不是性能调整实现。可以非推测性地确定存在这样的指令的最早点是在流水线的解码阶段的末尾。可以预测获取的字节是否包含一条可能或确实引发异常的指令,但我找不到研究这个想法的研究论文,所以它似乎不值得。

INTn的执行包括从IDT中获取指定的条目,执行多次检查,计算异常处理程序的地址,然后告诉获取单元从那里开始预取。这个过程取决于处理器的运行模式(实模式、64 位模式等)。该模式由 CR0CR4Eflags 寄存器中的多个标志描述。因此,实际调用异常处理程序需要很多微指令。在Skylake中,有4个简单解码器和1个复杂解码器。简单的解码器只能发出一个融合的 uop。复数解码器最多可以发出 4 个融合微指令。其中 None 个可以处理 INTn,因此需要使用 MSROM 才能执行软件中断。请注意,INTn 指令本身可能会导致异常。此时,不知道 INTn 本身是否会将控制权更改为指定的异常处理程序(无论其地址是什么)或其他某个异常处理程序。可以肯定的是,指令流肯定会在 INTn 结束并从其他地方开始。

激活微码定序器有两种可能的方式。第一个是解码需要超过 4 微指令的宏指令时,类似于 rdtsc。第二种是在退出一条指令时,并且至少它的微指令在它的 ROB 条目中有一个有效的事件代码。根据this专利,软件中断有专门的事件代码。所以我认为 INTn 被解码成一个带有中断向量的微指令(或最多 4 微指令)。 ROB 已经需要有一个字段来保存描述相应指令是否引发异常以及异常类型的信息。同一字段可用于保存中断向量。 uop 简单地通过分配阶段并且可能不需要被调度到其中一个执行单元中,因为不需要进行任何计算。当 uop 即将退出时,ROB 确定它是 INTn 并且它应该引发一个事件(参见专利中的图 10)。此时,有两种可能的方式进行:

  • ROB 调用通用微码辅助,首先检查处理器的当前操作模式,然后选择与当前模式对应的专用辅助。
  • ROB单元本身包含检查当前运行模式并选择相应辅助的逻辑。它将辅助地址传递给负责引发事件的逻辑,后者又指示 MSROM 发出存储在该地址的辅助例程。此例程包含获取 IDT 条目并执行异常处理程序调用过程的其余部分的微指令。

协助执行过程中,可能会出现异常。这将像任何其他导致异常的指令一样处理。 ROB单元从ROB中提取异常描述并调用辅助处理。

可以用类似的方式处理无效的操作码。在 predcode 阶段,唯一重要的是正确确定无效操作码之前的指令长度。在这些有效指令之后,边界是无关紧要的。当一个简单的解码器接收到一个无效的操作码时,它会发出一个特殊的 uop,其唯一目的就是引发一个无效的操作码异常。负责在最后一个有效指令之后的指令的其他解码器都可以发出特殊的 uop。由于指令是按顺序退出的,因此可以保证第一个特殊 uop 会引发异常。当然,除非先前的 uop 引发异常或发生分支预测错误或内存排序清除事件。

当任何解码器发出该特殊 uop 时,获取和解码阶段可能会停止,直到确定宏指令异常处理程序的地址。这可能是 uop 指定的异常或其他一些异常。对于处理那个特殊 uop 的每个阶段,该阶段可以自行停止(掉电/时钟门)。这样既省电又容易实现。

或者,如果另一个逻辑核心处于活动状态,则将其视为此逻辑线程将其前端周期放弃给另一个超线程的任何其他原因。分配周期通常在超线程之间交替,但是当一个线程停止时(例如 ROB 已满或前端为空),另一个线程可以在连续的周期中分配。这也可能发生在解码器中,但也许可以用足够大的代码块来测试,以从 uop 缓存中停止它 运行。 (或者太密集而无法进入 uop 缓存)。