是的。
几十年前,Lisp machines 在假设程序和状态有效的情况下运行程序时执行同时验证检查(例如类型检查和边界检查),如果检查失败,则“及时”跳转 - 不幸的是,这种能力当传统(即 x86)机器占据主导地位时,获取“免费”运行时验证会丢失。
https://en.wikipedia.org/wiki/Lisp_machine
Lisp Machines 与更传统的单指令添加并行运行测试。如果同时测试失败,则丢弃并重新计算结果;在许多情况下,这意味着速度会增加几个因素。 这种同时检查方法也用于测试引用时数组的边界,以及其他内存管理必需品(不仅仅是垃圾收集或数组)。
幸运的是,我们终于从过去中慢慢吸取了教训,逐渐地、零碎地重新引入这些创新 - 在 Skylake 一代处理器中引入了用于 x86 的 Intel's "MPX" (Memory Protection eXtensions) 用于硬件边界检查 - 尽管它并不完美。
(x86 在其他方面也是一种回归:IBM 的大型机在 1980 年代拥有真正的硬件加速系统虚拟化 - 我们直到 2005 年才在 x86 上使用英特尔的“VT-x”和 AMD 的“AMD-V”扩展名)。
x86 BOUND
从技术上讲,x86 确实具有硬件边界检查:the BOUND instruction 于 1982 年在 Intel 80188 中引入(以及 Intel 286 及更高版本,但不是 Intel 8086、8088或 80186 处理器)。
虽然BOUND 指令确实提供了硬件边界检查,但我理解它间接导致了性能问题,因为它破坏了硬件分支预测器(according to a Reddit thread,但我不确定为什么),而且还因为它需要边界在元组中指定 在内存中 - 这对性能来说很糟糕 - 我知道在运行时它并不比手动执行指令更快“如果index不在[x,y]范围内然后发出信号BR 程序或操作系统的异常”(因此您可能会想象添加了 BOUND 指令是为了方便手工编写汇编代码的人,这在 1980 年代很常见)。
BOUND 指令仍然存在于当今的处理器中,但它并未包含在 AMD64 (x64) 中 - 可能是出于我上面解释的性能原因,也可能是因为可能很少有人使用它(编译器可能微不足道用手动边界检查替换它,这可能有更好的性能,因为它可以使用寄存器)。
将数组边界存储在内存中的另一个缺点是其他地方的代码(不受BOUNDS检查的影响)可能会覆盖先前为另一个指针编写的边界并以这种方式规避检查 - 这主要是一个问题有意尝试禁用安全功能(即恶意软件)的代码,但如果边界存储在堆栈中 - 并且考虑到破坏堆栈是多么容易,它的实用性就更小了。
英特尔 MPX
英特尔 MPX 于 2015 年在 Skylake 架构中引入,并且应该出现在主流英特尔酷睿系列中的所有 Skylake 和后续处理器型号中(包括 Xeon,以及赛扬和奔腾的非 SoC 版本)。从 2016 年起,英特尔还在 Goldmont 架构(Atom 以及赛扬和奔腾的 SoC 版本)中实现了 MPX。
MPX 优于 BOUND,因为它提供专用寄存器来存储边界范围,因此与需要内存访问的 BOUND 相比,边界检查应该几乎零成本。在 Intel 486 上,BOUND 指令 takes 7 cycles(与 CMP 相比,即使操作数是内存地址也需要 only 2 cycles)。在 Skylake 中,MPX 等效项(BNDMK、BNDCL 和 BNDCU)都是 1 周期指令,BNDMK 可以摊销,因为它只需要为每个新指针调用一次)。
我找不到任何关于 AMD 是否在哪里实施了自己的 MPX 版本的信息(截至 2017 年 6 月)。
对 MPX 的批判性思考
不幸的是,MPX 的当前状态并不那么乐观 - a recent paper by Oleksenko, Kuvaiskii, et al. in February 2017 "Intel MPX Explained"(PDF link:警告:尚未经过同行评审)有点关键:
我们的主要结论是,英特尔 MPX 是一种很有前途的技术,但还不能广泛采用。英特尔 MPX 的性能开销仍然很高(平均约为 50%),并且支持的基础架构存在可能导致编译或运行时错误的错误。此外,我们展示了英特尔 MPX 的设计局限性:它无法检测时间错误,在多线程代码中可能存在误报和误报,以及它的局限性
在内存布局上需要对某些程序进行大量代码更改。
还请注意,与以前的 Lisp 机器相比,英特尔 MPX 仍然是内联执行的 - 而在 Lisp 机器中(如果我的理解是正确的)边界检查在硬件中同时发生,并具有向后追溯跳转如果检查失败;因此,只要运行程序的指针不指向越界位置,那么运行时性能成本绝对为零,所以如果你有这个 C 代码:
char arr[10];
arr[9] = 'a';
arr[8] = 'b';
然后在 MPX 下会执行:
Time Instruction Notes
1 BNDMK arr, arr+9 Set bounds 0 to 9.
2 BNDCL arr Check `arr` meets lower-bound.
3 BNDCU arr Check `arr` meets upper-bound.
4 MOV 'a' arr+9 Assign 'a' to arr+9.
5 MOV 'a' arr+8 Assign 'a' to arr+8.
但是在 Lisp 机器上(如果可以神奇地将 C 编译成 Lisp...),那么计算机中的程序-阅读器-硬件能够与“实际”指令同时执行附加的“辅助”指令指令,允许“侧面”指令指示计算机在发生错误时忽略“实际”指令的结果:
Time Actual instruction Side instruction
1 MOV 'A' arr+9 ENSURE arr+9 BETWEEN arr, arr+9
2 MOV 'A' arr+8 ENSURE arr+8 BETWEEN arr, arr+9
我了解“侧面”指令的每周期指令与“实际”指令不同 - 因此,Time=1 指令的侧面检查可能仅在“实际”指令完成后完成已经进展到Time=3 - 但如果检查失败,那么它将失败指令的指令指针传递给异常处理程序,该处理程序将指示程序忽略在Time=1 之后执行的指令的结果。我不知道他们如何在没有大量内存或一些强制执行暂停的情况下实现这一点,也可能是内存围栏 -
这超出了我的回答范围,但至少在理论上是可能的。
(请注意,在这个人为的示例中,我使用了constexpr 索引值,编译器可以证明它永远不会越界,因此会完全忽略 MPX 检查 - 所以假装它们是用户提供的变量: ) )。
我不是 x86 方面的专家(或者在微处理器设计方面有任何经验,省去我在 UW 上的 CS500 级课程并且没有为...做功课)但我不相信并发执行尽管存在乱序执行的现有实现,但 x86 的当前设计不可能进行边界检查或“时间旅行” - 但是我可能错了。我推测如果所有指针类型都被提升为 3 元组(struct BoundedPointer<T> { T* ptr, T* min, T* max } - 从技术上讲,这已经发生在 MPX 和其他基于软件的边界检查中,因为每个受保护的指针在调用 BNDMK 时都定义了其边界)然后MMU 可以免费提供保护 - 但现在每个指针将消耗 24 个字节的内存,而不是当前的 8 个字节 - 或者与 32 位 x86 下的微不足道的 4 个字节相比 - RAM 很丰富,但仍然是有限的不应该浪费的资源。
GCC 中的 MPX
在 MPX 版本 5.0 到 9.1 (https://gcc.gnu.org/wiki/Intel%20MPX%20support%20in%20the%20GCC%20compiler) 由于其 maintenance burden 而被删除时,GCC 支持它。
Visual Studio / Visual C++ 中的 MPX
Visual Studio 2015 Update 1 (2015.1) 使用 /d2MPX 开关 (https://blogs.msdn.microsoft.com/vcblog/2016/01/20/visual-studio-2015-update-1-new-experimental-feature-mpx/) 添加了对 MPX 的“实验性”支持。 Visual Studio 2017 中仍提供支持,但微软尚未宣布它是否被视为主流(即非实验性)功能。
Clang / LLVM 中的 MPX
Clang 过去曾部分支持手动使用 MPX,但在 10.0 版中完全删除了该支持
截至 2021 年 7 月,LLVM 似乎仍然能够输出 MPX 指令,但我看不到任何 MPX“通过”的证据。
英特尔 C/C++ 编译器中的 MPX
英特尔 C/C++ 编译器从 15.0 版开始支持 MPX。