
促进我写这篇文章的起因是,线上机器会使用基于 SPDK 的云盘存储方案,需要编译一个 RPM 包。
这个包在 Intel 机器是运行正常的,在 AMD 的机器上无法运行报不支持 AVX512 指令。
这个问题很常见,是指令集不兼容造成的。AMD 虽然是 x86 厂商,即使是同一代其生产的 CPU 也不是和 Intel 的 CPU 是指令集完全兼容的。两家厂商的 CPU 会有一些对方不支持的指令集。
出现问题的原因也很简单,这个 RPM 包是在一台编译机器上编译的,这台编译机器是 Intel 机器,正好支持部分 AVX512 指令集。但线上的 AMD 机器是不支持 AVX512 指令集的。解决这个问题很简单,只要使用 I 家 和 A 家 这两款 CPU 共同支持的指令集即可。实际上,是不需要我们手动对比指令集的差异,寻找交集。因为 CPU 指令集之间的差异会对软件的移植性造成重大影响,所以业界早早就有了对应的指令集基线(baseline)。
指令集的演进
gcc 编译器执行 gcc -Q --help=target
,输出会包含如下部分,
Known valid arguments for -march= option: |
其中 march option 下面罗列的就是 gcc 的支持的各种指令集,重点关注其中 x86-64/x86-64-v2/x86-64-v3/x86-64-v4
这几个,这几个是 x86 平台最常用的指令集了。绝大多数软件都会使用这几个指令集进行编译,保证不同 CPU 之间的指令集兼容。
指令集从 x86-64 到 x86-64-v4 ,每一个版本都往 baseline 中加入了新的指令集,性能会更好,但移植会变差,一些比较老的 CPU 可能跑不了使用 x86-64-v4 指令集编译的软件了。
- x86-64-v2 brings support (among other things) for vector instructions up to Streaming SIMD Extensions 4.2 (SSE4.2) and Supplemental Streaming SIMD Extensions 3 (SSSE3), the POPCNT instruction (useful for data analysis and bit-fiddling in some data structures), and CMPXCHG16B (a two-word compare-and-swap instruction useful for concurrent algorithms).
- x86-64-v3 adds vector instructions up to AVX2, MOVBE (for big-endian data access), and additional bit-manipulation instructions.
- x86-64-v4 includes vector instructions from some of the AVX-512 variants.
对比 Intel 与 AMD 指令集差异
查看两款 CPU 支持的指令集之间的差异
gcc -Q -march=native --help=target | grep enabled |
当 march 为 native 可以获得当前 CPU 支持的所有指令集。
表格中是 AMD EPYC 和 Intel Ice Lake CPU 的其中一款 CPU 指令集差异图, 两家的 CPU 每一代都会有各种各样的型号,这里是其中两款的对比,
从图中看到,这个两款 CPU 都是比较新的,均支持 AVX512 系列指令集。两款 CPU 支持的指令集大部分都是重合的,一小部分是自家 CPU 独有的。如果我们使用 -march=native
在其中一款 CPU 上编译软件,计划运行在这两款 CPU 上,那么编译器使用到了任何一家的独有的指令集,软件在这两款 CPU 上就存在移植性问题了。
AMD EPYC Bergamo | 仅 AMD 支持 | 仅 Intel 支持 | Intel Ice Lake |
---|---|---|---|
-m128bit-long-double | -m128bit-long-double | ||
-m64 | -m64 | ||
-m80387 | -m80387 | ||
-mabm | -mabm | ||
-madx | -madx | ||
-maes | -maes | ||
-malign-stringops | -malign-stringops | ||
-mavx | -mavx | ||
-mavx2 | -mavx2 | ||
-mavx512bf16 | AMD ONLY | -mavx512bitalg | |
-mavx512bitalg | -mavx512bw | ||
-mavx512bw | -mavx512cd | ||
-mavx512cd | -mavx512dq | ||
-mavx512dq | -mavx512f | ||
-mavx512f | -mavx512ifma | ||
-mavx512ifma | -mavx512vbmi | ||
-mavx512vbmi | -mavx512vbmi2 | ||
-mavx512vbmi2 | -mavx512vl | ||
-mavx512vl | -mavx512vnni | ||
-mavx512vnni | -mavx512vpopcntdq | ||
-mavx512vpopcntdq | -mbmi | ||
-mbmi | -mbmi2 | ||
-mbmi2 | -mclflushopt | ||
-mclflushopt | -mclwb | ||
-mclwb | -mcrc32 | ||
-mclzero | AMD ONLY | -mcx16 | |
-mcrc32 | -mf16c | ||
-mcx16 | -mfancy-math-387 | ||
-mf16c | -mfma | ||
-mfancy-math-387 | -mfp-ret-in-387 | ||
-mfma | -mfsgsbase | ||
-mfp-ret-in-387 | -mfxsr | ||
-mfsgsbase | Intel ONLY | -mgfni | |
-mfxsr | -mglibc | ||
-mglibc | |||
-mhard-float | |||
-mhard-float | Intel ONLY | -mhle | |
-mieee-fp | -mieee-fp | ||
-mlong-double-80 | -mlong-double-80 | ||
-mlzcnt | -mlzcnt | ||
-mmmx | -mmmx | ||
-mmovbe | -mmovbe | ||
-mmwait | -mmwait | ||
-mpclmul | -mpclmul | ||
-mpopcnt | -mpopcnt | ||
-mprfchw | -mprfchw | ||
-mpush-args | -mpush-args | ||
-mrdpid | AMD ONLY | -mrdrnd | |
-mrdrnd | -mrdseed | ||
-mrdseed | -mred-zone | ||
-mred-zone | Intel ONLY | -mrtm | |
-msahf | -msahf | ||
-msha | -msha | ||
-msse | -msse | ||
-msse2 | -msse2 | ||
-msse3 | -msse3 | ||
-msse4 | -msse4 | ||
-msse4.1 | -msse4.1 | ||
-msse4.2 | -msse4.2 | ||
-msse4a | AMD ONLY | -mssse3 | |
-mssse3 | -mstv | ||
-mstv | -mtls-direct-seg-refs | ||
-mtls-direct-seg-refs | -mvaes | ||
-mvaes | -mvpclmulqdq | ||
-mvpclmulqdq | -mvzeroupper | ||
-mvzeroupper | -mwbnoinvd | ||
-mwbnoinvd | -mxsave | ||
-mxsave | -mxsavec | ||
-mxsavec | -mxsaveopt | ||
-mxsaveopt |
指令集的基线 baseline
不同 CPU 指令集之间的差异,会导致软件的移植性产生重大影响。通常的情况下,我们希望我在 x86 平台编译的软件,能在任意 x86 机器(无论 Intel 还是 AMD)上运行,而不是在每一 款 CPU 上都要重新编译。这种问题就是指令集的移植性问题。
目前已经有解决方案了,那就是 gcc 的 -march
选项,通过在编译的时候指定 -march
实现一次编译,任意运行。
gcc 编译的 -march
选项会限定是编译过程可以使用的指令集,目前这个阶段 x86-64-v2
架构是当前推荐的 -march
选项,在性能和移植性之间取得了比较好的平衡。
感兴趣的小伙伴可以在不同 CPU 平台上查询 x86-64-v2
指令集是否存在差异,结论肯定是不存在差异,因为这是一个 baseline,已经被标准化了。
gcc -Q -march=x86-64-v2 --help=target | grep enabled |
那这些指令集的 baseline 是怎么标准化的?
有些同学可能会对标准的由来感兴趣,通俗来说,是 CPU/OS/Compiler (具有影响力的头部)厂商大家一起坐下来开个会,在会上确定下一代指令集标准,也就是 baseline,确定后,软件厂商按照这个指令集标准编译软件,CPU 厂商按照这个标准设计 CPU。共同保证软件的移植性。其他非头部厂商也会跟进这个标准。
其他语言 go/java/python 编译
前面的介绍,主要是 C 语言,那其他语言比如(go/java/python/rust)编译的/运行的软件是否需要考虑指令集兼容?
理论上,所有的语言,无论是编译型还是解释性语言都会面临兼容性的问题。毕竟写的软件总要在 CPU 上跑的。。。 前面介绍 C 语言,主要是因为 C 和硬件打交道的多,对性能和可移植性都有较高的要求,其他语言更多的集中在应用层。
- python 是解释性语言,用户侧不存在指令集兼容问题。指令集差异在 cpython 那块。我们就认为不存在吧
- go 是编译型语言。用户侧存在指令集兼容问题。但是因为 go 的默认配置使用的是最原始版本的 x86-64 指令集(go env 中 GOAMD64 环境变量代表是指令集版本),所以兼容性很好。。。。我们就认为不存在吧
- java 是运行在 JVM 上的,指令集差异在 JVM 那层被屏蔽了,和 python 情况一样。我们也认为不存在
- rust 是编译型语言,存在指令集兼容问题,情况类似 go,取决于默认配置。
编译优化与可移植性的矛盾
这里有一个规律,当我们使用 native 指令集(编译器使用机器上所有可用的指令集来优化) 编译的软件性能是最好的,同时可移植性也是最差的。
还有一个规律,所有语言都存在指令集的兼容性问题,但是实际使用中,我们并不需要考虑,这是因为语言的设计者已经将指令集兼容通过各种方式屏蔽掉了,比如 go 是使用最原始的 x86-64 指令集来屏蔽,java 通过 JVM 来屏蔽等等。
实际上往往只有像 C 语言会考虑指令集的兼容性,因为为了性能,编译器应该使用所有支持的指令集进行编译期优化,但是为了可移植性,又不能使用独有指令集。这本身就是一个矛盾。举个例子,像 DPDK/SPDK 这种专门为了性能而生的软件框架,他们在编译的时候,默认采用 native 进行编译,实际上为了可移植性,我么在编译时会使用 x86-64-v2/corei7 这些指令集,牺牲一些性能,提高可移植性。
在现实场景中,如果 CPU 型号是可控的,那么在编译时候,可以激进一些。如果 CPU 型号不可控的,那么还是保守一些。