多年前构建的编译器(例如 gcc)如何仍然可以为最近发布的处理器进行编译?

the*_*ang 3 compiler-construction optimization gcc intel compiler-optimization

假设我使用编译器:gcc 4.8。还有来自英特尔的处理器,比如说 Skylake 或其他一些奇特的新系列。

检查这个问题:如何查看哪些标志 -march=native 将激活?; 如果我这样做gcc -march=native -E -v - </dev/null 2>&1 | grep cc1,这将为主机(即上述处理器skylake)喷出一些标志。

当 4.8 在 Skylake 处理器发布之前发布时,gcc 如何知道启用禁用的标志?其他较新的处理器系列呢?

因此,下一个问题是将编译器升级到最新的,以便为新的目标处理器准确和最佳地编译?

这个问题并不是真正针对 gcc/intel,我想知道其他人也如何保持处理器和编译器之间的同步。

Pet*_*des 6

旧的编译器知道如何针对新的微架构进行调优。 (并且通常也错过了更好的优化:新版本的 gcc/clang 通常会添加新的优化,有助于全面帮助,例如 gcc8 可以将多个相邻小变量或数组元素的加载/存储合并为单个 4 或 8-字节加载或存储。这对所有事情都有帮助。)

他们也只能使用他们知道的 ISA 扩展。

它们可以编写正确的代码,因为新的 x86 CPU 仍然是 x86,并且向后兼容旧 CPU 的代码1。与ARM相同。ARMv8 ISA 向后兼容 ARMv7、ARMv6 等,因此新的 ARM CPU 可以运行现有的 ARM 二进制文件。(有一些 AArch64 CPU 放弃了对 32 位模式的支持,但没关系。)

因此,下一个问题是将编译器升级到最新的,以便为新的目标处理器准确和最佳地编译?

是的,您希望您的编译器至少了解您的 CPU 以进行调整选项。

但是,是的,总是如此,即使您的 CPU 不是新的。新的编译器版本通常也会使旧的 CPU 受益,但是是的,一组用于自动矢量化的新 SIMD 扩展可能会为在一个热循环中花费大量时间的代码带来潜在的大幅加速。假设该循环自动矢量化良好。

例如,Phoronix 最近发布了GCC 5 到 GCC 10 编译器基准测试 - C/C++ 编译器性能的五年价值,他们在 i7 5960X (Haswell-E) CPU 上进行了基准测试。我认为 GCC5 知道-march=haswell. 在某些基准测试中,GCC9.2 生成的代码甚至比 gcc8 快得多。

但我几乎可以保证它不是最佳的!!编译器在大规模上表现良好,但如果人们知道优化给定微体系结构的低级细节,通常可以在单个热循环中找到一些东西。它与您从任何编译器中获得的一样好。(实际上性能回归是存在的,所以即使这样也不总是正确的。如果你发现了一个遗漏的优化错误,请提交一个)。


-march=native 做两件不同的事情

  • CPU 功能检测以启用诸如-mfma和 之类的东西-mbmi2。这在 x86 上使用CPUID 指令很容易。 GCC 将启用它所知道的实际 CPU 支持的所有扩展。例如,我认为 GCC4.8 是第一个知道任何 AVX512 扩展的 GCC,因此您甚至可能在 Ice Lake 或 Skylake-avx512 上获得一些 AVX512 自动矢量化。对于任何非平凡的事情,它是否做得好是另一回事。但是没有带有 GCC4.7 的 AVX512。
  • CPU类型检测设置-mtune=skylake 这取决于 GCC 实际上将您的特定 CPU 识别为它所知道的东西。 如果不是,则回退到-mtune=generic。它可能会检测(使用 CPUID)您的 L1/L2/L3 缓存大小并使用它来影响一些调整决策,例如内联/展开,而不是使用已知大小的-mtune=haswell. 我认为这没什么大不了的。当前的编译器没有 AFAIK 将缓存阻塞优化引入 matmul 循环或类似的东西,这就是知道缓存大小真正重要的地方。

CPU类型检测也可以在x86上使用CPUID;供应商字符串和型号/系列/步进编号唯一标识微体系结构。((维基百科)沙堆InstLatx64https: //agner.org/optimize/

x86 旨在支持在多个微体系结构上运行的单个二进制文件,并且可能需要运行时功能检测/调度。因此,一种高效/便携/可扩展的 CPU 检测机制以 CPUID 指令的形式存在,在 Pentium 和一些后期 486 CPU 中引入。(因此是 x86-64 的基线。)

其他 ISA 更常用于嵌入式用途,其中代码为特定 CPU 重新编译。他们大多没有对运行时检测的良好支持。GCC 可能需要为 SIGILL 安装一个处理程序,然后尝试运行一些指令。或者查询知道支持什么的操作系统,例如 Linux 的/proc/cpuinfo.


脚注1

特别是对于 x86,它的主要声望/流行原因是严格的向后兼容性。无法运行某些现有程序的新 CPU 将更难销售,因此供应商不会这样做。他们甚至会向后弯腰超越纸上的 ISA 文档,以确保现有代码继续工作。正如前英特尔架构师 Andy Glew 所说:所有或几乎所有现代英特尔处理器都比手册更严格。(用于自修改代码,以及一般情况下)。

当您以传统 BIOS 模式启动时,现代 PC 主板固件甚至仍然模拟 IBM PC/XT 的传统硬件,并为磁盘、键盘和屏幕访问实施软件 ABI。因此,即使引导加载程序和诸如 GRUB 之类的东西也有一个一致的向后兼容接口可供使用,然后才能加载具有实际存在的真实硬件的实际驱动程序的内核。

我认为现代 PC 仍然可以在 16 位实模式下运行真正的 MS-DOS(操作系统)二进制文件。

在不破坏向后兼容的情况下添加新指令操作码使可变长度的 x86 机器代码指令变得更加复杂,而 x86 历史上粗心/反竞争的发展也无济于事,导致例如 SSSE3 及更高版本的指令编码更加臃肿。参见 Agner Fog 的文章停止指令集战争

但是,依赖于rep foo解码的代码可能会损坏foo:英特尔的手册非常清楚,随机前缀可能会导致代码在未来出现错误行为。这使得 Intel 或 AMD 可以安全地引入在旧 CPU 上以已知方式解码的新指令,但在新 CPU 上执行新指令。喜欢pause= rep nop。或者事务性内存 HLE 在lock旧 CPU 会忽略的 ed 指令上使用前缀。

并且像 VEX (AVX) 和 EVEX (AVX512) 这样的前缀经过精心选择,不会与指令的有效编码重叠,尤其是在 32 位模式下。请参阅指令解码器如何区分 32 位模式下的 EVEX 前缀和 BOUND 操作码?. 这就是为什么 32 位模式仍然只能使用 8 个向量寄存器 (zmm​​0..7) 的原因之一,即使 VEX 或 EVEX 在 64 位模式下分别允许 ymm0..15 或 zmm0..31。(在 32 位模式下,VEX 前缀是某些操作码的无效编码。在 64 位模式下,该操作码首先无效,后面的字节更灵活。但为了简化解码器硬件,它们不是根本不同。)

2014 年的MIPS32r6 / MIPS64r6是一个向后兼容的值得注意的例子。它为保持不变的指令重新排列了一些操作码,并删除了一些指令以将其操作码重用于其他新指令,例如没有延迟槽的分支。这是非常不寻常的,并且只对用于嵌入式系统的 CPU(如当前的 MIPS)有意义。为 MIPS32r6 重新编译所有内容对于嵌入式系统来说不是问题。


一些编译器可以生成执行运行时CPU 检测和分派的二进制文件,以便它们可以利用 CPU 支持的任何内容,但当然仍然仅适用于编译器在编译时知道的扩展。函数的 AVX+FMA 机器代码版本必须存在于可执行文件中,因此在这些版本发布之前的编译器将无法创建这样的机器代码。

在具有这些功能的真正 CPU 可用之前,编译器开发人员还没有机会为这些功能调整代码生成,因此更新的编译器可能会为相同的 CPU 功能编写更好的代码。

GCC 通过它的ifunc机制对此提供了一些支持,但是 IIRC 没有源代码更改就无法做到这一点。

我认为英特尔的编译器 (ICC)在自动矢量化时确实支持多版本化一些热门功能,仅使用命令行选项。