使用distutils加速构建过程

36

我正在为 Python 编写 C++ 扩展,并使用 distutils 来编译项目。随着项目的增长,重建它需要的时间越来越长。是否有方法可以加速构建过程?

我了解到并行构建(如使用 make -j)在 distutils 中不可能。是否有任何更快的替代品可供选择?

我还注意到,每次调用 python setup.py build 时,它都会重新编译所有目标文件,即使我只更改了一个源文件。这是否应该是这种情况,或者我可能做错了什么?

如果有帮助的话,这里是一些我尝试编译的文件:https://gist.github.com/2923577

谢谢!


1
你的构建过程如何?你总是清理/重建吗?你的代码(特别是头文件)怎么样?你使用前向声明吗?你的环境是什么?你能使用预编译头文件吗? - David Rodríguez - dribeas
我不知道如何回答您所有的问题,所以我添加了源代码的一部分链接。我正在使用python setup.py build构建项目,是否有更好的方法或更好的命令?环境是Linux和Mac。 - Lucas
1
重新编译所有目标文件是可以预料的:扩展额外选项可能会改变输出,而不改变 .c 文件。http://bugs.python.org/issue5372 - merwok
4个回答

39
  1. 尝试使用环境变量 CC="ccache gcc" 进行构建,当源代码未更改时,这将显著加速构建过程。(奇怪的是,distutils在编译c++源文件时也使用CC)。当然,需要安装ccache软件包。

  2. 由于您有一个单扩展,它由多个已编译的目标文件组成,您可以在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. 为了完整起见,如果你有多个扩展,可以使用以下解决方案:

  4. 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
    

1
太棒了,猴子补丁!+1。为了帮助调试,我还在imap()迭代周围添加了一个try块,以捕获CompileError,只有在终止/加入ThreadPool后才引发它们。这样,我仍然可以轻松地在输出底部看到失败的确切编译命令,同时不会留下任何孤立的进程。 - Alex Leach
1
有人在Windows上成功使用了猴子补丁吗?我尝试过了,但似乎它跳过了构建对象的步骤,直接跳到了链接步骤! - Nick
@Nick:我也在Windows下使用它,而且它很好用。你可以在self._compile之前加上一个打印语句,以查看实际要发生的情况。 - eudoxos
谢谢你的建议。这对于单个扩展和多个对象最有效。在我的情况下,我有几个扩展,每个扩展只有一个对象。最终我选择使用新的concurrent.futures.ThreadPoolExecutor,并在函数定义之外使用单个全局pool - jadelord
1
@jadelord,你可以给一个例子吗?我想看看多个扩展是如何加快编译速度的。 - Vyas
显示剩余6条评论

9

我从eudoxos的回答中得到启发,在Windows操作系统上使用clcache这个工具,现在已经实现了它的功能:

# 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:
#
# https://dev59.com/P2gu5IYBdhLWcg3w3qx7
#

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

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来加快构建速度。


1
在你提供的链接中,仅有的一些示例中似乎很明显你对语言的某些特性存在误解。例如,gsminterface.h 有许多命名空间级别的 static,这可能是无意的。每个包含该头文件的翻译单元都将编译出自己版本的在该头文件中声明的符号。这样做的副作用不仅是编译时间长,而且代码膨胀(更大的二进制文件),以及链接时间,因为链接器需要处理所有这些符号。
还有许多影响构建过程的问题您尚未回答,例如,您是否每次重新编译之前都清理一次。如果是这样,那么您可能需要考虑使用 ccache,这是一个工具,可以缓存构建过程的结果,以便如果您运行 make clean; make target,则只会为任何未更改的翻译单元运行预处理器。请注意,只要您继续在头文件中维护大部分代码,这将不会提供太多优势,因为头文件的更改会修改包含它的所有翻译单元。(我不知道您的构建系统,所以无法告诉您 python setup.py build 是否会执行 clean 操作)
这个项目似乎不是很大,所以如果编译需要超过几秒钟的时间,我会感到惊讶。

我遵循了http://docs.python.org/extending/extending.html上的教程和API文档,其中也经常使用`static`。我没有显式地清理构建(即我没有调用`python setup.py clean),似乎在重新构建之前简单地调用python setup.py build`并不会执行清理操作,尽管我不确定它到底做了什么。而且你说得对,构建只需要大约40秒。但如果只需要10秒,我会更开心。 - Lucas
1
@Lucas:这个教程可能是指在单个翻译单元中定义的函数,而不是头文件中的命名空间级别的“static”符号。头文件中的静态符号没有意义。只需声明该函数(不带“static”),并在单个翻译单元中定义它们(同样不带“static”)。这将减少重新编译的翻译单元数量,当您仅更改实现时,并且减少了头文件更改时重新编译的成本。 - David Rodríguez - dribeas
@dribeas:谢谢,我重新组织了代码,虽然它没有加速构建过程(所有翻译单元仍然会被重新编译),但我认为现在的代码更有意义了。 - Lucas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接