使用更新的 CPU 指令支持构建向后兼容的二进制文件

Posted

技术标签:

【中文标题】使用更新的 CPU 指令支持构建向后兼容的二进制文件【英文标题】:Building backward compatible binaries with newer CPU instructions support 【发布时间】:2020-04-03 05:15:09 【问题描述】:

如果可用(在运行时测试),实现使用特定 CPU 指令(在运行时测试)的同一函数的多个版本的最佳方法是什么,或者如果没有,则回退到较慢的实现?

例如,x86 BMI2 提供了一个非常有用的PDEP 指令。我将如何编写一个 C 代码,以便它在启动时测试执行 CPU 的 BMI2 可用性,并使用两种实现之一——一种使用_pdep_u64 调用(可与-mbmi2 一起使用),另一种进行位操作使用 C 代码“手动”。是否有对此类情况的内置支持?在提供对较新内在函数的访问权限的同时,如何使 GCC 为较旧的架构进行编译?我怀疑如果函数是通过全局函数指针调用的,而不是每次都使用 if/else,执行速度会更快?

【问题讨论】:

查看操作系统内核是如何做到这一点的(提示:对于 x86,它是基于 cpuid 指令返回的大量特性)。 【参考方案1】:

您可以通过调用cpuid来声明一个函数指针,并在程序启动时将其指向正确的版本以确定当前架构

但最好利用许多现代编译器的支持。 Intel的ICC早就有automatic function dispatching为每个架构选择优化版本。我不知道细节,但看起来它只适用于英特尔的图书馆。此外,它仅调度到 Intel CPU 上的高效版本,因此将是 unfair to other manufacturers。 Agner`s CPU blog 中有很多补丁和解决方法

后来在GCC 4.8 中引入了一个名为Function Multiversioning 的功能。它添加了target 属性,您将在函数的每个版本中声明该属性

__attribute__ ((target ("sse4.2")))
int foo()  return 1; 

__attribute__ ((target ("arch=atom")))
int foo()  return 2; 

int main() 
    int (*p)() = &foo;
    return foo() + p();

这会重复很多代码并且很麻烦,因此 GCC 6 添加了target_clones,告诉 GCC 将一个函数编译为多个克隆。例如 __attribute__((target_clones("avx2","arch=atom","default"))) void foo() 将创建 3 个不同的 foo 版本。有关它们的更多信息,请访问GCC's documentation about function attribute

该语法随后被Clang 和ICC 采用。性能甚至可以优于全局函数指针,因为 function symbols can be resolved at process loading time 而不是运行时。这是Intel's Clear Linux 运行so fast 的原因之一。 ICC 也可以在自动矢量化期间创建multiple versions of a single loop

Function multi-versioning in GCC 6 Function Multi-Versioning The - surprisingly limited - usefulness of function multiversioning in GCC Generate code for multiple SIMD architectures

这是来自The one with multi-versioning (Part II) 的示例以及它的demo,它是关于 popcnt 但你明白了

__attribute__((target_clones("popcnt","default")))
int runPopcount64_builtin_multiarch_loop(const uint8_t* bitfield, int64_t size, int repeat) 
    int res = 0;
    const uint64_t* data = (const uint64_t*)bitfield;

    for (int r=0; r<repeat; r++)
    for (int i=0; i<size/8; i++) 
        res += popcount64_builtin_multiarch_loop(data[i]);
    

    return res;

请注意PDEP and PEXT are very slow on current AMD CPUs,因此它们只能在 Intel 上启用

【讨论】:

popcount64_builtin_multiarch_loop 是围绕__builtin_popcountll 的包装器的一个非常奇怪的名称,因为它只是一个包装器,不包含循环。也不需要包装函数,它仍然可以直接在循环内编译 __builtin_popcountll。 (您可能希望将其包装到非 GNU 编译器或任何其他用例的可移植性,例如使用 std::bitset&lt;64&gt;::count 。但这对于已经仅适用于 GNU C / C++ 的示例来说是多余的,即使用 gcc 或 clang。并使用 ICC 编译,但不使用 popcnt 制作版本。) 我也不知道为什么会这样包装。可能作者认为移植到其他编译器更容易? ICC:godbolt.org/z/d9BoHF。哦,clang 也没有多版本,但它确实使用 SSE2 自动矢量化。与 popcnt 相比可能没有利润,但可能与其标量后备 bithack 相比。无论如何,clang10.0 说 unknown attribute 'target_clones',所以对于具有紧凑语法的 clang 来说还不是。 在第一个示例中,如果我只是在不匹配任一属性的平台上调用foo() 会发生什么? @Yurik 我没有检查过,但通常你必须提供一个“默认”版本

以上是关于使用更新的 CPU 指令支持构建向后兼容的二进制文件的主要内容,如果未能解决你的问题,请参考以下文章

jQuery 对向后兼容性的支持如何?

谷歌发布第二代机器学习框架:TensorFlow 1.5.0-rc0

向后iOS兼容性 - Swift 2

Java 7 FileSystemProvider向后兼容

Delphi 区域编译器指令 - 向后兼容的单元文件?

CUDA 11.2 是不是支持向后兼容在 CUDA 10.2 上编译的应用程序?