GNU GCC 多版本函数扩展
多版本函数指的是可以为同一个函数在不同的处理器平台或者指令集下编写不同的实现程序在运行时会自动选择一个最合适的实现作为这个函数真正运行的实体。文字解释可能比较抽象我们拿具体的cpp代码举个例子#include cassert#include cstdio__attribute__ ((target (default)))int foo (){// The default version of foo.std::printf(default\n);return 0;}__attribute__ ((target (sse4.2)))int foo (){// foo version for SSE4.2std::printf(sse4.2\n);return 1;}__attribute__ ((target (archatom)))int foo (){// foo version for the Intel ATOM processorstd::printf(atom\n);return 2;}__attribute__ ((target (avx)))int foo (){// foo version for the avx.std::printf(avx\n);return 3;}int main (){int (*p)() foo;assert ((*p) () foo ()); // 注意这里指针和通过函数名应该调用相同的函数return 0;}在例子中我们编写了4个foo函数虽然它们函数体各不相同但函数签名是完全相同的——不管是标准c语言还是c这都是不允许的。但GNU扩展允许这种写法我们可以正常编译运行示例代码$ g -Wall example.cpp$ ./a.outavxavx我们为foo创建了四个不同的版本两个为SSE4.2和AVX指令集特化的版本一个为atom处理器特化的版本以及一个默认实现。前面说过当我们使用这个多版本函数时程序会在运行时动态选择一个最符合要求的版本然后让所有的操作作用到这个被选择的版本上。我的系统上支持avx指令因此运行时程序选择了avx特化版本所以取函数指针时取到的是avx特化版本通过函数名调用也会调用到avx版本。多版本函数的工作原理如果不知道工作原理很多人会以为多版本函数和条件编译是类似的东西。但两者没有什么关系多版本函数中每一个版本的代码都会被编译进程序或者库而条件编译只会把符合条件的代码编译进产物因为要编译所有版本的代码因此多版本函数的每一个实现都得是当前编译条件下合法的代码条件编译则比这灵活的多多版本函数是在运行时选择版本的有轻微性能开销条件编译通常能主动控制哪个路径上的代码被编译而多版本函数的版本选择受到一定限制尤其是编译器自动生成的。等我们了解了多版本函数的工作原理上面列出的这些区别就都能自然理解了。多版本函数利用了一个叫做IFUNC的技术它的全称是GNU indirect function。如同它的名字“间接函数”IFUNC类似普通的函数但调用它只会返回一个指向被选择的函数的指针。程序在获得其返回结果后才会调用真正要执行的函数。所以多版本函数的执行流程是这样的开发者自己手动编写或者编译器自动生成一个转发函数它会根据执行环境的硬件特性自动从多个版本中选出最符合条件的函数实现开发者或者链接器按要求把转发函数放进IFUNC对应的二进制文件的section中在编译器自动生成的情况下所有对多版本函数的操作都会重定向到IFUNC对应的符号上开发者自己编写时则需要手动处理在程序开始执行并加载动态链接库或者第一次使用多版本函数前取决于环境变量LD_BIND_NOW的值IFUNC会被调用它会返回最合适的函数实现程序会缓存这个返回结果来减轻运行时开销通常是写入PLT后续调用可以直接调用而不必走间接函数。我们以上一节的代码编译生成的程序为例看下它的符号表$ nm ./example0000000000002240 T _Z3foov0000000000002280 T _Z3foov.arch_atom00000000000022a0 T _Z3foov.avx0000000000002320 i _Z3foov.ifunc0000000000002320 W _Z3foov.resolver0000000000002260 T _Z3foov.sse4.2_Z3foo是我们的函数经过名称变换后的名字。名字后边的v.xxx则表示它是个多版本函数。默认版本的函数符号名以v结尾其他的则把target指定的内容拼接在了尾部。除了我们编写的那些还有两个符号比较特殊_Z3foov.ifunc和_Z3foov.resolver。其中_Z3foov.ifunc的类型是i这是间接函数看它的符号名也能猜到。而_Z3foov.resolver是实际的转发函数。这两个符号在二进制中的偏移量相同因此调用间接函数时会直接调用_Z3foov.resolver它的返回结果则会被程序特殊处理。上面是编译器自动生成时的例子手动编写时的情况是类似的具体可以参考glibc源码路径下的sysdeps/x86_64/multiarch/目录里的代码。这就是多版本函数的工作原理。如何编写多版本函数有两种方法编写多版本函数。第一种是类似glibc那样完全手动操作包括编写转发函数和插入符号表。第二种则是和文章开头的例子一样按编译器的规则自动生成。第一种方案需要我们自己编写转发函数并设置ifunc#include stddef.hextern void foo(unsigned *data, size_t len);void foo_c(unsigned *data, size_t len) { /* ... */ }void foo_sse42(unsigned *data, size_t len) { /* ... */ }void foo_avx2(unsigned *data, size_t len) { /* ... */ }extern int cpu_has_sse42(void);extern int cpu_has_avx2(void);void foo(unsigned *data, size_t len) __attribute__((ifunc (resolve_foo)));static void *resolve_foo(void){if (cpu_has_avx2())return foo_avx2;else if (cpu_has_sse42())return foo_sse42;elsereturn foo_c;}属性__attribute__((ifunc (resolve_foo)));指定间接函数调用的转发函数剩下的编译器会处理。resolver的函数签名必须是void *f(void)函数本身不能是weak属性的且需要和ifunc属性修饰的函数原型在同一编译单元。手动处理对于大部分开发者来说都过于繁琐因此我们更倾向于第二种编译器自动生成。想要编译器生成多版本函数在x86平台我们需要编译器属性__attribute__((target(string, ...)))和__attribute__((target_clones(string, ...)))。多版本函数的每一个版本需要有相同的函数签名然后使用上面两个属性中的一种进行修饰。__attribute__((target(string, ...)))属性可以接受多个条件并且需要每一个条件都被满足时才使用这个版本的实现。条件中可以是指令集名称或者-march命令行选项后面可以跟随的值也可以是编译器自己规定的其他值