如何基于 linux 上的 CPU 功能进行运行时绑定
How to do runtime binding based on CPU capabilities on linux
是否可以让 linux 库(例如 "libloader.so")加载另一个库来解析任何外部符号?
我有一大堆代码有条件地编译以支持 SIMD 级别(SSE2、AVX、AVX2)。如果构建平台与运行时平台相同,则此方法工作正常。但它阻碍了跨不同处理器代的重用。
一个想法是让 executable
调用 function
link 到不直接实现 function
的 libloader.so
。相反,它解析(绑定?)来自另一个加载库的符号,例如libimpl_sse2.so
、libimpl_avx2.so
等取决于 cpuflags。
有数百个函数需要通过这种方式动态绑定,因此更改声明或调用代码是不切实际的。
linkage 这个程序很容易改变。运行时环境变量也可以更改,但我不想更改。
我已经制作了一个可执行文件,该可执行文件通过 ld 标志 --unresolved-symbols=ignore-all
构建并以未解析的外部符号 (UES) 启动。但是随后加载 impl lib 不会将 UES 函数的值从 NULL 更改。
编辑: 后来我发现下面描述的技术只在有限的情况下有效。具体来说,您的共享库必须只包含函数,没有任何全局变量。如果您要分派到的库中有全局变量,那么您最终会遇到 运行time dynamic linker 错误。这发生 because global variables are relocated before shared library constructors are invoked。因此,linker 需要尽早解决这些引用,在此描述的调度方案有机会 运行 之前。
完成您想要的事情的一种方法是(ab)使用共享库的 ELF header 中的 DT_SONAME
字段。这可用于更改动态加载程序 (ld-linux-so*
) 在 运行 时加载的文件的名称,以解决共享库依赖性问题。这最好用一个例子来解释。假设我使用以下命令行编译共享库 libtest.so
:
g++ test.cc -shared -o libtest.so -Wl,-soname,libtest_dispatch.so
这将创建一个文件名为 libtest.so
的共享库,但其 DT_SONAME
字段设置为 libtest_dispatch.so
。让我们看看当我们 link 一个程序反对它时会发生什么:
g++ testprog.cc -o test -ltest
让我们检查生成的应用程序二进制文件的 运行time 库依赖性 test
:
> ldd test
linux-vdso.so.1 => (0x00007fffcc5fe000)
libtest_dispatch.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd1e4a55000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd1e4e4f000)
请注意,动态加载程序不是寻找 libtest.so
,而是想要加载 libtest_dispatch.so
。您可以利用它来实现您想要的调度功能。以下是我的做法:
创建共享库的各种版本。我假设有一些 "generic" 版本可以一直使用,其他优化版本在 运行 时间适当使用。我会用 "plain" 库名称 libtest.so
命名通用版本,然后根据您的选择命名其他版本(例如 libtest_sse2.so
、libtest_avx.so
等)。
当 link 使用库的通用版本时,将其 DT_SONAME
覆盖为其他内容,例如 libtest_dispatch.so
.
创建一个名为 libtest_dispatch.so
的调度程序库。当应用程序启动时加载调度程序时,它负责加载库的适当实现。下面是 libtest_dispatch.so
的实现的伪代码:
#include <dlfcn.h>
#include <stdlib.h>
// the __attribute__ ensures that this function is called when the library is loaded
__attribute__((constructor)) void init()
{
// manually load the appropriate shared library based upon what the CPU supports
// at runtime
if (avx_is_available) dlopen("libtest_avx.so", RTLD_NOW | RTLD_GLOBAL);
else if (sse2_is_available) dlopen("libtest_sse2.so", RTLD_NOW | RTLD_GLOBAL);
else dlopen("libtest.so", RTLD_NOW | RTLD_GLOBAL);
// NOTE: this is just an example; you should check the return values from
// dlopen() above and handle errors accordingly
}
当 link针对您的库进行应用程序时,link 它针对 "vanilla" libtest.so
,[=14] =] 重写以指向调度程序库。这使得调度对于使用您的库的任何应用程序作者来说基本上是透明的。
这应该像上面在 Linux 上描述的那样工作。在 Mac OS 上,共享库有一个 "install name" 类似于 ELF 共享库中使用的 DT_SONAME
,因此可以使用与上述非常相似的过程来代替。我不确定是否可以在 Windows.
上使用类似的东西
注意:上面有一个重要的假设:库的各种实现之间的 ABI 兼容性。也就是说,您的库应该设计成 link 在 link 时针对最通用的版本是安全的,而在 运行 时使用优化版本(例如 libtest_avx.so
)时间。
是否可以让 linux 库(例如 "libloader.so")加载另一个库来解析任何外部符号?
我有一大堆代码有条件地编译以支持 SIMD 级别(SSE2、AVX、AVX2)。如果构建平台与运行时平台相同,则此方法工作正常。但它阻碍了跨不同处理器代的重用。
一个想法是让 executable
调用 function
link 到不直接实现 function
的 libloader.so
。相反,它解析(绑定?)来自另一个加载库的符号,例如libimpl_sse2.so
、libimpl_avx2.so
等取决于 cpuflags。
有数百个函数需要通过这种方式动态绑定,因此更改声明或调用代码是不切实际的。 linkage 这个程序很容易改变。运行时环境变量也可以更改,但我不想更改。
我已经制作了一个可执行文件,该可执行文件通过 ld 标志 --unresolved-symbols=ignore-all
构建并以未解析的外部符号 (UES) 启动。但是随后加载 impl lib 不会将 UES 函数的值从 NULL 更改。
编辑: 后来我发现下面描述的技术只在有限的情况下有效。具体来说,您的共享库必须只包含函数,没有任何全局变量。如果您要分派到的库中有全局变量,那么您最终会遇到 运行time dynamic linker 错误。这发生 because global variables are relocated before shared library constructors are invoked。因此,linker 需要尽早解决这些引用,在此描述的调度方案有机会 运行 之前。
完成您想要的事情的一种方法是(ab)使用共享库的 ELF header 中的 DT_SONAME
字段。这可用于更改动态加载程序 (ld-linux-so*
) 在 运行 时加载的文件的名称,以解决共享库依赖性问题。这最好用一个例子来解释。假设我使用以下命令行编译共享库 libtest.so
:
g++ test.cc -shared -o libtest.so -Wl,-soname,libtest_dispatch.so
这将创建一个文件名为 libtest.so
的共享库,但其 DT_SONAME
字段设置为 libtest_dispatch.so
。让我们看看当我们 link 一个程序反对它时会发生什么:
g++ testprog.cc -o test -ltest
让我们检查生成的应用程序二进制文件的 运行time 库依赖性 test
:
> ldd test
linux-vdso.so.1 => (0x00007fffcc5fe000)
libtest_dispatch.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd1e4a55000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd1e4e4f000)
请注意,动态加载程序不是寻找 libtest.so
,而是想要加载 libtest_dispatch.so
。您可以利用它来实现您想要的调度功能。以下是我的做法:
创建共享库的各种版本。我假设有一些 "generic" 版本可以一直使用,其他优化版本在 运行 时间适当使用。我会用 "plain" 库名称
libtest.so
命名通用版本,然后根据您的选择命名其他版本(例如libtest_sse2.so
、libtest_avx.so
等)。当 link 使用库的通用版本时,将其
DT_SONAME
覆盖为其他内容,例如libtest_dispatch.so
.创建一个名为
libtest_dispatch.so
的调度程序库。当应用程序启动时加载调度程序时,它负责加载库的适当实现。下面是libtest_dispatch.so
的实现的伪代码:#include <dlfcn.h> #include <stdlib.h> // the __attribute__ ensures that this function is called when the library is loaded __attribute__((constructor)) void init() { // manually load the appropriate shared library based upon what the CPU supports // at runtime if (avx_is_available) dlopen("libtest_avx.so", RTLD_NOW | RTLD_GLOBAL); else if (sse2_is_available) dlopen("libtest_sse2.so", RTLD_NOW | RTLD_GLOBAL); else dlopen("libtest.so", RTLD_NOW | RTLD_GLOBAL); // NOTE: this is just an example; you should check the return values from // dlopen() above and handle errors accordingly }
当 link针对您的库进行应用程序时,link 它针对 "vanilla"
libtest.so
,[=14] =] 重写以指向调度程序库。这使得调度对于使用您的库的任何应用程序作者来说基本上是透明的。
这应该像上面在 Linux 上描述的那样工作。在 Mac OS 上,共享库有一个 "install name" 类似于 ELF 共享库中使用的 DT_SONAME
,因此可以使用与上述非常相似的过程来代替。我不确定是否可以在 Windows.
注意:上面有一个重要的假设:库的各种实现之间的 ABI 兼容性。也就是说,您的库应该设计成 link 在 link 时针对最通用的版本是安全的,而在 运行 时使用优化版本(例如 libtest_avx.so
)时间。