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.

results matching ""

    No results matching ""