1. Debugging Related Macros
When compiling programs with gcc on Linux, there are some special syntax rules for debugging statements. During the gcc compilation process, certain macros are generated that can be used to print information about the current source file, including the current file, the currently running function, and the current line of code.
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 produced when the compiler processes the file. 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;}
2. 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 indicates that a string is generated based on the parameter (i.e., the content of the expression) in the macro. This process is also generated by the compiler, which automatically creates a string macro based on the content of the expression when it encounters such macros during the compilation of the source file. The advantage of this method is that it allows for a unified way to print the content of expressions, making it easy to visually see the expressions after they have been converted to strings during debugging. The specific content of the expressions is automatically written into the program by the compiler, allowing the same macro to print the strings of all expressions.
// Print character#define debugc(expr) printf("<char> %s = %c\n", #expr, expr)// Print floating point#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 equivalently expressed as:
// Print character#define debugc(expr) printf("<char> #expr = %c\n", expr)// Print floating point#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 of macros into strings.3. Concatenation Operator ## In the gcc compilation system, ## is the concatenation operator in the C language, which can perform 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 indicates 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 that the variable represents 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.4. First Form of Debugging Macro One way to define it:
#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 definition of DEBUG is a combination of two statements, which cannot produce a return value, so its return value cannot be used.5. Second Definition of Debugging Macro The second definition of the 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.6. Hierarchical Review of Debugging Statements Even if debugging macros are defined, in sufficiently large projects, enabling the macro switch can lead to a large amount of information appearing in the terminal, making it difficult to distinguish what is useful. At this point, a hierarchical checking mechanism should be added, allowing different debugging levels to be defined, so that different important programs and modules can be distinguished, and the debugging level for the module that needs debugging can be enabled. Generally, a configuration file can be used to display this; in fact, the Linux kernel does this by dividing debugging levels into seven different importance levels, where only setting a certain level will display the corresponding debugging information in the terminal. A configuration file can be written as follows.
[debug]debug_level=XXX_MODULE
The configuration file can be parsed using standard string manipulation 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 (...) { .... }}
7. Conditional Compilation of Debugging 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. Then, based 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 included; 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 be performed as follows.
#ifndef USE_DEBUG#define USE_DEBUG 0#endif
8. Using do…while Macro Definition Using macro definitions can encapsulate some smaller functionalities for convenience. The form of the macro is similar to that of a function but can save the overhead of function jumps. To encapsulate a statement as a macro, the do…while(0) form is commonly used in programs.
#define HELLO(str) do { \printf("hello: %s\n", str); \}while(0)
Program example:
int cond = 1;if (cond) HELLO("true");else HELLO("false");
9. Code Profiling For larger programs, some tools can be used to first identify the points that need optimization. Next, let’s look at tools for obtaining data and analyzing it during program execution: 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 deng@itcast:~/tmp$
Use gprof to profile the main program.
deng@itcast:~/tmp$ gprof test
Flat profile:
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 time each function takes relative to the total program time, and the other is the number of times each function is called.By analyzing this information, the core implementation of the program can be optimized to improve efficiency.
Of course, this profiling program has some limitations due to its inherent characteristics; it is more suitable for programs with longer execution times because the statistics are based on an interval counting mechanism. Therefore, the relative execution time of functions must also be considered; if the program execution time is too short, the information obtained is of no reference value. 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;}
The profiling results are as follows.
deng@itcast:~/tmp$ gcc -pg test.c -o test
deng@itcast:~/tmp$ ./test deng@itcast:~/tmp$ gprof test
Flat profile:
Each sample counts as 0.01 seconds. no time accumulated
% cumulative self self total time seconds seconds calls Ts/call Ts/call name 0.00 0.00 0.00 10 0.00 0.00 call_one 0.00 0.00 0.00 10 0.00 0.00 call_three 0.00 0.00 0.00 10 0.00 0.00 call_two
Therefore, this profiling program is suitable for functions that are more complex and have longer execution times. So, does a longer absolute execution time for each function really mean a longer time shown in the profiling? 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;}
The profiling results are as follows.
deng@itcast:~/tmp$ gcc -pg test.c -o test
deng@itcast:~/tmp$ ./test deng@itcast:~/tmp$ gprof test
Flat profile:
Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls ms/call ms/call name 101.69 0.15 0.15 10 15.25 15.25 call_two 0.00 0.15 0.00 10 0.00 0.00 call_one 0.00 0.15 0.00 10 0.00 0.00 call_three
Summary When using the gprof tool, the time for profiling a function essentially refers to the time spent running the actual application code, excluding the time spent on library function calls and system calls. In other words, the time value described does not include the time taken by library functions like printf or system calls. Although these utility library functions may take more time to run than the original program, they do not affect the profiling of the function.