C++ 并发编译,串行链接

C++ 并发编译,串行链接,c++,makefile,cmake,linker,C++,Makefile,Cmake,Linker,我有一个很大的cmake/c++/linux项目:很多小的静态库,一些大的相互依赖的静态库,一些大的可执行二进制文件。带有调试符号的单个二进制数为几GB。大约有10个这样的二进制文件(worker、testA、testB、testC…)。编译通常需要比我们希望的更多的时间,但我们有快速构建服务器,并且我们使用make-j20。但最糟糕的是链接。单次链接大约需要60秒和4GB内存。但是,当所有最终的二进制文件同时链接时(通常发生在1个小的子库被修改时,很少需要重新编译,很多需要重新链接),10个链

我有一个很大的cmake/c++/linux项目:很多小的静态库,一些大的相互依赖的静态库,一些大的可执行二进制文件。带有调试符号的单个二进制数为几GB。大约有10个这样的二进制文件(worker、testA、testB、testC…)。编译通常需要比我们希望的更多的时间,但我们有快速构建服务器,并且我们使用
make-j20
。但最糟糕的是链接。单次链接大约需要60秒和4GB内存。但是,当所有最终的二进制文件同时链接时(通常发生在1个小的子库被修改时,很少需要重新编译,很多需要重新链接),10个链接器使用40GB RAM(对于1个开发人员,可能会有更多)并且时间很长。IO很可能是瓶颈

我们在一个强大的服务器上有许多开发人员,每个人都使用
make-j20-l30
,这样我们就不会使CPU过载。但我们没有限制并发链接器数量的方法。在服务器上全局限制工作链接器的数量会很好,但每次make调用也会有所帮助。理想情况下,
make-j20-l30——并发链接器=2
。可能吗


我们使用黄金链接器。我们正在分离较小的独立模块,但这需要很长时间。

您可以尝试以下方法:

$ cat Makefile
OBJS := foo bar baz...
EXES := qux quux quuz...

.PHONY: all

all: $(OBJS)
    $(MAKE) -j $(concurrent-linkers) $(EXES)

$(OBJS): ...
    <compile>

$(EXES): ...
    <link>
基本上,它使用不同的
-j
选项将构建分为两个make调用,一个用于编译,一个用于链接。主要缺点是所有编译必须在第一个链接开始之前完成。更好的解决方案是设计一个简单的链接作业服务器(一个简单的shell脚本,带有一点
flock
和标记文件),并将链接作业委托给它。但是如果你能接受这个

使用虚拟Makefile进行演示:

$ cat Makefile
OBJS := a b c d e f
EXES := u v w x y z

.PHONY: all

all: $(OBJS)
    $(MAKE) -j $(concurrent-linkers) $(EXES)

$(OBJS) $(EXES):
    @printf 'building $@...\n' && sleep 2 && printf 'done\n' && touch $@
$ make -j20 -l30 concurrent-linkers=2
building a...
building d...
building b...
building c...
building e...
building f...
done
done
done
done
done
done
make -j 2 u v w x y z
make[1]: warning: -jN forced in submake: disabling jobserver mode.
make[1]: Entering directory 'foobar'
building u...
building v...
done
done
building w...
building x...
done
done
building y...
building z...
done
done
make[1]: Leaving directory 'foobar'
如您所见,所有
$(obj)
目标都是并行构建的,而
$(exe)
目标一次构建2个(最多)

编辑如果您的makefile是由CMake生成的,则至少有两个选项:

  • 调整CMake文件,使CMake生成两个不同的makefile:一个用于编译,另一个用于链接。然后编写一个简单的包装器生成文件,如:

    .PHONY: myAll myCompile
    
    myAll: myCompile
        $(MAKE) -j $(concurrent-linkers) -f Makefile.link
    
    myCompile:
        $(MAKE) -f Makefile.compilation
    
    .DEFAULT_GOAL := myAll
    
    include CMake.generated.Makefile
    
    .PHONY: myAll
    
    myAll: $(OBJS)
        $(MAKE) -j $(concurrent-linkers) $(EXES)
    
  • 说服CMake(如果还没有)生成一个makefile,该makefile定义了两个make变量:一个(
    OBJS
    )设置为所有对象文件的列表,另一个(
    EXES
    )设置为所有可执行文件的列表。然后编写一个简单的包装器生成文件,如:

    .PHONY: myAll myCompile
    
    myAll: myCompile
        $(MAKE) -j $(concurrent-linkers) -f Makefile.link
    
    myCompile:
        $(MAKE) -f Makefile.compilation
    
    .DEFAULT_GOAL := myAll
    
    include CMake.generated.Makefile
    
    .PHONY: myAll
    
    myAll: $(OBJS)
        $(MAKE) -j $(concurrent-linkers) $(EXES)
    
    如果CMake生成两个虚假目标,一个用于所有对象文件,另一个用于所有可执行文件,则存在一个非常类似的解决方案:

    .DEFAULT_GOAL := myAll
    
    include CMake.generated.Makefile
    
    .PHONY: myAll
    
    myAll: cmake-target-for-compilation
        $(MAKE) -j $(concurrent-linkers) cmake-target-for-link
    

  • 问题是
    make
    本身并不真正知道发生了什么,它所做的只是在不知道命令真正在做什么的情况下执行命令。作为一种可能的解决方法,您可以使用两个目标:一个用于编译,另一个用于链接。编译目标使用高并行化运行,而链接目标没有。我可能没有抓住要点。使用较少的链接器不会使链接更快,是吗?您将更快地获得第一个输出,但最后一个链接将开始得更晚,并与之前一样在同一时间完成(当然,假设您确实有足够的RAM)。@MSalters我们基本上有到中央服务器的瘦客户端。当链接器开始滥用服务器时,由于IO过载,任何人都无法工作。其次,我们有第二台RAM较少的服务器-它可以编译-j30没有问题,但是链接-j4会导致OOM杀手您是否仅限于
    制作
    ?CMake也可以瞄准忍者,忍者支持问题中的
    Makefile
    ,由CMake生成;这个答案假定手动生成的Makefile.MSalters是正确的-是否可以使用CMake?如果可以使用CMake生成两个Makefile,一个用于编译,一个用于链接,那么是的,这是可能的(而且很容易)。否则,您必须找到一种方法来告诉CMake您想要一个makefile,该makefile定义了一个设置为所有对象文件的make变量集和另一个设置为所有可执行文件的make变量集。然后,您只需将这个CMake生成的makefile包含在一个包装器makefile中,该包装器makefile或多或少地实现了我在示例中的建议。