Ускорение процесса сборки с distutils

Я программирую расширение C++ для Python и использую distutils для компиляции проекта. По мере роста проекта его восстановление занимает все больше и больше времени. Есть ли способ ускорить процесс сборки?

Я читал, что параллельные сборки (как с make -j) не возможно с distutils. Есть ли хорошие альтернативы distutils, которые могут быть быстрее?

Я также заметил, что он перекомпилирует все объектные файлы каждый раз, когда я звоню python setup.py build, даже когда я изменил только один исходный файл. Должно ли это быть так, или я могу сделать что-то не так?

Если это поможет, вот некоторые файлы, которые я пытаюсь скомпилировать: https://gist.github.com/2923577

Спасибо!

3 ответа

Решение
  1. Попробуйте построить с переменной среды CC="ccache gcc", это значительно ускорит сборку, если источник не изменился. (как ни странно, distutils использует CC также для исходных файлов C++). Конечно, установите пакет ccache.

  2. Поскольку у вас есть единственное расширение, которое собирается из нескольких скомпилированных объектных файлов, вы можете использовать distutils для monkey-patch для компиляции их параллельно (они независимы) - поместите это в ваш setup.py (настройте N=2 как хотите):

    # monkey-patch for parallel compilation
    def parallelCCompile(self, sources, output_dir=None, macros=None, include_dirs=None, debug=0, extra_preargs=None, extra_postargs=None, depends=None):
        # those lines are copied from distutils.ccompiler.CCompiler directly
        macros, objects, extra_postargs, pp_opts, build = self._setup_compile(output_dir, macros, include_dirs, sources, depends, extra_postargs)
        cc_args = self._get_cc_args(pp_opts, debug, extra_preargs)
        # parallel code
        N=2 # number of parallel compilations
        import multiprocessing.pool
        def _single_compile(obj):
            try: src, ext = build[obj]
            except KeyError: return
            self._compile(obj, src, ext, cc_args, extra_postargs, pp_opts)
        # convert to list, imap is evaluated on-demand
        list(multiprocessing.pool.ThreadPool(N).imap(_single_compile,objects))
        return objects
    import distutils.ccompiler
    distutils.ccompiler.CCompiler.compile=parallelCCompile
    
  3. Для полноты, если у вас есть несколько расширений, вы можете использовать следующее решение:

    import os
    import multiprocessing
    try:
        from concurrent.futures import ThreadPoolExecutor as Pool
    except ImportError:
        from multiprocessing.pool import ThreadPool as LegacyPool
    
        # To ensure the with statement works. Required for some older 2.7.x releases
        class Pool(LegacyPool):
            def __enter__(self):
                return self
    
            def __exit__(self, *args):
                self.close()
                self.join()
    
    def build_extensions(self):
        """Function to monkey-patch
        distutils.command.build_ext.build_ext.build_extensions
    
        """
        self.check_extensions_list(self.extensions)
    
        try:
            num_jobs = os.cpu_count()
        except AttributeError:
            num_jobs = multiprocessing.cpu_count()
    
        with Pool(num_jobs) as pool:
            pool.map(self.build_extension, self.extensions)
    
    def compile(
        self, sources, output_dir=None, macros=None, include_dirs=None,
        debug=0, extra_preargs=None, extra_postargs=None, depends=None,
    ):
        """Function to monkey-patch distutils.ccompiler.CCompiler"""
        macros, objects, extra_postargs, pp_opts, build = self._setup_compile(
            output_dir, macros, include_dirs, sources, depends, extra_postargs
        )
        cc_args = self._get_cc_args(pp_opts, debug, extra_preargs)
    
        for obj in objects:
            try:
                src, ext = build[obj]
            except KeyError:
                continue
            self._compile(obj, src, ext, cc_args, extra_postargs, pp_opts)
    
        # Return *all* object filenames, not just the ones we just built.
        return objects
    
    
    from distutils.ccompiler import CCompiler
    from distutils.command.build_ext import build_ext
    build_ext.build_extensions = build_extensions
    CCompiler.compile = compile
    

У меня это работает на Windows с clcache, полученным из ответа eudoxos:

# Python modules
import datetime
import distutils
import distutils.ccompiler
import distutils.sysconfig
import multiprocessing
import multiprocessing.pool
import os
import sys

from distutils.core import setup
from distutils.core import Extension
from distutils.errors import CompileError
from distutils.errors import DistutilsExecError

now = datetime.datetime.now

ON_LINUX = "linux" in sys.platform

N_JOBS = 4

#------------------------------------------------------------------------------
# Enable ccache to speed up builds

if ON_LINUX:
    os.environ['CC'] = 'ccache gcc'

# Windows
else:

    # Using clcache.exe, see: https://github.com/frerich/clcache

    # Insert path to clcache.exe into the path.

    prefix = os.path.dirname(os.path.abspath(__file__))
    path = os.path.join(prefix, "bin")

    print "Adding %s to the system path." % path
    os.environ['PATH'] = '%s;%s' % (path, os.environ['PATH'])

    clcache_exe = os.path.join(path, "clcache.exe")

#------------------------------------------------------------------------------
# Parallel Compile
#
# Reference:
#
# http://stackru.com/questions/11013851/speeding-up-build-process-with-distutils
#

def linux_parallel_cpp_compile(
        self,
        sources,
        output_dir=None,
        macros=None,
        include_dirs=None,
        debug=0,
        extra_preargs=None,
        extra_postargs=None,
        depends=None):

    # Copied from distutils.ccompiler.CCompiler

    macros, objects, extra_postargs, pp_opts, build = self._setup_compile(
        output_dir, macros, include_dirs, sources, depends, extra_postargs)

    cc_args = self._get_cc_args(pp_opts, debug, extra_preargs)

    def _single_compile(obj):

        try:
            src, ext = build[obj]
        except KeyError:
            return

        self._compile(obj, src, ext, cc_args, extra_postargs, pp_opts)

    # convert to list, imap is evaluated on-demand

    list(multiprocessing.pool.ThreadPool(N_JOBS).imap(
        _single_compile, objects))

    return objects


def windows_parallel_cpp_compile(
        self,
        sources,
        output_dir=None,
        macros=None,
        include_dirs=None,
        debug=0,
        extra_preargs=None,
        extra_postargs=None,
        depends=None):

    # Copied from distutils.msvc9compiler.MSVCCompiler

    if not self.initialized:
        self.initialize()

    macros, objects, extra_postargs, pp_opts, build = self._setup_compile(
        output_dir, macros, include_dirs, sources, depends, extra_postargs)

    compile_opts = extra_preargs or []
    compile_opts.append('/c')

    if debug:
        compile_opts.extend(self.compile_options_debug)
    else:
        compile_opts.extend(self.compile_options)

    def _single_compile(obj):

        try:
            src, ext = build[obj]
        except KeyError:
            return

        input_opt = "/Tp" + src
        output_opt = "/Fo" + obj
        try:
            self.spawn(
                [clcache_exe]
                + compile_opts
                + pp_opts
                + [input_opt, output_opt]
                + extra_postargs)

        except DistutilsExecError, msg:
            raise CompileError(msg)

    # convert to list, imap is evaluated on-demand

    list(multiprocessing.pool.ThreadPool(N_JOBS).imap(
        _single_compile, objects))

    return objects

#------------------------------------------------------------------------------
# Only enable parallel compile on 2.7 Python

if sys.version_info[1] == 7:

    if ON_LINUX:
        distutils.ccompiler.CCompiler.compile = linux_parallel_cpp_compile

    else:
        import distutils.msvccompiler
        import distutils.msvc9compiler

        distutils.msvccompiler.MSVCCompiler.compile = windows_parallel_cpp_compile
        distutils.msvc9compiler.MSVCCompiler.compile = windows_parallel_cpp_compile

# ... call setup() as usual

Вы можете сделать это легко, если у вас есть Numpy 1.10. Просто добавь:

 try:
     from numpy.distutils.ccompiler import CCompiler_compile
     import distutils.ccompiler
     distutils.ccompiler.CCompiler.compile = CCompiler_compile
 except ImportError:
     print("Numpy not found, parallel compile not available")

использование -j N или установить NPY_NUM_BUILD_JOBS,

В ограниченных примерах, которые вы предоставили в ссылке, кажется довольно очевидным, что у вас есть какое-то недопонимание относительно того, каковы некоторые из особенностей языка. Например, gsminterface.h имеет много уровня пространства имен staticс, что, вероятно, непреднамеренно. Каждый модуль перевода, который включает этот заголовок, будет компилировать свою собственную версию для каждого из символов, объявленных в этом заголовке. Побочными эффектами этого являются не только время компиляции, но также раздувание кода (большие двоичные файлы) и время компоновки, поскольку компоновщик должен обрабатывать все эти символы.

Есть еще много вопросов, которые влияют на процесс сборки, на которые вы не ответили, например, очищаете ли вы каждый раз перед перекомпиляцией. Если вы делаете это, то вы можете рассмотреть ccache, который является инструментом, который кэширует результат процесса сборки, так что если вы запустите make clean; make target только препроцессор будет запущен для любого модуля перевода, который не изменился. Обратите внимание, что, пока вы сохраняете большую часть кода в заголовках, это не даст большого преимущества, так как изменение заголовка изменяет все блоки перевода, которые его включают. (Я не знаю вашу систему сборки, поэтому не могу сказать, python setup.py build будет чистить или нет)

В противном случае проект не кажется большим, поэтому я был бы удивлен, если бы компиляция заняла более нескольких секунд.

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