By Hunter Li, July 2008 (Updated by Nikolay Molchanov, June 2016)
Modern commercial applications are usually very complicated. They might consist of a large number of source files with thousands or even millions of lines of code. The source files are typically organized and managed by the so-called project files, or make files, or build files for some specific build utilities, such as the make utility or ant utility to build applications in a managed way. Most of these utilities work in the serial mode, that is, based on the dependencies of the build targets to dispatch build jobs one after the other. For the medium-sized or more complicated applications, it can take hours up to days to fully rebuild the entire application. Fast machines seem to be the only way to accelerate the build process. The question is: Besides a fast machine, is there any other way to accelerate this process in today's networked environment? The answer is yes, and Oracle Developer Studio software could be the solution.
Oracle Developer Studio software is an integrated development environment. It contains not only compilers but a lot of useful tools, such as debugging tools, performance analysis and optimization tools, and development tools. One of these tools is dmake, which can accelerate the application build by parallelizing build activities over multiple CPUs or even multiple servers. This article uses examples to illustrate the steps to set up a distributed build environment and describes some guidelines on the makefile modification for dmake.
The full name of dmake is Distributed Make. It comes with Oracle Developer Studio software and is a superset of the standard Oracle Solaris make utility. Oracle Developer Studio software has both Oracle Solaris and Linux versions, so dmake can run on both platforms. Dmake has a unique feature that the Oracle Solaris make utility does not have: it supports building applications concurrently. Since Sun Studio 12, it has four make modes:
By dispatching build jobs to multiple CPUs or even to multiple servers, dmake parallelizes the build processes and shortens the duration. This ability differentiates dmake from other standard build utilities.
Dmake accelerates the build process by taking advantage of multiple CPUs or multiple servers to build application components concurrently. Spare CPU resources or servers are expected. To build the application properly in the concurrent mode, some makefiles might need to be slightly modified to define which parts of the application can be built simultaneously, while others can't. As such, Dmake can dispatch build jobs in the appropriate sequence. In distributed mode, source files and outcomes are transferred among the dmake host and build servers over the network. It might introduce some network load, though not much. Hence, before you decide to use dmake, you need to consider and compare the benefits of using the concurrent build versus some extra work or overhead mentioned here.
Here are some factors to consider when deciding whether to use dmake:
By default, dmake works in parallel mode. In other words, you don't have to do any extra work to enable the parallel mode. However, as noted previously, to dispatch build jobs in the appropriate sequence in terms of dependencies, you need to modify some makefiles first. Some guidelines are discussed in the next section.
To enable the distributed mode, you must configure the build environment beforehand. Here's the concept and configuration example to set up the dmake distributed mode:
root@dmake_host# share -F nfs \
-o rw=<build servers delimited by colon> \
/fullpath/to_source_root
root@build_server# mount -F nfs dmake_host:/fullpath/to_source_root \
/fullpath/to_source_root
/etc/hosts
and /etc/inet/ipnodes
should be configured properly in the dmake host and all build servers to resolve all host names.
appuser@build_server% id
uid=201(appuser) gid=1
appuser@build_server% cat $HOME/.rhosts
dmake_host appuser
appuser@dmake_host% id
uid=201(appuser) gid=1
The appuser
in the preceding code is an example user. Any subsequent occurrence of the user account described in the following sections of this article refers to this user account.
$HOME/.cshrc
can be applied when executing rsh commands, as shown here.
appuser@build_server% echo $SHELL
/bin/csh
appuser@build_server% cat $HOME/.cshrc
setenv PATH /opt/SUNWspro/bin:/usr/bin
appuser@dmake_host% rsh build_server which dmake
/opt/SUNWspro/bin/dmake
/etc/opt/SPROdmake/dmake.conf
defines at least the job capacity that the build server can accept from the dmake host. It must exist on the build server and be accessible from the user account of the build server. Without this file, no dmake job will be allowed to run on that build server. See the following example.
appuser@build_server% cat /etc/opt/SPROdmake/dmake.conf
max_jobs: 2
nice_prio: 5
The preceding example sets the maximum job capacity to two jobs for the build server, and the optional nice priority to five.
.dmakerc
, in the home directory of the build user on the dmake host. An example of .dmakerc
is shown here.
appuser@dmake_host% cat $HOME/.dmakerc
group "solaris10_x64" {
host build_server_1 { jobs = 2, path="/opt/SUNWspro/bin" }
host build_server_2 { jobs = 5, path="/export/home/SUNWspro/bin" }
host build_server_3 { jobs = 8, path="/opt/SUNWspro/bin" }
}
The example .dmakerc
file defines a build server group named solaris10_x64
that consists of three build servers. The build_server_1
can accept a maximum of two build jobs, while build_server_2
and build_server_3
can accept more jobs--five and eight, respectively. The path to the dmake binary is different on the three build servers. They are explicitly specified.
Multiple groups can exist in one .dmakerc file
. Unless explicitly specifying the group by the dmake -g
command-line option or by the DMAKE_GROUP
environment variable or macro, the first group instance is used.
.cshrc
or .profile
file on the dmake host, DMAKE_MODE
and DMAKE_ODIR. DMAKE_MODE
overrides the default parallel build mode. DMAKE_ODIR
specifies a common physical directory that dmake can write temporary output files to and read temporary output files from. By default, the directory used is $(HOME)/.dmake
, and it must be visible to all build servers. If possible, use the same source root directory as the parent directory for .dmake
because the source root directory must be shared via NFS to the network anyway, and to reduce the exposure of the irrelevant files in the user's home directory. Thus, DMAKE_ODIR
should be set to .dmake
under the source root directory on the dmake host. To further reduce the exposure of the source files and newly created files, set umask
to 0077
.
An example of the .cshrc
file is shown here.
appuser@dmake_host% cat $HOME/.cshrc
setenv PATH /opt/SUNWspro/bin:/usr/bin:/usr/sbin:/usr/ucb:.
setenv BASE_PATH /fullpath/to_source_root
setenv MAKE dmake
setenv DMAKE_MODE distributed
setenv DMAKE_ODIR /fullpath/to_source_root/.dmake
umask 0077
Thus, you don't have to specify dmake command-line options each time you type dmake
on the dmake host. And, of all build servers, only appuser
can visit the shared directory of the dmake host.
Now the distributed build environment is set.
To run dmake in the parallel mode or distributed mode, the makefile usually needs to be slightly changed to define which components can be built concurrently while others can't, in terms of dependency. Otherwise, dmake could erroneously launch both the dependency and the dependent components simultaneously or even ahead of the dependent ones. Thus, the build will fail.
Makefiles can be organized differently from application to application. It's beyond the scope of this article to give guidelines for all cases. The guidelines in the following sections might only work for the nested makes, that is, when there's one makefile per component, and a top-level makefile describes the build sequence by calling to every make for each component one after the other. The idea for the nested makes might be borrowed by other cases.
In the case of the nested makes, you might put the focus on the top-level makefile first.
.PARALLEL
to all subtarget labels to enable the full parallel build, and let dmake judge the runtime build sequence based on the dependencies.Here's a simple example that illustrates the idea:
main.mak
builds the final executable. It relies on four libraries, libmylib1.so
, libmylib2.so
, libmylib3.so
, and libmylib4.so
. The libmylib1.so
calls functions in the libmylib2.so
. The top-level makefile calls to all makes. It can be changed from this code:
appuser@dmake_host% cat Makefile
include $(BASE_PATH)/Makefile.in
all:
$(MAKE) -f $(BASE_PATH)/src/mylib2/mylib2.mak
$(MAKE) -f $(BASE_PATH)/src/mylib3/mylib3.mak
$(MAKE) -f $(BASE_PATH)/src/mylib4/mylib4.mak
$(MAKE) -f $(BASE_PATH)/src/mylib1/mylib1.mak
$(MAKE) -f $(BASE_PATH)/src/main/main.mak
clean:
$(MAKE) -f $(BASE_PATH)/src/main/main.mak clean
$(MAKE) -f $(BASE_PATH)/src/mylib1/mylib1.mak clean
$(MAKE) -f $(BASE_PATH)/src/mylib2/mylib2.mak clean
$(MAKE) -f $(BASE_PATH)/src/mylib3/mylib3.mak clean
$(MAKE) -f $(BASE_PATH)/src/mylib4/mylib4.mak clean
To this code:
appuser@dmake_host% cat Makefile
include $(BASE_PATH)/Makefile.in
TARGET= libmylib1.so \
libmylib2.so \
libmylib3.so \
libmylib4.so \
main
.PARALLEL: $(TARGET)
all: $(TARGET)
main: libmylib1.so libmylib2.so libmylib3.so libmylib4.so
$(MAKE) -f $(BASE_PATH)/src/main/main.mak
libmylib1.so: libmylib2.so
$(MAKE) -f $(BASE_PATH)/src/mylib1/mylib1.mak
libmylib2.so:
$(MAKE) -f $(BASE_PATH)/src/mylib2/mylib2.mak
libmylib3.so:
$(MAKE) -f $(BASE_PATH)/src/mylib3/mylib3.mak
libmylib4.so:
$(MAKE) -f $(BASE_PATH)/src/mylib4/mylib4.mak
clean:
$(MAKE) -f $(BASE_PATH)/src/main/main.mak clean
$(MAKE) -f $(BASE_PATH)/src/mylib1/mylib1.mak clean
$(MAKE) -f $(BASE_PATH)/src/mylib2/mylib2.mak clean
$(MAKE) -f $(BASE_PATH)/src/mylib3/mylib3.mak clean
$(MAKE) -f $(BASE_PATH)/src/mylib4/mylib4.mak clean
The makefile of a component usually does not require change because in most cases, the component makefile contains only one target. If there are multiple targets in one component makefile, it can be treated as a sub top-level makefile. Refer to the top-level makefile guidelines for customization.
However, even if the component makefile contains one target, if one build rule would generate an interim type of object that is fed as a source type of object to another rule, adjusting the build sequence with the dmake built-in special targets, including .NO_PARALLEL
, .PARALLEL
, .LOCAL
, and .WAIT
, is necessary. The idea is to have all the interim objects generated by the first rule prior to the second rule.
Here's an example:
main.c
contains a user-defined DTrace probe that is described by main_dtrace.d
. My1func.c
contains a probe described by my1func_dtrace.d
. My2func.c
contains a probe described by my2func_dtrace.d
. To build the final main executable, use cc to compile main.c
to get main.o
. Then, use dtrace to compile main_dtrace.d
with the interim main.o
to get main_dtrace.o
. Similarly, you can get my1func.o
and my1func_dtrace.o
, my2func.o
and my2func_dtrace.o
. Finally, link main.o
, main_dtrace.o
, my1func.o
, my1func_dtrace.o
, my2func.o
, and my2func_dtrace.o
together to get the final main executable. The original makefile is shown here:
appuser@dmake_host% cat main.mak
include $(BASE_PATH)/Makefile.in
DTRACE=dtrace
TARGET=$(BIN_PATH)/main
SRC_PATH=$(BASE_PATH)/src/main
OBJECT= $(SRC_PATH)/main.o \
$(SRC_PATH)/main_dtrace.o \
$(SRC_PATH)/my1func.o \
$(SRC_PATH)/my1func_dtrace.o \
$(SRC_PATH)/my2func.o \
$(SRC_PATH)/my2func_dtrace.o
all: $(TARGET)
$(TARGET): $(OBJECT)
$(CC) $(CFLAGS) -o $@ $? -ldtrace -L$(LIB_PATH) \
-lmylib1 -lmylib2 -lmylib3 -lmylib4
%_dtrace.o: %_dtrace.d
$(DTRACE) -o $@ -G -s $< $(<:%_dtrace.d=%.o)
.SUFFIXES: .c.o
.c.o:
$(CC) -o $@ -c $<
clean:
$(RM) $(TARGET) $(OBJECT)
The output of running the standard serial Solaris make is shown here:
appuser@dmake_host% make -f main.mak
cc -o /export/home/appuser/src/main/main.o -c /export/home/appuser/
src/main/main.c
dtrace -o /export/home/appuser/src/main/main_dtrace.o -G -s /export
/home/appuser/src/main/main_dtrace.d /export/home/appuser/src/main/
main.o
cc -o /export/home/appuser/src/main/my1func.o -c /export/home/appus
er/src/main/my1func.c
dtrace -o /export/home/appuser/src/main/my1func_dtrace.o -G -s /exp
ort/home/appuser/src/main/my1func_dtrace.d /export/home/appuser/src
/main/my1func.o
cc -o /export/home/appuser/src/main/my2func.o -c /export/home/appus
er/src/main/my2func.c
dtrace -o /export/home/appuser/src/main/my2func_dtrace.o -G -s /exp
ort/home/appuser/src/main/my2func_dtrace.d /export/home/appuser/src
/main/my2func.o
cc -o /export/home/appuser/bin/main /export/home/appuser/src/main/m
ain.o /export/home/appuser/src/main/main_dtrace.o /export/home/appu
ser/src/main/my1func.o /export/home/appuser/src/main/my1func_dtrace
.o /export/home/appuser/src/main/my2func.o /export/home/appuser/src
/main/my2func_dtrace.o -ldtrace -L/export/home/appuser/lib -lmylib1
-lmylib2 -lmylib3 -lmylib4
But if you run dmake directly with the distributed mode without customizing the makefile beforehand, cc and dtrace could be launched simultaneously. There's a chance that the interim object file is not ready when dtrace starts to refer to it. Thus, dtrace fails to build the relevant *_dtrace.o
. To overcome this problem, adjust the makefile to run cc to build all interim objects ahead of dtrace by reordering all interim objects ahead of all *_dtrace.o
and using .WAIT
to synchronize the process.
The new makefile is shown below. See the section in bold, which fixes the problem.
appuser@dmake_host% cat main.mak
include $(BASE_PATH)/Makefile.in
DTRACE=dtrace
TARGET=$(BIN_PATH)/main
SRC_PATH=$(BASE_PATH)/src/main
OBJECT=$(SRC_PATH)/main.o \
$(SRC_PATH)/my1func.o \
$(SRC_PATH)/my2func.o \
.WAIT \
$(SRC_PATH)/main_dtrace.o \
$(SRC_PATH)/my1func_dtrace.o \
$(SRC_PATH)/my2func_dtrace.o
all: $(TARGET)
$(TARGET): $(OBJECT)
$(CC) $(CFLAGS) -o $@ $? -ldtrace -L$(LIB_PATH) \
-lmylib1 -lmylib2 -lmylib3 -lmylib4
%_dtrace.o: %_dtrace.d
$(DTRACE) -o $@ -G -s $<$(<:%_dtrace.d=%.o)
.SUFFIXES: .c.o
.c.o:
$(CC) -o $@ -c $<
clean:
$(RM) $(TARGET) $(OBJECT)