Ускорение процесса сборки с distutils
Я программирую расширение С++ для Python, и я использую distutils для компиляции проекта. По мере роста проекта восстановление его занимает больше времени и дольше. Есть ли способ ускорить процесс сборки?
Я читал, что параллельные сборки (как с make -j
) невозможны с distutils. Есть ли хорошие альтернативы distutils, которые могут быть быстрее?
Я также заметил, что он перекомпилирует все объектные файлы каждый раз, когда я вызываю python setup.py build
, даже когда я только изменил один исходный файл. Если это так, или я могу делать что-то неправильно здесь?
В случае, если это помогает, вот некоторые из файлов, которые я пытаюсь скомпилировать: https://gist.github.com/2923577
Спасибо!
Ответы
Ответ 1
-
Попробуйте создать переменную окружения CC="ccache gcc"
, которая значительно ускорит сборку, если исходный код не изменился. (странно, distutils использует CC
также для исходных файлов c++). Конечно, установите пакет ccache.
-
Так как у вас есть единственное расширение, которое собирается из нескольких скомпилированных объектных файлов, вы можете использовать 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
-
Для полноты, если у вас есть несколько расширений, вы можете использовать следующее решение:
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
Ответ 2
У меня это работает на Windows с clcache, полученным от eudoxos answer:
# 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://stackoverflow.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
Ответ 3
В ограниченных примерах, которые вы указали в ссылке, кажется довольно очевидным, что у вас есть некоторые недоразумения в отношении некоторых особенностей языка. Например, gsminterface.h
имеет много уровней пространства имен static
s, что, вероятно, непреднамеренно. Каждая единица перевода, которая включает этот заголовок, скомпилирует ее собственную версию для всех символов, объявленных в этом заголовке. Побочными эффектами этого являются не только время компиляции, но и разбухание кода (большие двоичные файлы) и время ссылки, поскольку компоновщик должен обрабатывать все эти символы.
Есть еще много вопросов, которые влияют на процесс сборки, на который вы не ответили, например, очищаете ли вы каждый раз, прежде чем перекомпилировать. Если вы это делаете, то вы можете рассмотреть ccache
, который является инструментом, который кэширует результат процесса сборки, так что если вы запустите make clean; make target
, будет выполняться только препроцессор для любой единицы перевода, изменилось. Обратите внимание, что до тех пор, пока вы сохраняете большинство кодов в заголовках, это не будет иметь большого преимущества, так как изменение заголовка изменяет все единицы перевода, которые его включают. (Я не знаю вашу систему сборки, поэтому я не могу сказать, будет ли python setup.py build
очищаться или нет)
Проект не выглядит большим в противном случае, поэтому я был бы удивлен, если потребовалось больше нескольких секунд для компиляции.
Ответ 4
Вы можете сделать это легко, если у вас есть 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
.