01. Debugging Related Macros
When compiling programs in Linux using gcc, there are some special syntaxes for debugging statements. During the compilation process, gcc generates some macros that can be used to print information about the current source file, mainly including the current file, the currently running function, and the current line of the program.
The specific macros are as follows:
__FILE__ Current program source file (char*)__FUNCTION__ Current running function (char*)__LINE__ Current function line (int)
These macros are not defined in the program code but are generated by the compiler. This information is dynamically generated while the compiler processes the files.
Test example:
#include <stdio.h>
int main(void){ printf("file: %s\n", __FILE__); printf("function: %s\n", __FUNCTION__); printf("line: %d\n", __LINE__);
return 0;}
02. Stringification Operator #
In the gcc compilation system, the # operator can be used to convert the current content into a string.
Program example:
#include <stdio.h>
#define DPRINT(expr) printf("<main>%s = %d\n", #expr, expr);
int main(void){ int x = 3; int y = 5;
DPRINT(x / y); DPRINT(x + y); DPRINT(x * y); return 0;}
Execution result:
deng@itcast:~/tmp$ gcc test.c
deng@itcast:~/tmp$ ./a.out <main>x / y = 0<main>x + y = 8<main>x * y = 15
#expr represents the generation of a string based on the parameter in the macro (i.e., the content of the expression). This process is also generated by the compiler. When the compiler encounters such macros while compiling the source file, it automatically generates a string macro based on the content of the expression in the program.
The advantage of this method is that it allows for a unified way to print the content of expressions, making it easy and intuitive to see the expression after it has been converted to a string during the debugging process. The specific content of the expression is automatically written into the program by the compiler, allowing the same macro to print the string of all expressions.
// Print character
#define debugc(expr) printf("<char> %s = %c\n", #expr, expr)
// Print floating point number
#define debugf(expr) printf("<float> %s = %f\n", #expr, expr)
// Print integer in hexadecimal
#define debugx(expr) printf("<int> %s = 0X%x\n", #expr, expr);
Since #expr is essentially a macro representing a string, it can also be concatenated directly with other strings in the program without using %s to print its content. Therefore, the above macros can be expressed in the following equivalent forms:
// Print character
#define debugc(expr) printf("<char> #expr = %c\n", expr)
// Print floating point number
#define debugf(expr) printf("<float> #expr = %f\n", expr)
// Print integer in hexadecimal
#define debugx(expr) printf("<int> #expr = 0X%x\n", expr);
Summary:
# is the stringification operator in the C language preprocessing stage, which can convert the content in a macro into a string.
03. Concatenation Operator ##
In the gcc compilation system, ## is the concatenation operator in the C language, which can achieve string concatenation during the preprocessing stage of compilation.
Program example:
#include <stdio.h>
#define test(x) test##x
void test1(int a){ printf("test1 a = %d\n", a);}
void test2(char *s){ printf("test2 s = %s\n", s);}
int main(void){ test(1)(100);
test(2)("hello world"); return 0;}
In the above program, the test(x) macro is defined as test##x, which represents the concatenation of the strings test and x.
In debugging statements, the ## operator is commonly used as follows:
#define DEBUG(fmt, args...) printf(fmt, ##args)
The replacement method connects the two parts of the parameters with ##. ## indicates the connection of the variable representing the preceding parameter list. Using this form allows the macro parameters to be passed to a single parameter. args… represents the macro parameters, indicating a variable parameter list, and using ##args passes it to the printf function.
Summary:
## is the concatenation operator in the C language preprocessing stage, which can achieve the concatenation of macro parameters.
04. First Form of Debugging Macro
One way to define:
#define DEBUG(fmt, args...) \ { \ printf("file:%s function: %s line: %d ", __FILE__, __FUNCTION__, __LINE__);
printf(fmt, ##args); \ }
Program example:
#include <stdio.h>
#define DEBUG(fmt, args...) \ { \ printf("file:%s function: %s line: %d ", __FILE__, __FUNCTION__, __LINE__);
printf(fmt, ##args); \ }
int main(void){ int a = 100; int b = 200;
char *s = "hello world"; DEBUG("a = %d b = %d\n", a, b); DEBUG("a = %x b = %x\n", a, b); DEBUG("s = %s\n", s); return 0;}
Summary:
The above way of defining DEBUG is a combination of two statements, so it cannot return a value, hence it cannot be used to return values.
05. Second Definition of Debugging Macro
The second definition of debugging macro
#define DEBUG(fmt, args...) \ printf("file:%s function: %s line: %d "fmt, \ __FILE__, __FUNCTION__, __LINE__, ##args)
Program example
#include <stdio.h>
#define DEBUG(fmt, args...) \ printf("file:%s function: %s line: %d "fmt, \ __FILE__, __FUNCTION__, __LINE__, ##args)
int main(void){ int a = 100; int b = 200;
char *s = "hello world"; DEBUG("a = %d b = %d\n", a, b); DEBUG("a = %x b = %x\n", a, b); DEBUG("s = %s\n", s); return 0;}
Summary:
fmt must be a string and cannot be a pointer; only in this way can the string functionality be achieved.
06. Leveling Debug Statements
Even if debugging macros are defined, in sufficiently large projects, enabling the macro switch can result in a large amount of information appearing in the terminal, making it difficult to distinguish what is useful. At this point, a leveling check mechanism should be added, allowing different debugging levels to be defined, thus differentiating between different important programs and modules. You can enable the debugging level for the module that needs debugging.
Generally, a configuration file can be used for this purpose. In fact, the Linux kernel does the same, dividing debugging levels into seven different importance levels. Only when a certain level is set can the corresponding debugging information be printed to the terminal.
A configuration file can be written as follows
[debug]debug_level=XXX_MODULE
The configuration file can be parsed using standard string operation library functions to obtain the value of XXX_MODULE.
int show_debug(int level){ if (level == XXX_MODULE) { #define DEBUG(fmt, args...) \ printf("file:%s function: %s line: %d "fmt, \ __FILE__, __FUNCTION__, __LINE__, ##args) } else if (...) { .... }}
07. Conditional Compilation of Debug Statements
In actual development, two types of source programs are generally maintained: one is the debug version with debugging statements, and the other is the release version without debugging statements. Depending on different conditional compilation options, different debug and release versions of the program can be compiled.
In the implementation process, a debugging macro can be used to control the switch of debugging statements.
#ifdef USE_DEBUG #define DEBUG(fmt, args...) \ printf("file:%s function: %s line: %d "fmt, \ __FILE__, __FUNCTION__, __LINE__, ##args) #else #define DEBUG(fmt, args...)
#endif
If USE_DEBUG is defined, then debugging information is available; otherwise, DEBUG is empty.
If debugging information is needed, only one line in the program needs to be changed.
#define USE_DEBUG#undef USE_DEBUG
The method of defining conditional compilation uses a macro with a value.
#if USE_DEBUG #define DEBUG(fmt, args...) \ printf("file:%s function: %s line: %d "fmt, \ __FILE__, __FUNCTION__, __LINE__, ##args) #else #define DEBUG(fmt, args...)
#endif
Conditional compilation can also be done as follows:
#ifndef USE_DEBUG#define USE_DEBUG 0#endif
08. Using do…while Macro Definition
Using macro definitions can encapsulate some smaller functionalities for easier use. The form of macros is similar to functions but can save the overhead of function jumps.
How to encapsulate a statement into a macro is often done using the do…while(0) form.
#define HELLO(str) do { \printf("hello: %s\n", str); \}while(0)
Program example:
int cond = 1;if (cond) HELLO("true");else HELLO("false");
09. Code Profiling
For larger programs, some tools can be used to first clean out the points that need optimization. Next, let’s take a look at the tools that can obtain data during program execution and analyze it: code profiling programs.
Test program:
#include <stdio.h>
#define T 100000
void call_one(){ int count = T * 1000; while(count--);}
void call_two(){ int count = T * 50; while(count--);}
void call_three(){ int count = T * 20; while(count--);}
int main(void){ int time = 10;
while(time--) { call_one(); call_two(); call_three(); } return 0;}
Compile with the -pg option:
deng@itcast:~/tmp$ gcc -pg test.c -o test
After execution, a gmon.out file is generated in the current directory.
deng@itcast:~/tmp$ ./test deng@itcast:~/tmp$ ls gmon.out test test.c
Use gprof to profile the main program:
deng@itcast:~/tmp$ gprof test
Profile result:
Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls ms/call ms/call name 95.64 1.61 1.61 10 160.68 160.68 call_one 3.63 1.67 0.06 10 6.10 6.10 call_two 2.42 1.71 0.04 10 4.07 4.07 call_three
The main information includes two aspects: one is the percentage of total time each function takes, and the other is the number of times each function is called. Using this information, the core program implementation can be optimized to improve efficiency.
Of course, this profiling program has some limitations due to its inherent characteristics, and it is more suitable for programs with longer execution times. Since the statistics are based on an interval counting mechanism, the relative time of function execution should also be considered. If the program execution time is too short, the information obtained is not meaningful for reference.
Shortening the execution time of the above program:
#include <stdio.h>
#define T 100
void call_one(){ int count = T * 1000; while(count--);}
void call_two(){ int count = T * 50; while(count--);}
void call_three(){ int count = T * 20; while(count--);}
int main(void){ int time = 10;
while(time--) { call_one(); call_two(); call_three(); } return 0;}
Profiling results are as follows:
deng@itcast:~/tmp$ gcc -pg test.c -o test
deng@itcast:~/tmp$ ./test deng@itcast:~/tmp$ gprof test
Therefore, this profiling program is applicable to functions that are more complex and have longer execution times.
So, does a longer absolute execution time for each function necessarily result in longer profiling times? Let’s look at the following example.
#include <stdio.h>
#define T 100
void call_one(){ int count = T * 1000; while(count--);}
void call_two(){ int count = T * 100000; while(count--);}
void call_three(){ int count = T * 20; while(count--);}
int main(void){ int time = 10;
while(time--) { call_one(); call_two(); call_three(); } return 0;}
Profiling results are as follows:
deng@itcast:~/tmp$ gcc -pg test.c -o test
deng@itcast:~/tmp$ ./test deng@itcast:~/tmp$ gprof test
Summary:
When using the gprof tool, profiling a function using gprof essentially measures the time that excludes library function calls and system calls, focusing on the actual code execution time of the application part developed. In other words, the time described does not include the time taken by library functions like printf or system calls. Although these library functions may take more time to run than the original program, they do not affect the profiling of the function.