Preparing your code for debugging
In broad terms when building software you typically have the option between generating either a release or a debug build.
This also applies to ROOT and your compiled analysis tasks and macros. As the name suggests:
- release builds are meant for production and should be used when running your code at scale;
- debug builds should be used exclusively when trying to sort out problems in the code.
There are two main correlated characteristics that differ between release and debug builds:
- compile-time optimization
- debug symbols
Compile-time optimization
Every compiler performs some form of optimization while producing executables and libraries. Optimizing a code while compiling means that the compiler does not merely translate a written programming language into machine code, but it also tries to interpret up to a certain extent what the code aims to do.
All levels of optimization have the result of avoiding unnecessary code to be executed, and since optimization requires some code analysis, compilation time is longer (even if this is mostly unnoticeable with modern computers).
Different levels of optimization can be choosen, ranging from no optimization
to aggressive optimization; the most common compilers (for instance, clang
or gcc
) you would use the following switches:
[-O0|-O1|-O2|-O3|-Ofast|-Os|-Oz|-O|-O4]
The default optimization level for the aforementioned compilers is -O2
:
higher levels of optimization are typically not recommended, as they might alter
the program flow to a point it does no longer match any longer the developer's
intentions.
A list of what each -O
flag enables in GCC is available on the
GCC documentation.
When debugging code, you want binary instructions generated by the compiler to match as close as possible the readable code's flow. This is one of the reasons why optimization should be turned off in such a case.
Increasing the optimization does not necessarily lead to more efficient code! "Extreme" optimizations (like
-O4
) are not recommended and can even lead to unexpected results.
Symbols and debug symbols
Shared libraries and executables contain binary code divided into "sections", and "symbols" are a way of referring to such sections. Each function or method name of your code is translated into a symbol.
Extra "symbols" are written to a library when compiling in debug mode, pointing directly fragments of functions to the corresponding lines in the source code.
To compile with clang
or gcc
with debug symbols, you would use the -g
switch: there is no guarantee that optimizations are also turned off, so you'd
better specify also -O0
:
gcc -O0 -g ...
The same flags are used for clang
.
Compiling your ALICE code for debug
Complex frameworks aren't typically built by directly invoking a compiler and its options: for instance, when compiling AliPhysics you use aliBuild, managing dependencies between different packages, and CMake, a tool that generates compile commands based on the dependencies between source files inside the same project.
A standard aliBuild compilation of AliPhysics, which is as simple as issuing:
aliBuild build AliPhysics
will use some "defaults" which, if no extra option is passed, are read from the
defaults-release.sh
file. Options
in this file are such that we compile our three main packages (ROOT, AliRoot and
AliPhysics) using both a normal optimization level (-O2
) and debug symbols
(-g
).
The default configuration is sufficient in most cases and it is the same used for Grid packages.
You have the option to use a different set of "defaults" for debug (note that this will trigger a full recompilation of most packages):
aliBuild build AliPhysics --defaults debug
Such debug configuration uses -O0
and -g
.
Your analysis
Suppose your analysis task is called AliAnalysisTaskDummyTask
. In the macro
you use to load the analysis task, you can do:
gInterpreter->LoadMacro("AliAnalysisTaskDummyTask.cxx++g");
The "double plus" forces recompilation, while the appended g
tells ROOT to
compile the task with debug symbols. Once your code is in production, just
remove the g
:
gInterpreter->LoadMacro("AliAnalysisTaskDummyTask.cxx++");
Please note that debug code is considerably slower than production code, although it provides you with more information (for instance line numbers in stacktraces as we will see later).
Only use debug builds for temporary tests: turn off debugging when sending your code to production!
Got Debug Symbols? (Linux only)
To check if your code has been compiled with debug symbols, on Linux you can do:
nm -a libName.so | grep debug
The nm -a
command lists all symbols: we grep only the ones containing the
word "debug". For instance:
$> cd $ALICE_BUILD/lib/tgt_*
$> objdump --syms libPWGEMCAL.so
0000000000000000 l d .debug_aranges 0000000000000000 .debug_aranges
0000000000000000 l d .debug_info 0000000000000000 .debug_info
0000000000000000 l d .debug_abbrev 0000000000000000 .debug_abbrev
0000000000000000 l d .debug_line 0000000000000000 .debug_line
0000000000000000 l d .debug_str 0000000000000000 .debug_str
0000000000000000 l d .debug_loc 0000000000000000 .debug_loc
0000000000000000 l d .debug_ranges 0000000000000000 .debug_ranges
The library libPWGEMCAL.so
on our reference Linux installation does
contain debug symbols. The above command usually contains no output if no
debug symbols are present.
Please note that on macOS debug symbols do not show up with nm -a
.