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

Yur*_*rik 4 c x86 gcc inline-assembly compiler-optimization

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

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

phu*_*clv 6

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

但是最好利用许多现代编译器的支持。Intel的ICC早就有自动功能调度,可以为每个架构选择优化的版本。我不知道细节,但看起来它只适用于英特尔的库。此外它只发送到 Intel CPU 上的高效版本,因此对其他制造商不公平Agner 的 CPU 博客中有许多补丁和解决方法

后来在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();
}
Run Code Online (Sandbox Code Playgroud)

这会复制大量代码并且很麻烦,因此 GCC 6 添加了target_clones它告诉 GCC 将一个函数编译为多个克隆。例如__attribute__((target_clones("avx2","arch=atom","default"))) void foo() {}将创建 3 个不同的foo版本。关于它们的更多信息可以在GCC 的关于函数属性的文档中找到

该语法随后被ClangICC采纳。性能甚至可以比全局函数指针更好,因为函数符号可以在进程加载时而不是运行时解析。这是英特尔的 Clear Linux运行速度如此之快的原因之一。ICC 还可以在自动矢量化期间创建单个循环的多个版本

这是多版本控制(第二部分)中的一个例子,以及它的关于 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;
}
Run Code Online (Sandbox Code Playgroud)

请注意,PDEPPEXT是目前AMD CPU的速度很慢,使他们只能在英特尔启用