动态链接器(ld.so)中的运行时CPU检测

5
我希望将运行时CPU分派集成到我的库中。我有一些函数的几个版本,针对sse2/sse3/avx进行了优化,并有一个x87通用变体。我想将所有版本编译成单个.so库,并考虑如何实现CPU分派。
我认为最快的方式是在链接步骤(动态链接)中获取CPU分派,因此当ld.so加载我的库时,我希望它检查CPU是否支持sse2、sse3或avx,然后我希望ld.so选择正确的函数集。
例如(使用gcc目标属性):
库:
float* func3_generic(float *a, float *b)  __attribute__ ((__target__ ("fpmath=387")));
float* func3_sse2(float *a, float *b)  __attribute__ ((__target__ ("sse2")));
float* func3_sse3(float *a, float *b)  __attribute__ ((__target__ ("sse3")));
float* func3_avx(float *a, float *b)  __attribute__ ((__target__ ("avx")));

我希望有一个特殊的符号func3(),由链接器(ld.so)设置为func3_genericfunc3_sse2func3_sse3func3_avx中最先进的一个。因此,如果CPU是Core i7-xxxx,我希望每次对func3的调用都是对func3_avx的调用,如果CPU是PentiumPro,则对func3的调用将是对func3_generic的调用。
同时,我不想手动编写大量的分派代码,并且希望以最小的开销选择正确的变体(没有额外的间接跳转)。这意味着我可以在应用程序启动时承受额外的时间,但在调用此函数时不需要任何额外的东西(在某些情况下调用次数非常高)。
更新。链接器可以根据AUXV向量的AT_HWCAP:字段进行分派:
$ LD_SHOW_AUXV=1 /bin/echo
...
AT_HWCAP:    fpu ... mmx fxsr sse sse2

IBM的这篇文章介绍了针对Power平台进行优化的库,非常不错。此外,gnu_indirect_function(或STT_GNU_IFUNC)看起来也很有前途。 - osgx
1
是的,这就是STT_IFUNC的作用(顺便提一下,这似乎是第一个需要更改Linux ELF可执行文件ABI字段的扩展)。 - ninjalj
ninjalj,能否将STT_IFUNC添加为答案并简要说明它的工作原理以及为什么需要更改ABI字段? - osgx
https://www.ibm.com/developerworks/wikis/display/LinuxP/Optimized+Libraries 这个链接现在很遗憾已经无法访问了。 - Brian
3个回答

1

…使用dlopen加载多个.so文件中的一个是否可行?您可以使用各种方法查询CPU类型,然后选择适当的库来绑定func3


我只需要以不同的方式编译一些函数。而且我需要相当多的变体。 - osgx
我害怕自己去做这件事,但听起来你想要做的是在运行时更改链接器对过程链接表的解析...?在http://www.apriorit.com/our-company/dev-blog/181-elf-hook上有一个这样做的例子...但我个人会感到害怕 :-) - BRPocock
稍微不同的版本(?)该文章 http://www.codeproject.com/KB/library/elf-redirect.aspx?display=Print - BRPocock

-1
如何使用函数指针数组,然后使用CPUID指令在启动时将它们指向特定的实现。这个启动时所需的循环次数应该是微不足道的。
另一种方法,如果你真的想避免任何启动成本,就是编写另一个小程序来查询你的CPU关于其功能的信息,然后构造一个带有一些宏定义的gcc命令行,以便只编译某些函数实现。 CPUID指令

"函数指针数组"不是库的一种方法。我需要像普通方式一样导出函数。 - osgx
2
жҲ‘зӣёдҝЎ@Jamesзҡ„ж„ҸжҖқжҳҜи®©func3()зұ»жҜ”дәҺstatic (float* fnPtr)(float*,float*) = NULL; if (NULL == fnPtr) fnPtr = set_up_pointer(); return *fnPtr(a,b);...дҪҶиҝҷдјҡеҜјиҮҙдҪ жүҖжҸҗеҲ°зҡ„йўқеӨ–й—ҙжҺҘи·іиҪ¬пјҢеҜ№еҗ—пјҹ - BRPocock
static (float* fnPtr) 不兼容通常的库接口。 - osgx

-1

也许我没有理解你的推理。在我看来,有比链接器更好的地方来完成这种事情。尽管在你的情况下可能不是这样,但我认为程序通常是在一台机器上构建并在另一台或多台机器上执行的。因此,构建机器的CPU通常不重要。

如果你的目标CPU具有特定功能,那么可能会有针对该CPU的通用编译器优化,你将想要利用这些优化。因此,似乎不太理想的是,有一个应用程序模块,它被编译为所有(或一个特定的)CPU,并与特定于CPU的库链接。可以使用命令行#定义将func3重命名为适当的运行时函数。这将导致链接器生成较小的程序或引用较少的dll(如果你的库放置在那里)。

如先前建议的那样,测试也可以在运行时执行。如果func3是指向某个(通用)函数的指针,则可以根据CPUID的检查结果进行覆盖。这将导致初始化期间产生开销,并且在后续执行期间没有负面性能影响。缺点是.exe文件将变得更大,因为它需要包含所有可用的函数变体。

要执行链接,我建议你编写一个小的命令行程序,它返回一个取决于程序检测到的 CPU 的退出代码。然后在 make 文件中解释该退出代码以选择(复制)适当的库。

1
如果func3是一个指针,那么它不是库中的指针,而是函数。该库将在各种PC上使用。我应该保留库接口,所以不能使func3成为指针。此外,我谈论的不是库构建时的链接步骤(ld命令),而是运行时链接器(ld.so)。这是动态库加载的时间,就在应用程序启动时。我不想有10或20个预编译版本的库,因为库中并不是每个函数都有经过CPU调整的版本。 - osgx
如果您的应用程序涉及所有函数变体(例如在指针向量中,以便静态链接器不会将它们优化掉),则您的应用程序可以通过CPUID(例如)确定应使用哪个变体,并将对其的引用放置在指针变量中,以便func3可以访问它。或者,通过一个开关(C)使func3引用这些变体,在启动时使用CPUID确定适当的索引。 - Olof Forshell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接