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 level||Breakpoint-enabled source code lines||Variable 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 |
- Line number table has only the lines listed for
|- 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|
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).
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.
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
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.
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.
- Explore the Rational software area on developerWorks for technical resources, best practices, and information about Rational collaborative and integrated solutions for software and systems delivery.
- Stay current with developerWorks technical events and webcasts focused on a variety of IBM products and IT industry topics.
- Improve your skills. Check the Rational training and certification catalog, which includes many types of courses on a wide range of topics. You can take some of them anywhere, anytime, and many of the Getting Started ones are free.
Get products and technologies
- Download a free trial version of Rational software.
- Evaluate IBM software in the way that suits you best: Download it for a trial, try it online, use it in a cloud environment.
- Check the Rational software forums to ask questions and participate in discussions.
- Get connected with your peers and keep up on the latest information in the Rational community.
- Ask and answer questions and increase your expertise when you get involved in the Rational forums, cafés, and wikis.
- Rate or review Rational software. It's quick and easy.
- Share your knowledge and help others who use Rational software by writing a developerWorks article. Find out what makes a good developerWorks article and how to proceed.
- Follow Rational software on Facebook, Twitter (@ibmrational), and YouTube, and add your comments and requests.
Dig deeper into Rational software on developerWorks
Get samples, articles, product docs, and community resources to help build, deploy, and manage your cloud apps.
Experiment with new directions in software development.
Software development in the cloud. Register today to create a project.
Evaluate IBM software and solutions, and transform challenges into opportunities.