几年前构建的编译器,比如 gcc,如何仍然可以为最近发布的处理器编译?

How does a compiler, say gcc, version built years ago can still compile for a processor released recently?

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

检查这个问题:How to see which flags -march=native will activate?;如果我这样做 gcc -march=native -E -v - </dev/null 2>&1 | grep cc1,这将为主机喷出一些标志,也就是上面的处理器 skylake。

当 4.8 在 skylake 处理器出现之前发布时,gcc 如何知道要启用哪些标志?其他较新的处理器系列呢?

因此,下一个问题是将编译器升级到它所必需的最新版本准确且最佳地为新的目标处理器编译?

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

只有当新处理器专门设计为向后兼容旧型号时才会发生这种情况。

暂时忘记 gcc。你有一个从 2000 年开始编译的 X86 二进制文件,比如说,一个为原始 Windows NT 构建的可执行文件。请问SkylakeCPU运行吗?完全正确。请问Itanium CPU 运行 iit?不,它不是为了这样做而设计的。这是一个完全不同的架构

现在可执行文件很可能不会有效地使用 Skylake,但这就是发展架构和引入新指令的全部意义所在。

回到 gcc,-march=native 并不神奇。它不可能预测出新指令和新时间。它只是选择它知道的 运行 所在的 CPU 支持的 "best" 指令集。它是如何完成的是特定于体系结构的。可以使用 CPUID 指令查询 X86 CPUs 的功能。其他架构可能会有所不同。

换句话说,-O3 -march=native 针对您编译的机器进行了优化,因此当您在构建主机上将代码编译为 运行 时,这很好。在 Nehalem 系统上使用 -march=native 构建的二进制文件与在 any 系统上使用 -march=nehalem 构建的二进制文件基本相同。 -march=native 可能会检测您的特定 L3 缓存大小,而不是使用默认值,如果任何 GCC 调整决定(如内联或展开)取决于 L3 大小。除非你 运行 一个旧的编译器在一个新的 CPU 上它无法识别,你会得到像 -mavx 这样的东西的特征检测,但只用于调整 tune=generic.

当 运行 在 Skylake 或 Ice Lake 系统上运行时,

None 可以利用 AVX2 或 BMI2 等新功能。一些在 Nehalem 上很好的特定调优决策在另一个 CPU 上可能不是最佳的。 (尽管这种可能性较小;英特尔主要保持性能和正确性的向后兼容性。让每个人都为 P4 重新编译所有内容并没有成功,因此他们通常尝试在新的 [=41] 上使现有的二进制文件 运行 很好=]s.)


一些编译器可以生成执行运行timeCPU检测和调度的二进制文件,这样他们就可以CPU 支持的任何优点,但仅限于编译器在编译时知道的扩展。函数的 AVX+FMA 机器代码版本必须存在于可执行文件中,因此在这些版本发布之前的编译器将无法创建此类机器代码。在具有这些功能的真正 CPU 可用之前,编译器开发人员还没有机会为这些功能调整代码生成,因此更新的编译器可能会为相同的 CPU 功能编写更好的代码.

旧的编译器不知道如何针对新的微体系结构进行调优。(并且通常也错过了更好的优化: gcc/clang 的新版本通常添加有助于全面的新优化,例如 gcc8 可以将多个相邻的小变量或数组元素合并 loads/stores 到单个 4 或 8 字节的加载或存储中。这有助于一切。)

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

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

Consequently, next question is upgrading the compiler to latest necessary for it accurately and optimally compile for target processor which is new?

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

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

例如Phoronix 最近发布了 GCC 5 Through GCC 10 Compiler Benchmarks - Five Years Worth Of C/C++ Compiler Performance 他们在 i7 5960X (Haswell-E) CPU 上进行了基准测试。我认为 GCC5 知道 -march=haswell。在某些基准测试中,GCC9.2 生成的代码甚至比 gcc8 快得多。

但我几乎可以保证它不是最佳!!编译器适用于大规模编译器,但如果人类知道针对给定微体系结构进行优化的底层细节,通常可以在单个热循环中找到一些东西。它与您将从任何编译器获得的一样好。 (实际上存在性能回归,所以即使这并不总是正确的。如果发现错误,请提交优化错误)。


-march=native 做两件事

  • CPU 功能检测以启用 -mfma-mbmi2 等功能。这在使用 CPUID instruction 的 x86 上很容易。 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;供应商字符串和型号/系列/步进编号唯一标识微体系结构。 ((wikipedia), sandpile, InstLatx64, https://agner.org/optimize/)

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

其他 ISA 更常用于为特定 CPU 重新编译代码的嵌入式用途。他们大多不支持 运行 时间检测。 GCC 可能必须为 SIGILL 安装处理程序,然后尝试 运行ning 一些指令。或者查询知道支持什么的 OS,例如Linux 的 /proc/cpuinfo.


脚注 1:

特别是对于 x86,它的主要声名/流行原因是严格的向后兼容性。无法 运行 某些现有程序的新 CPU 将很难销售,因此供应商不会这样做。他们甚至会竭​​尽全力超越纸质 ISA 文档,以确保现有代码继续工作。正如前英特尔架构师 Andy Glew 所说:All or almost all modern Intel processors are stricter than the manual.(对于自修改代码,一般而言)。

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

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

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

依赖 rep foo 解码为 foo 的代码可能会中断,但是:英特尔的手册非常清楚,随机前缀 可以 导致代码行为异常在未来。这使得 Intel 或 AMD 可以安全地引入新指令,这些指令在旧 CPU 上以已知方式解码,但在较新的 CPU 上执行一些新操作。喜欢 pause = rep nop。或者事务内存 HLE 在 locked 指令上使用旧 CPUs 将忽略的前缀。

VEX (AVX) 和 EVEX (AVX512) 等前缀经过精心挑选,不会与指令的有效编码重叠,尤其是在 32 位模式下。参见 How does the instruction decoder differentiate between EVEX prefix and BOUND opcode in 32-bit mode?。这是 32 位模式仍然只能使用 8 个向量寄存器 (zmm​​0..7) 的原因之一,即使 VEX 或 EVEX 在 64 位模式下分别允许 ymm0..15 或 zmm0..31。 (在 32 位模式下,VEX 前缀是某些操作码的无效编码。在 64 位模式下,该操作码首先无效,因为后面的字节更灵活。但为了简化解码器硬件,它们不是根本不同。)

2014 年的

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


一些编译器可以生成执行运行time CPU检测和调度,以便他们可以利用 CPU 支持的任何东西 ,但当然仍然只针对编译器在编译时知道的扩展。一个函数的 AVX+FMA 机器代码版本必须存在于可执行文件中,因此在这些版本发布之前的编译器将无法创建这样的机器代码。

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

GCC 通过 its ifunc mechanism 对此提供了一些支持,但 IIRC 如果不更改源代码就无法做到这一点。

Intel 的编译器 (ICC) 我认为 确实 支持在自动矢量化时对一些热门函数进行多版本控制,仅使用命令行选项。