对于 SIMD 优化来说,不同平台支持不同的指令,如果使用宏定义,那么必然会导致多个发行二进制版本,可以通过如下方式实现单个二进制发行包。
简介
通常为了维护简单,希望是以单独的二进制包进行发布,但是不同平台可能会有差异性,所以需要根据平台动态选择对应实现,其中有两类方法:
- 运行时,代码中动态检测当前 CPU 支持的特性,以此选择不同的代码实现,适用于分支选择相对计算量要小很多的场景。
- 加载时,用于分支选择会影响性能的场景。
运行时
简单来说,编译时包含所有的实现,在运行时检测 CPU 支持的特性,然后根据 CPU 特性跳转到不同的函数执行。
核心是如何快速检测 CPU 特性,当前已存在 Google CPU Features 库,用来检测 CPU 相关的特性,也可以通过内部的实现,如下是两种方式。
#include <cpuid.h>
int cpu_info[4] = {-1};
__cpuid(0, cpu_info[0], cpu_info[1], cpu_info[2], cpu_info[3]);
__builtin_cpu_init();
__builtin_cpu_supports("sse");
加载时
某个函数多版本实现,在加载过程中会调用解析函数完成加载,而且在后面的程序运行过程中会一直使用对应的函数,也就是所谓的 Indirect Function 实现。
Indirect Function, IFUNC
实际上,标准库已经在使用这种方式,可以通过如下命令查看。
# nm -D /usr/lib64/libc.so.6 | grep -w memcpy
000000000008b460 i memcpy
00000000000a42c0 T memcpy
# readelf -s /usr/lib64/libc.so.6 | grep -w IFUNC
有很多的符号通过 i
标识,也就是 indirect
函数,此时提供了一个函数的多种实现,在程序加载阶段由 LD 决定具体使用那个实现,如下是一个简单示例。
void hello_world() __attribute__((ifunc("_hello_world_resolver")));
static void hello_world_sse4_2() {
std::cout << "sse4.2" << std::endl;
}
static void hello_world_default() {
std::cout << "default" << std::endl;
}
extern "C" void *_hello_world_resolver() {
__builtin_cpu_init();
if (__builtin_cpu_supports("sse4.2"))
return reinterpret_cast<void *>(&hello_world_sse4_2);
return reinterpret_cast<void *>(&hello_world_default);
}
因为此时还未执行运行态代码,只能使用内部函数调用,如上的 __builtin_cpu_supports()
函数。
注意,这种方式是有限制的:
- 仅适用于 GNU/Linux 平台。
- 对应的 Resolver 必须在当前单元内,如果是 C++ 函数则需要提供 Mangle 后的名字。
- 此时未进入运行态,如果要检测 CPU 则需要使用内部的实现。
更多细节可以参考官方 What is an indirect function 中的介绍。
Function Multiversioning
在 x86-64 上,GCC 实际上提供了更便捷的 IFUNC 实现方案。
__attribute__ ((target("sse4.2")))
static void hello_world() {
std::cout << "sse2" << std::endl;
}
__attribute__((target("default")))
static void hello_world() {
std::cout << "default" << std::endl;
}
void hello_world_wrap() {
hello_world();
}
可以通过如下命令查看。
$ nm --demangle simd | grep hello_world
另外,还可以使用如下方式指定多个目标 __attribute__((target("sse4.2,popcnt")))
。
注意,测试发现,如果 main
函数和实现不在相同文件中,那么调用会失败,需要添加一个 wrap
函数才可以,或者在改文件中直接调用。另外,当放到头文件中会导致在链接时报错,怀疑是由于自动生成 resolver 异常导致。
还可以通过 __attribute__((target_clones("default", "sse4.2")))
指定多个,详见官方 Function Multiversioning 中的介绍。
C++
FMV 只支持 GNU C11 ABI 平台使用,这就会导致 C++ 中使用成员变量函数是不支持的,例如如下方式。
#include <iostream>
class Hello {
public:
explicit Hello(std::string name) : name(std::move(name)) {};
void hello();
private:
std::string name;
};
__attribute__((target("sse4.2")))
void Hello::hello() {
std::cout << "sse4.2 " << name << std::endl;
}
__attribute__((target("default")))
void Hello::hello() {
std::cout << "default " << name << std::endl;
}
int main() {
Hello hello("andy");
hello.hello();
}
但是可以转换为下面方式。
#include <iostream>
class Hello {
public:
explicit Hello(std::string name) : name(std::move(name)) {};
void hello();
private:
std::string name;
};
__attribute__((target("sse4.2")))
static void hello_impl(const std::string &name) {
std::cout << "sse4.2 " << name << std::endl;
}
__attribute__((target("default")))
static void hello_impl(const std::string &name) {
std::cout << "default " << name << std::endl;
}
void Hello::hello() {
hello_impl(name);
}
int main() {
Hello hello("andy");
hello.hello();
}
另外,在 LLVM 7
中开始支持 target
特性,可以参考 CLang 7 Documentation 中的介绍,但是没有验证过 target_clones
特性是否支持,估计是暂不支持。
注意事项
如果编译时开启了 -Wall -Werror
参数时,那么会出现 unused function 'xxx' [-Wunused-function]
的报错,可以添加 -Wno-unused-function
或者类似如下只封装部分代码。
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-function"
// Your Function Multi-Version Here.
#pragma GCC diagnostic pop
注意,这种方式可以兼容 GCC
和 CLang
编译器。
ARM 兼容
当前只有 x86
实现了 Multi-Version,可以通过如下方式适配。注意,_Pragma
和 #pragma
的区别,前者是类似 sizeof
的函数,可以用在宏定义中。
#if defined(__GNUC__) && defined(__x86_64__)
#define MFV_IMPL(IMPL, ATTR) \
_Pragma("GCC diagnostic push") \
_Pragma("GCC diagnostic ignored \"-Wunused-function\"") \
ATTR static inline IMPL \
_Pragma("GCC diagnostic pop") \
#define MFV_SSE42(IMPL) MFV_IMPL(IMPL, __attribute__((target("sse4.2"))))
#define MFV_AVX2(IMPL) MFV_IMPL(IMPL, __attribute__((target("avx2"))))
#define MFV_AVX512(IMPL) MFV_IMPL(IMPL, __attribute__((target("avx512f,avx512bw"))))
#define MFV_DEFAULT(IMPL) MFV_IMPL(IMPL, __attribute__((target("default"))))
#else
#define MFV_SSE42(IMPL)
#define MFV_AVX2(IMPL)
#define MFV_AVX512(IMPL)
#define MFV_DEFAULT(IMPL) IMPL
#endif
然后通过如下方式调用。
MFV_SSE42(void foobar(int args) {
})
另外,还可以通过 sse2neon 适配。