如何为单个Python/C扩展源文件指定不同的编译器标志?

14 投票
3 回答
4826 浏览
提问于 2025-04-17 19:45

我有一个用Python写的扩展,它会利用一些特定于CPU的功能,如果这些功能可用的话。这是通过运行时检查来实现的。如果硬件支持POPCNT指令,它就会选择我内部循环的一种实现;如果支持SSSE3,它就会选择另一种实现;否则,它就会退回到一些通用的版本,这些版本对性能影响很大。(我大约95%以上的时间都花在这个核心部分上。)

不过,不幸的是,我遇到了一个意想不到的问题。我在编译所有C代码时使用了-mssse3-O3,尽管只有一个文件需要-mssse3这个选项。结果是,其他文件在编译时都假设SSSE3是存在的。这导致了以下这行代码出现了段错误:

start_target_popcount = (int)(query_popcount * threshold);

因为编译器使用了fisttpl,这是一个SSSE3指令。毕竟,我告诉它要假设SSSE3是存在的。

最近,我的包的Debian打包者遇到了这个问题,因为测试机器上的GCC理解-mssse3并生成相应的代码,但这台机器本身的CPU比较旧,没有这些指令。

我希望能有一个解决方案,让同一个二进制文件可以在旧机器和新机器上都能运行,这样Debian的维护者就可以在那个发行版上使用。

理想情况下,我希望只对一个文件使用-mssse3选项。因为我的CPU特定选择代码不在这个文件里,所以除非CPU支持它,否则不会执行任何SSSE3的代码。

但是,我找不到任何方法告诉distutils某些编译选项是特定于单个文件的。
这可能吗?

3 个回答

2

很遗憾,原作者的解决方案只适用于Unix编译器。这里有一个跨平台的编译器解决方案:
(MSVC不支持自动生成SSSE3代码,所以我这里用AVX作为例子)

from setuptools import setup, Extension
import distutils.ccompiler


filename = 'example_avx'

compiler_options = {
    'unix': ('-mavx',),
    'msvc': ('/arch:AVX',)
}

def spawn(self, cmd, **kwargs):
    extra_options = compiler_options.get(self.compiler_type)
    if extra_options is not None:
        # filenames are closer to the end of command line
        for argument in reversed(cmd):
            # Check if argument contains a filename. We must check for all
            # possible extensions; checking for target extension is faster.
            if not argument.endswith(self.obj_extension):
                continue

            # check for a filename only to avoid building a new string
            # with variable extension
            off_end = -len(self.obj_extension)
            off_start = -len(filename) + off_end
            if argument.endswith(filename, off_start, off_end):
                if self.compiler_type == 'bcpp':
                    # Borland accepts a source file name at the end,
                    # insert the options before it
                    cmd[-1:-1] = extra_options
                else:
                    cmd += extra_options

                # we're done, restore the original method
                self.spawn = self.__spawn

            # filename is found, no need to search any further
            break

    distutils.ccompiler.spawn(cmd, dry_run=self.dry_run, **kwargs)

distutils.ccompiler.CCompiler.__spawn = distutils.ccompiler.CCompiler.spawn
distutils.ccompiler.CCompiler.spawn = spawn


setup(
    ...
    ext_modules = [
        Extension('extension_name', ['example.c', 'example_avx.c'])
    ],
    ...
)

想了解更多关于如何在跨平台编译器中指定编译器/链接器选项的信息,可以查看我的回答

4

虽然已经过去了5年,但我找到了一种比我之前的“CC”包装器更好的解决方案。

这个“build_ext”命令会创建一个自定义的编译器实例。编译器的compile()方法会接收一个要编译的源文件列表。基础类会进行一些准备工作,然后有一个compiler._compile()的钩子,供具体的编译器子类来实现每个文件的实际编译步骤。

我觉得这个过程足够稳定,所以我可以在这个点上拦截代码。

我从distutils.command.build_ext.build_ext派生了一个新命令,稍微调整了self.compiler._compile,使得绑定的类方法被一个临时函数包裹起来,这个函数是附加在实例上的:

class build_ext_subclass(build_ext):
    def build_extensions(self):

        original__compile = self.compiler._compile
        def new__compile(obj, src, ext, cc_args, extra_postargs, pp_opts):
            if src != "src/popcount_SSSE3.c":
                extra_postargs = [s for s in extra_postargs if s != "-mssse3"]
            return original__compile(obj, src, ext, cc_args, extra_postargs, pp_opts)
        self.compiler._compile = new__compile
        try:
            build_ext.build_extensions(self)
        finally:
            del self.compiler._compile

然后我告诉setup()使用这个命令类:

setup(
   ...
   cmdclass = {"build_ext": build_ext_subclass}
)
6

一个非常糟糕的解决方案是创建两个(或者更多的 Extension)类,一个用来存放SSSE3的代码,另一个用来处理其他所有的代码。这样你就可以在Python层面上把接口整理得更整洁。

c_src = [f for f in my_files if f != 'ssse3_file.c']

c_gen = Extension('c_general', sources=c_src,
                 libraries=[], extra_compile_args=['-O3'])

c_ssse3 = Extension('c_ssse_three', sources=['ssse3_file.c'],
                 libraries=[], extra_compile_args=['-O3', '-mssse3'])

还有在某个 __init__.py 文件里

from c_general import *
from c_ssse_three import *

当然,你不需要我把代码写出来!我知道这样做不够简洁,我期待看到更好的答案!

撰写回答