Split DWARF - Explanation
Split DWARF - Explanation
PRODUCTIVE C++
Discussing the state of the art in C++ projects
Large- and medium-sized C++ projects often suffer from long build times. We can distinguish these two scenarios:
Subscribe to Blo
Scratch build
After pulling in the latest changes from upstream, or after a major refactoring that affects central headers, a lot of Enter your email addres
source files need to be rebuilt. This can take a long time. To a large extent this is caused by the insufficient module this blog and receive not
concept of C++: Each source includes a lot of headers, so after preprocessing, there can be thousands or even posts by email.
millions of lines of C++ code the compiler has to process. Therefore, each source file will take seconds to compile, and
a large application can have thousands of source files. Compile clusters can speed things up by distributing the Name
In this article we will be discussing a great way of speeding up incremental builds which also benefits scratch makes. The
goal is to increase developer productivity: Shorter turnaround times allow for quicker iterations. You don’t have to switch Subscri
to other tasks while the build runs, but can keep your focus on the problem at hand.
RSS - Posts
Prerequisites
Your incremental build should be “minimal”: No unneeded steps should be performed when invoking the build. A
repeated build without any changes should not perform any actions at all. Sometimes this is not easy to achieve, and if Archive
the redundant actions only take a short time it is tolerable. But a large overhead here means your developers are
already wasting time. October 2018
April 2018
You should have a somewhat recent toolchain supporting split DWARF (introduced below) across the board. The
December 2017
versions below are the absolute minimum. More recent versions are recommended, especially for gdb.
October 2017
gcc >= 4.8
clang >= 3.3
September 2017
gdb >= 7.7
binutils >= 2.24 (including gold)
https://www.productive-cpp.com/improving-cpp-builds-with-split-dwarf/ 1/6
3/20/22, 8:46 PM Improving C++ Builds with Split DWARF – Productive C++
The libraries or executables containing the changed source files need to be rebuilt. This means creating them from
Home About Recommended Books Links Privacy Policy
scratch. All the contained objects need to be read again, even if unchanged, then processed by the linker, and the new
binary must be written to disk. 1
All other binaries which are depending on binaries that were rebuilt must also be relinked. Although a smarter
approach seems possible, in most build systems this means recreating these binaries from scratch.
Note that the linker also needs to process all debug information contained in the object files. Duplicate information gets
removed, and the merged debug information is written to the generated binary. It gets duplicated on disk, since it is
already contained in the object files. And debug information tends to be very large:
“ In a large C++ application compiled with -O2 and -g, the debug information accounts for 87% of the total size
of the object files sent as inputs to the link step, and 84% of the total size of the output binary.
So a large bottleneck for an incremental build is processing of debug info. Ironically, debug info is most important when
analyzing and fixing bugs, during which you are doing lots of incremental builds! For release builds without debug info,
linking can be surprisingly fast, and sometimes developers working on large projects use them as a last resort.
The binaries generated by the linker will not contain debug information, but references to the .dwo files that are already
on disk. Let’s examine how this works in detail:
main.cpp
#include <iostream>
int main()
{
int a = 1;
std::cout << "Split DWARF test" << std::endl;
return 0;
}
We compile this simple program in two ways, with and without split DWARF. First, compiling with debug information only
( -g ):
Shell
1 $ g++ -c -g main.cpp -o main.o
2 $ g++ main.o -o app
Now we also enable split DWARF by adding -gsplit-dwarf to the compiler invocation:
Shell
1 $ g++ -c -g -gsplit-dwarf main.cpp -o main_splitdwarf.o
2 $ g++ main_splitdwarf.o -o app_splitdwarf
The program is not interesting here, but let’s take a look at the files generated:
No surprises for the regular build, which produces main.o and app . The split DWARF compilation creates two files,
main_splitdwarf.o and main_splitdwarf.dwo . app_splitdwarf takes up only 12728 bytes, in contrast to app , which is
20224 bytes. The reason is that it references the debug info, instead of containing it:
Shell
1 $ readelf -wi app_splitdwarf | grep dwo
2 <20> DW_AT_GNU_dwo_name: (indirect string, offset: 0x0): main_splitdwarf.dwo
That reference is already present in the object file, so all the linker had to do with regards to debugging information is
copying that reference:
Shell
1 $ readelf -wi main_splitdwarf.o | grep dwo
2 <20> DW_AT_GNU_dwo_name: (indirect string, offset: 0x0): main_splitdwarf.dwo
3 <2c> DW_AT_GNU_dwo_id : 0xae0d75cbd6671bc1
https://www.productive-cpp.com/improving-cpp-builds-with-split-dwarf/ 2/6
3/20/22, 8:46 PM Improving C++ Builds with Split DWARF – Productive C++
Although I couldn’t get gdb to trace loading of .dwo files, you can see via strace that it pulls them in:
Shell
1 $ strace -o log gdb --batch-silent --eval-command=quit app_splitdwarf
2 $ grep dwo log
3 stat("/projects/prodcpp/splitdwarf/main_splitdwarf.dwo", {st_mode=S_IFREG|0664, st_size=12256, ...}) = 0
4 open("/projects/prodcpp/splitdwarf/main_splitdwarf.dwo", O_RDONLY|O_CLOEXEC) = 8
5 lstat("/projects/prodcpp/splitdwarf/main_splitdwarf.dwo", {st_mode=S_IFREG|0664, st_size=12256, ...}) = 0
We will be building llvm 7.0.0 with and without split DWARF. llvm in it’s latest incarnation is a rather large C++ project,
clocking in at 22838 C/C++ files. On top of that, the clang compiler is linked statically against the llvm libraries, so a lot of
work has to be redone even if only one file changes.
First, let’s do a scratch build. I’m using a clone of the git monorepo with the tag RELEASE_700/final checked out. The
root of the cmake project is in the llvm directory. To also build all the other projects, I have symlinked them to the root as
follows:
Shell
1 $ pwd
2 /h/sources/llvm-project-20170507
3 $ cd llvm/tools
4 $ ln -s ../../lld lld
5 $ ln -s ../../lldb lldb
6 $ ln -s ../../clang clang
7 $ cd ../projects
8 $ ln -s ../../compiler-rt compiler-rt
First, let’s use the defaults, which is a Debug build without split DWARF.
Shell
1 $ mkdir llvm
2 $ cd llvm
3 $ cmake /h/sources/llvm-project-20170507/llvm/
4 $ /usr/bin/time -v make -j 80
5 Percent of CPU this job got: 5072%
6 Elapsed (wall clock) time (h:mm:ss or m:ss): 13:00.54
7 ...
8 Maximum resident set size (kbytes): 11222228
9 $ du -shL llvm
10 55G
Shell
1 $ mkdir llvm_sd
2 $ cd llvm_sd
3 $ cmake /h/sources/llvm-project-20170507/llvm/ -DLLVM_USE_SPLIT_DWARF=ON
4 $ /usr/bin/time -v make -j 80
5 Percent of CPU this job got: 5939%
6 Elapsed (wall clock) time (h:mm:ss or m:ss): 11:01.42
7 ...
8 Maximum resident set size (kbytes): 4940236
9 $ du -shL .
10 36G
Elapsed time goes down from 13min 01sec to 11min 2sec (about 15%). More CPU is used by the second build on
average, which probably means that the linker completes some blocking links faster and parallelism during the build
can increase.
Maximum resident size set halves (10.7GB to 4.7GB). The linker does not have to process debug info, therefore it
needs much less memory. This is significant on constrained machines.
Disk consumption goes down by 50% (55GB to 36GB). There is no duplication of debug info in the binaries, only a
reference to the .dwo file is stored. For you as developer this means you can keep more builds around.
These improvements are nice, considering the low effort needed to obtain them. But what about an incremental build?
Let’s change one file, and then rebuild clang. First, without split DWARF:
https://www.productive-cpp.com/improving-cpp-builds-with-split-dwarf/ 3/6
3/20/22, 8:46 PM Improving C++ Builds with Split DWARF – Productive C++
Shell
Home
1 $ echo About
"//" Recommended Books Links
>>./llvm/lib/Target/X86/X86FlagsCopyLowering.cpp Privacy Policy
2 $ /usr/bin/time -v make 80 clang
3 Percent of CPU this job got: 150%
4 Elapsed (wall clock) time (h:mm:ss or m:ss): 3:16.90
5 ...
6 Maximum resident set size (kbytes): 11222232
Shell
1 $ echo "//" >>./llvm/lib/Target/X86/X86FlagsCopyLowering.cpp
2 $ /usr/bin/time -v make -j 80 clang
3 Percent of CPU this job got: 195%
4 Elapsed (wall clock) time (h:mm:ss or m:ss): 1:42.74
5 ...
6 Maximum resident set size (kbytes): 4940236
Elapsed time nearly halves from 3min 17 sec to 1min 43 sec. Resident set size shows the same behavior as above,
which makes sense considering that clang is probably the largest executable in llvm, linking in all needed llvm libraries
statically.
All in all, split DWARF is a huge win for development workflows. At the cost of adding a flag, you get significant
improvements for everybody building the code base.
Shell
1 $ dwp -e app_splitdwarf
2 $ ll app_*
3 -rwxrwxr-x 1 prodcpp prodcpp 20256 Oct 7 23:39 app*
4 -rwxrwxr-x 1 prodcpp prodcpp 12728 Oct 7 23:39 app_splitdwarf*
5 -rw-rw-r-- 1 prodcpp prodcpp 12440 Oct 7 23:41 app_splitdwarf.dwp
We now have a new file app_splitdwarf.dwp containing all debug info we need. We can now delete the .dwo file. Let’s
verify that debugging still works afterwards:
Shell
1 $ rm *dwo
2 $ gdb app_splitdwarf
3 GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
4 ...
5 Reading symbols from app_splitdwarf...done.
6 (gdb) b main
7 Breakpoint 1 at 0x40084e: file main.cpp, line 5.
8 (gdb) r
9 Starting program: app_splitdwarf
10
11 Breakpoint 1, main () at main.cpp:5
12 5 int a = 1;
13 (gdb) p a
14 $1 = 0
The variable can be printed, so debug information is available. Without the .dwp file you will get a warning as follows:
Shell
1 $ rm *dwp
2 $ gdb app_splitdwarf
3 GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
4 ...
5 Reading symbols from app_splitdwarf...
6 warning: Could not find DWO CU main_splitdwarf.dwo(0xae0d75cbd6671bc1) referenced by CU at offset 0x0 [in module app_splitdwarf
7 done.
You will get the same warning when removing the .dwo file (provided there is no .dwp file either).
When there is a .dwp file present for a shared library or executable, gdb will not look for .dwo files. When your binaries
are more recent than your .dwp file, you will not be able to debug the changed files until you remove or update the .dwp
file.
That wraps up our discussion of split DWARF. Hopefully you can make use of it in your projects and reduce your build
times!
https://www.productive-cpp.com/improving-cpp-builds-with-split-dwarf/ 4/6
3/20/22, 8:46 PM Improving C++ Builds with Split DWARF – Productive C++
Remarks
Home About Recommended Books Links Privacy Policy
Since you want to speed up your link as much as possible, you should also use the fastest linker. gold is faster than the
default ld.bfd linker, lld is even faster. lld is still under development, so may have more issues. gold is more mature.
So add -fuse-ld=linker during linking. Example:
1 -fuse-ld=gold
2 -fuse-ld=/path/to/lld
Also, you may want to use -Wl,--gdb-index . This creates the .gdb_index section in binaries, which speeds up
debugging a bit.
Limitations
Split DWARF is not used by that many projects, so some friction with tooling is possible. If you encounter any problems,
please let me know in the comments.
icecream supports split DWARF, distcc doesn’t. I have tested neither. In a build cluster you need to ship two files as a
result of the compilation. Other than that, there is only one piece of information that needs to be adjusted: The
reference to the .dwo file encoded in the object file. To make it fit to the node running the build, the compiler options -
fdebug-map-prefix ( gcc ) or -fdebug-compilation-dir ( clang ) can be used.
clang 7.0.0 recently implemented partial support for DWARF5, which does not support split DWARF yet. But it is not
the default.
Notes
1
Incremental linking is another solution to this problem, but not discussed here. In my experience it does not work as
reliably as the split DWARF approach.
References
DebugFission – DWARF Extensions for Separate Debug Information Files
DWP tool
If you found this article helpful, you can email subscribe for new articles below and vote for it on your favorite
site. Thanks!
Compilers, Toolchain Build, clang, Compiler, Faster linking, gcc, Linker, Speed up build
← Previous
Ultra-wide curved screens for increased productivity
Absolutely love this article! Implemented it in the build system at work and got these results:
https://www.productive-cpp.com/improving-cpp-builds-with-split-dwarf/ 5/6
3/20/22, 8:46 PM Improving C++ Builds with Split DWARF – Productive C++
* build folder size is reduced 25% (from 5.6 GiB to 4.2 GiB)
Home About Recommended Books Links Privacy Policy
For the incremental build a significant portion of time is now spent by CMake automoc (our software uses Qt), so if it
wasn’t for the that the improvement would be even more significant.
Reply
Reply
Martin says:
October 24, 2018 at 8:34 pm
It should also help. The effect won’t be as dramatic, but it’s worth the effort.
Reply
Leave a Reply
Copyright © 2022 Productive C++ All Rights Reserved. Privacy Policy Catch A
https://www.productive-cpp.com/improving-cpp-builds-with-split-dwarf/ 6/6