Как указать разные флаги компилятора в distutils только для одного файла расширения Python/C?

У меня есть расширение Python, которое использует функции процессора, если таковые имеются. Это делается с помощью проверки во время выполнения. Если аппаратное обеспечение поддерживает инструкцию POPCNT, то оно выбирает одну реализацию моего внутреннего цикла, если SSSE3 доступен, то выбирает другую, в противном случае оно возвращается к универсальным версиям моего ядра, критичного к производительности. (Около 95% времени уходит на это ядро.)

К сожалению, есть режим сбоя, которого я не ожидал. Я использую -mssse3 и -O3 для компиляции всего кода на C, хотя эта опция -mssse3 требуется только для одного файла.

В результате остальные файлы компилируются с ожиданием того, что SSSE3 будет существовать. Это вызывает segfault для линии

start_target_popcount = (int)(query_popcount * threshold);

потому что компилятор использовал fisttpl, который является инструкцией SSSE-3. В конце концов, я сказал, что предполагается, что ssse3 существует.

Упаковщик Debian для моего пакета недавно столкнулся с этой проблемой, потому что на тестовой машине есть gcc, который понимает -mssse3 и генерирует код с учетом этого, но сам компьютер имеет более старый ЦП без этих инструкций.

Мне нужно решение, в котором один и тот же двоичный файл может работать на старых и новых машинах, который сопровождающий Debian может использовать для этого дистрибутива.

В идеале я хотел бы сказать, что только один файл скомпилирован с опцией -mssse3. Так как мой код селектора, специфичный для CPU, не является частью этого файла, никакой код SSSE-3 никогда не будет выполняться, если только CPU не поддерживает.

Однако я не могу придумать, как сообщить distutils, что набор параметров компилятора относится к одному файлу.

Это вообще возможно?

3 ответа

Очень уродливым решением было бы создать два (или больше 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 *

Конечно, вам не нужно, чтобы я написал этот код! И я знаю, что это не СУХОЙ, я с нетерпением жду лучшего ответа!

Прошло 5 лет, но я нашел решение, которое мне нравится больше, чем моя обертка "CC".

Команда "build_ext" создает экземпляр self.compiler. Метод compiler.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}
)

К сожалению, решение OP будет работать только для компиляторов 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'])
    ],
    ...
)

См. Мой ответ здесь, чтобы узнать о способе кросс-компилятора указать параметры компилятора / компоновщика в целом.

Другие вопросы по тегам