C++(Qt)软件调试---验证GCC编译优化和生成调试信息(8) 原创
C++(Qt)软件调试—验证GCC编译优化和生成调试信息(8)
文章目录
更多精彩内容 |
---|
👉个人内容分类汇总 👈 |
👉C++软件调试、异常定位 👈 |
👉PDF版本下载 👈 |
1、前言
1.1 编译器优化是什么
编译器编译优化是指编译器在编译源代码为目标代码的过程中,通过对代码结构和语义的分析,自动优化目标代码的生成方式,编译器编译优化可以提高程序的性能和可靠性,但也可能会对程序的可读性和可维护性产生负面影响。因此,开发者需要在代码调试和维护的过程中进行权衡和选择,以获得最佳的程序性能和开发效率。
编译优化主要包括以下几个方面:
- 算法优化:通过优化算法的复杂度,减少程序执行的时间和空间消耗。
- 代码优化:通过优化代码的结构、指令的选择和数据存储方式等,减少程序执行的时间和空间消耗。
- 循环优化:通过分析循环结构,优化循环的执行过程,减少循环的执行时间和空间消耗。
- 内联函数优化:通过将函数的代码直接嵌入到调用该函数的代码中,减少函数调用的开销,提高程序的执行效率。
- 代码生成优化:通过对目标代码的生成方式进行优化,减少目标代码的大小和执行时间。
1.2 调试信息是什么
C++调试信息是在编译C++程序时生成的一些附加信息,它包括了程序中各个变量、函数以及类的定义和使用等详细信息。
调试信息可以帮助开发人员在程序运行时快速定位和解决问题,特别是在出现崩溃、错误或异常情况时,可以帮助开发人员追踪到具体的代码行数和错误原因。
调试信息可以通过编译器选项来开启和关闭,通常在开发和测试阶段开启,而在正式发布时关闭。
1.3 适用范围和测试环境
由于MinGW和GCC基本相同,所以本文适用于GCC和MinGW编译器;
在本文中验证的结果仅供参考;
系统环境:ubuntu22.04;
编译器:g++ (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0;
2、C++代码编译-O参数验证
1.1 准备工作
-
下面验证会从编译后的程序大小、运行性能两个方面进行比较,因为代码比较小,所以编译速度就不进行验证了。
-
下面是一个简单的代码示例,代码用于计算1到100000000的整数和,并输出计算结果和程序运行时间。用于验证g++编译参数-O的每个级别的效果:
#include <iostream> #include <chrono> using namespace std; using namespace std::chrono; inline int add(int a, int b) { return a + b; } int main() { int n = 100000000; int sum = 0; auto start = high_resolution_clock::now(); // 记录开始时间 for (int i = 1; i <= n; ++i) { sum = add(sum, i); } for (int i = 1; i <= n; ++i) { sum = add(sum, i); } auto end = high_resolution_clock::now(); // 记录结束时间 auto duration = duration_cast<microseconds>(end - start); // 计算运行时间 cout << "sum = " << sum << endl; cout << "time = " << duration.count() << " microseconds" << endl; return 0; }
-
我们可以在编译时使用不同的-O级别参数来比较不同优化级别、不同优化参数的效果。具体地,我们可以使用以下命令编译代码:
g++ test.cpp -o test g++ -O0 test.cpp -o test-O0 g++ -O1 test.cpp -o test-O1 g++ -O2 test.cpp -o test-O2 g++ -O3 test.cpp -o test-O3 g++ -Os test.cpp -o test-Os g++ -Ofast test.cpp -o test-Ofast g++ -Og test.cpp -o test-Og
1.2 验证不同-O参数对程序大小、性能的影响
-
使用
ll -Slr
命令将所有编译后的程序按大小从小到大排序显示,如图所示,使用-Os
参数编译的程序大小最小,使用-Ofast
参数编译的程序大小最大,而优化级别0~3和大小没有对应关系,优化级别高的大小不一定小。其中没有使用-O参数和使用-O0参数编译的大小相同,可用看出程序编译时默认时关闭优化的。
-
下面将每个程序执行10次,统计每个优化参数编译的程序运行消耗的时间,并将时间数据放入Excel中绘制折线图进行比较。
-
从数据看不同的优化参数对性能的影响天差地别。(注意:由于测试使用的代码比较简单,测试数据量较少,所以结果仅供参考,不同的代码优化后的性能区别不一样,不能一概而论)
- 默认编译、
-O0
编译为一个一个级别,性能最低; -O1
、-O2
、-Og
、-Os
编译为一个级别,性能适中;-O3
、-Ofast
编译为一个级别,性能最高。
- 默认编译、
-
总的来说,使用适当的优化级别可以显著提高程序的运行效率,但也需要注意代码大小的影响,以及可能出现的优化错误和不可预测的行为。因此,在选择优化级别时,需要综合考虑代码的性能需求、可靠性和可维护性等方面的因素。
3、C++代码编译-g参数验证
1.1 准备工作
-
以下是一段用于验证g++编译参数-g的不同级别的代码:这里会验证使用不同级别-g参数编译后程序的大小,运行速度,调试信息内容。
#include <iostream> #include <chrono> using namespace std::chrono; using namespace std; #define PI 3.14 // 未使用的宏定义 #define ADD(x, y) (x + y) // 使用的宏 int g_x = 10; // 使用的全局变量 int g_y = 20; // 未使用的全局变量 const int g_cx = 100; // 使用的全局常量 const int g_cy = 200; // 未使用的全局常量 static int g_sx = 1000; // 使用的static全局变量 static int g_sy = 2000; // 未使用static的全局变量 int add(int a, int b) // 未使用的函数 { int temp = a + b; return temp; } void runTime() // 验证运行时间 { int n = 100000000; int sum = 0; auto start = high_resolution_clock::now(); // 记录开始时间 for (int i = 1; i <= n; ++i) { sum = add(sum, i); } for (int i = 1; i <= n; ++i) { sum = add(sum, i); } auto end = high_resolution_clock::now(); // 记录结束时间 auto duration = duration_cast<microseconds>(end - start); // 计算运行时间 cout << "sum = " << sum << endl; cout << "time = " << duration.count() << " microseconds" << endl; } int main () { int a = 100; // 使用的局部变量 int b = 200; // 未使用的局部变量 const int ca = 101; // 使用的局部常量 const int cb = 202; // 未使用的局部常量 static int sa = 101; // 使用的static变量 static int sb = 202; // 未使用的static变量 cout << ADD(a, g_x) << endl; cout << ADD(ca, g_cx) << endl; cout << ADD(sa, g_sx) << endl; runTime(); return 0; }
-
在使用g++编译此代码时,可以通过-g参数设置不同的调试信息级别。具体来说,-g参数有以下不同级别:
- -g:以操作系统的本机格式(stabs、COFF、XCOFF或DWARF)生成调试信息。GDB可以使用这些调试信息。
- -g0:不生成任何调试信息。
- -g1:生成基本的调试信息,包括外部变量、函数名和行号。
- -g2:生成更详细的调试信息,包括局部变量和类型信息。
- -g3:生成最详细的调试信息,包括源代码和宏定义。现在新版本的gcc编译器默认使用DWARF5,DWARF5是DWARF格式的最新版本,它在前几个版本的基础上增加了一些新的特性。然而,DWARF5并不支持展开宏定义。需要加上
-gdwarf-2
、-gdwarf-3
或者-gdwarf-4
参数使用低版本的DWARF。
-
下面是使用不同级别的-g参数编译上述代码的命令和输出:旧版本的编译器不一定支持
-gdwarf-4
,可以选择使用-2
。
g++ test.cpp -o test # 不使用-g参数
g++ -gdwarf-4 -g test.cpp -o test-g # 使用默认-g参数
g++ -gdwarf-4 -g0 test.cpp -o test-g0 # 使用-g0参数
g++ -gdwarf-4 -g1 test.cpp -o test-g1 # 使用-g1参数
g++ -gdwarf-4 -g2 test.cpp -o test-g2 # 使用-g2参数
g++ -gdwarf-4 -g3 test.cpp -o test-g3 # 使用-g3参数
1.2 验证不同级别-g参数对大小、性能的影响
- 使用
ll -Slr
命令将所有编译后的程序按大小从小到大排序显示,如图所示,不使用-g
参数和使用-g0
参数编译的程序大小相同,使用-g
和-g2
参数编译的程序大小相同,说明不使用-g
参数编译默认为关闭调试信息,使用默认-g
参数编译默认为2级调试信息。
- 分别将每个程序执行10次,将消耗的时间放到Excel中,如下图所示,不同级别的
-g
参数对程序运行速度基本没什么影响。(由于测试程序较为简单,测试数据量较少,所以结果仅代表个人观点)
1.3 验证不同级别-g参数对调试的影响
-
使用下列GDB命令来验证不同
-g
参数的效果:-
b main
:在main函数开始位置设置一个断点。详细用法 -
run
:使用该命令在 GDB 下启动程序。详细用法 -
next count
:继续向下执行count行代码。详细用法 -
info locals
:打印所选帧的局部变量,每个变量都在单独的一行上。这些都是在所选帧的执行点可访问的所有变量(声明为静态或自动)。详细用法 -
list main
:打印以main函数开头为中心的源代码信息(如果找不到源代码文件则不显示)。详细用法 -
list 1,10
:打印1~10行的源代码信息。 -
print x
:打印变量x或者函数x的值,如果要打印局部变量的值,需要运行到局部变量所在作用域内部,执行到过函数所在行。详细用法 -
info functions
:打印所有已定义函数的名称和数据类型。此命令按源文件对其输出进行分组,并用其源行号注释每个函数定义。详细用法 -
info variables
:打印在函数之外定义的所有变量的名称和数据类型(即不包括局部变量)。打印的变量按源文件分组,并用它们各自的源行号进行注释。 -
info source
:显示有关当前源文件的信息,即包含当前执行点的函数的源文件:-
源文件的名称以及包含该源文件的目录,
-
它编译的目录,
-
其长度以行为单位,
-
它是用哪种编程语言编写的,
-
如果调试信息提供了它,则编译文件的程序(其可以包括例如编译器版本和命令行参数),
-
可执行文件是否包括该文件的调试信息,如果是,信息的格式是什么(例如,STABS、Dwarf 2等),以及调试信息是否包括关于预处理器宏的信息。
-
-
info macro ADD
:显示指定宏的当前定义或所有定义,并描述建立该定义的源位置或编译器命令行。详细用法
-
-
分别使用四个终端窗口,使用gdb打开不同
-g
参数编译的可执行程序;
-
分别使用
list 1,10
命令打印源代码1~10行的信息,可看出:-
-g0
编译的test-g0显示:未加载任何符号表。使用“file”命令。 -
使用
-g1 -g2 -g3
编译的可执行程序可调用源代码文件,打印源码信息。
-
-
分别执行
info functions
命令,可以看出:-
-g0
编译的test-g0无法显示定义的函数信息,只有在调试符号中可以看见函数名; -
使用
-g1 -g2 -g3
编译的可执行程序可显示定义的函数信息。
-
-
分别执行
info variables
命令,可以看出:-
test-g0无法显示定义的外部变量信息,只有在调试符号中包含外部变量名,main函数中定义的static变量名;
-
test-g1可以显示定义的普通外部变量信息,而定义的全局常量、static修饰的全局变量都只能在调试符号中看见名称;
-
test-g2、test-g3可以看见头文件中定义的变量、常量名称,test.cpp中定义的全局变量、静态全局变量,全局常量,而main函数中定义的静态局部变量只能在符号表中看见名称;
-
-
分别执行
info source
命令,可以看出:-
test-g0无法打印信息;
-
test-g1、test-g2、test-g3均可以显示有关当前源文件的信息,ui并且可看出调试信息使用的是DWARF4格式。
-
-
分别执行
info macro ADD
命令,可以看出:-
test-g0、test-g1、test-g2均不能显示宏定义信息;
-
test-g3可以宏ADD的详细信息。
-
-
使用
cat -n test.cpp
命令显示源代码文件,并且带上行号; -
在gdb里分别使用
b 54
命令在第54行打上断点,可以看出:-
test-g0无法打断点;
-
test-g1、test-g2、test-g3成功打上断点。
-
-
分别使用
run
命令运行可执行程序,可以看出:-
test-g0直接执行完成并退出程序,无法停止;
-
test-g1、test-g2、tst-g3成功停止在断点位置。
-
-
分别执行
info locals
命令,可以看出:-
test-g0由于程序执行完成退出了,无法打印局部变量信息,显示No frame selected.
-
test-g1显示没有局部变量No locals.;
-
test-g2、test-g3均成功打印所有的局部变量信息;
-
-
分别使用
print
命令打印定义的12个变量、常量,可以看出:-
test-g0可以打印出局部变量、静态局部变量打印的一串{}中的数字不知道是什么;
-
test-g1可以打印出全局变量、局部变量、静态局部变量打印的一串{}中的数字不知道是什么;
-
test-g2、test-g3可以打印出所有变量、常量的值。
-
-
分别使用
print
命令打印程序中的3个函数,可以看出:-
test-g0无法打印函数的调试符号信息,只有<>中的函数名称;
-
test-g1打印的{}中的调试符号全部都是void类型;
-
test-g2、test-g3打印的{}中的调试符号和函数的实际参数、返回值相同。
-
4、总结
我们可以通过学习GCC编译器参数,在程序编译时选择合适的优化参数和生成调试信息参数,在运行性能、程序大小、调试方便三个方向进行权衡利弊。
例如在不需要考虑性能时可以完全关闭优化,生成尽可能多的调试信息,以方便调试;
而有些程序运行需要一定的性能,就可以选择开启一定较低级别的优化。
文章中所述内容多有不足,欢迎一起交流学习。