Debugging optimized code

Make the right tradeoffs to get the easiest possible debugging and fastest possible optimization

Making a program more debuggable and optimizing it requires an inherent tradeoff. The more the source code is optimized, the less it looks like the original code. Consequently, debugging the program is much harder, because the actual code run doesn't correspond well with the original code. This article describes a continuum of tradeoffs that can provide the right balance between debugging a program and making it run fast. This article also describes how to debug inlined functions and procedures.

Rajan Bhakta (rbhakta@us.ibm.com), Technical Architect, z/OS XL C/C++ compilers, IBM

Photo of Rajan BhaktaRajan Bhakta has six years of development experience for IBM XL C. He is currently the ISO C Standards representative for Canada, and the C representative for IBM in INCITS. He is also the Technical Architect for z/OS XL C/C++.



15 October 2013

Also available in Chinese Russian

Writing programs to run fast is not always easy to do. The compiler helps in transforming a program to work faster, but the tradeoff is that the transformed program can be different from the original program. In some cases with aggressive optimization transforms, the transformed program has almost no human readable correspondence to the original one.

Consequently, debugging the optimized program when a problem occurs is much harder. This is an issue since most production code runs an optimized version of the program for maximum performance benefits.

Debugging optimized programs

Typically, when you try to debug a highly optimized program through a debugger, a lot of the information that relates to the original source code isn't present — or if it's displayed, it's incorrect. Common cases include debugging a function that has been inlined or displaying the value of a variable that has been optimized out. These situations often result in manually mapping assembly or other forms of program representation back to the original source code — a process that is difficult and often error prone.

The IBM XL C and C++ compilers have always been able to restrict optimization or provide debugging information for the application program to try to make the task of debugging programs easier. In the z/OS V2R1 release of the z/OS XL C/C++ compiler, a major set of debugging enhancements under common optimization levels and inlining mechanisms have been introduced to make debugging optimized programs even easier.

With the introduction of debug levels, the compiler provides accurate and relevant aspects of the original program — for example, values of parameters and allowing breakpoints at branch points. With the enhancement of inline debugging, the compiler provides information on the values of the variables local to the inlined instance of the function or procedure.

These debugging enhancements make programming easier and faster with reduced maintenance costs, generating applications that can be optimized and yet still be debugged.


Step through code

The source level of code is where you describe how the program should act, and it's also the level of code with which you're most familiar. To exploit the hardware better — such as trying to fill an instruction cache to reduce the delay in retrieving code — optimizations may move code around. In doing so, the instructions from the source level of the code to the executable code that the machine actually runs are reordered. Therefore, when you debug the executable, you don't necessarily have a one-to-one correspondence to the original source code.

Debugging and optimization choices

Putting a breakpoint on a certain source code line may not cause the debugger to stop the executable, nor will stepping through the debugger necessarily cause the execution of the program to stop at the next source line. Keeping the execution order the same as the source code order constrains optimizations the compiler can perform, resulting in a slower running application.

However, there is a middle ground. By trading off some level of source-code-to-execution-code direct mapping, the compiler can optimize the points between directly mapped source code lines and keep the mapping at important source code lines to allow useful debugging at those points.

Debug level support and information

The debug level support provides a range of debug levels, or contracts, of minimum correct debugging information the compiler can provide. For example, at the highest level, level 9, all executable statements can be stopped in a debugger, and all variable values can be changed and have that change reflected in the continuation of the running program. At the other end, debug level 1 provides line number table information only, with no guarantee or contract as to the ability to stop at any particular source code line.

Other levels between these extreme levels provide stopping points at significant code events such as branch points (if statements, function calls, loop entry), which can provide the main information needed for debugging most applications. In addition, the common case of viewing variable values ranges from having no guarantee of seeing accurate values to seeing function parameters and seeing and changing all variable values.

You can see a summary of the important debugging levels and the information provided at those levels in Table 1:

Table 1: Debug level effects
Debug levelBreakpoint-enabled source code linesVariable effects
1 - Generates line number tables with no guarantee that the line numbers correspond to the original source code lines with optimized programs - No variable information
2 - Generates line number tables with no guarantee that the line numbers correspond to the original source code lines with optimized programs. (Same as debug level 1) - Variable information is generated, but no guarantee of correctness
3 - Generates line number tables with no guarantee that the line numbers correspond to the original source code lines with optimized programs. (Same as debug level 1) - Function parameters are visible in memory for XPLINK
5 - Lines with if statements, function calls, loops, and first executable statement of a function are all stoppable
- Line number table has only the lines listed for if statements, function calls, loops, and first executable statement of a function
- Variables are visible and correct at the points given in the source line column
8 - Every executable statement
- Line number table has only the lines listed for every executable statement
- Variables are correct at the points given in the source line column
9 - Every executable statement
- Line number table has only the lines listed for every executable statement
- Variables are correct and modifiable at the points given in the source line column

Note:
The debug levels not listed in Table 1 behave the same as the previously listed level — for example, debug level 4 provides the same information as debug level 3. These levels may have different functionality in future releases.


Debugging inlined functions

When the compiler optimizes a program, it often inlines a function or procedure to avoid the overhead of an explicit function call. These inlined procedures include elements like prolog and epilog code, which have to run in addition to the actual function code itself. Inlined functions usually have their own function parameters and local variables that could have been debugged with a direct correspondence to the original source code if the function were not inlined. After inlining the function, however, ambiguity (such as variables having the same name as ones in the calling function) can make debugging harder or impossible, because there may not be a function call to set a breakpoint at anymore.

Methods of inlining functions or procedures

A function or procedure can be inlined in many ways. Methods include using the inline option (allowing for a wide range of tuning itself), the optimize option, the inline keyword on both C and C++, or through use of the inline #pragmas. If the function is inlined through any of these methods, the problems of debugging the function occur. That's because the function itself may not exist, and if it still does exist, it may not be the "instance" of the function being executed (since it was inlined).

Linkage effects

In addition, the XPLINK linkage specification allows parameters to be passed in registers and not necessarily memory as per traditional Multiple Virtual Storage (MVS) linkage. This means that debuggers expecting to see and modify parameters of functions could modify the parameter at the memory location (if the XPLINK STOREARGS suboption is specified). However, this action would have no effect in the program because the surrounding code uses the register version.

Employ debugging level support

Using the debugging level support at debugging levels 2 and up, you can debug inlined function parameter and local variable data regardless of what method was used to inline the function. For example, by using debug level 8, you can see the local variables at every statement of the inlined function. At level 9, you can even change the values of the variables and have that affect the program. Function parameters can be modified and the changes reflected in the running of the program, as well.

Common examples of inlined functions include the getter and setter methods often used for object-oriented programming. These functions, when inlined, can have their parameters changed by the debugger on the fly with a high enough debugging level. For example, the setter function can change the parameter value to allow setting a different value than what the original source code described.


Specifying and using the debug levels

The debug levels described in this article are new in the z/OS V2.1 XL C/C++ compilers and so need this level of the compiler at a minimum. However, the new debug-level feature avoids any compatibility issues with existing builds.

The existing –g option in Unix® System Services (USS) has not changed behavior. The deprecated TEST option still provides ISD debugging information, and, as is the case for deprecated options, was not enhanced with this new debug level support. The DEBUG option was enhanced with the new debug levels as new values of the LEVEL suboption. The existing default was not changed, nor was the meaning of the default LEVEL value changed. Debug level 0 still means what it did before and is equivalent to the new debug level 9 for nonoptimized compilations and level 2 for optimized compilations.

Simplifying the debugging specification

When using the debug levels in USS, new flags have been added to make the debugging specification easier. The –g flag can now take an optional number representing the debug level after it.

Note:
The new number version of the –g flag allows optimization with debugging, whereas the –g flag without a number keeps the old behavior of forcing no optimization to allow full debugging capabilities. For example, Table 2 shows command lines that are equivalent and give the ability to stop on every executable statement and view (but not necessarily change) variable values:

Table 2: Equivalent debug level specifications
xlc –qdebug=level=8 –O2 hello.c
xlc –Wc,"DEBUG(LEVEL(8))" –O2 hello.c
xlc –g8 –O2 hello.c

No new libraries or datasets needed

The debug levels do not require any new libraries or datasets to be included in the build or for running the program. Of course, debuggers have to be able to interpret the debugging data given by the compiler, but since the debugging information format used is the open DWARF format, this information is very consumable.

The performance tradeoff with higher levels of debugging information vary with the type of program, programming constructs, and level of optimization, so there's no hard and fast rule about which debug level is the best to use. With the large range of debugging levels, however, you have more choices than ever before of what to trade off, so you can tune the optimization and debug levels to get the best outcome for your programs. Experiment until you find the best combination.


Conclusion

The need to debug a program and the need to make a program run as fast as possible often conflict. Usually a choice has to be made one way or the other: Either have a very debuggable program that runs suboptimally, or have a very fast program with limited ability to debug it and tie back to the original source code.

Differing levels of debug information generation and executable code modifications to enable tighter ties to the original source code allow a range of choices along the continuum of the fast-and-hard-to-debug programs and slower-but-easy-to-debug programs. The primary debugging areas of branching and variable value inquiries have been provided at higher levels of optimization than before, so you can have fast-running production code that is still relatively easy to debug.

The ability to debug inlined functions has also given a huge boost to the possibility of debugging optimized programs. This functionality is beneficial in object-oriented C++ code. Inlined functions can have their parameters viewed and even modified, and with a high enough debug level, the local variables have the same potential to be debugged, as well.

The new debugging tradeoffs come with new suboption values. Consequently, you can add in the new debugging support as needed with a simple recompile, while existing builds and programs can continue as they are.

Resources

Learn

Get products and technologies

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Rational software on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Rational
ArticleID=948043
ArticleTitle=Debugging optimized code
publish-date=10152013