C++(Qt)软件调试---验证GCC编译优化和生成调试信息(8) 原创

C++(Qt)软件调试—验证GCC编译优化和生成调试信息(8)



更多精彩内容
👉个人内容分类汇总 👈
👉C++软件调试、异常定位 👈
👉PDF版本下载 👈

1、前言

1.1 编译器优化是什么

编译器编译优化是指编译器在编译源代码为目标代码的过程中,通过对代码结构和语义的分析,自动优化目标代码的生成方式,编译器编译优化可以提高程序的性能和可靠性,但也可能会对程序的可读性和可维护性产生负面影响。因此,开发者需要在代码调试和维护的过程中进行权衡和选择,以获得最佳的程序性能和开发效率。

编译优化主要包括以下几个方面:

  1. 算法优化:通过优化算法的复杂度,减少程序执行的时间和空间消耗。
  2. 代码优化:通过优化代码的结构、指令的选择和数据存储方式等,减少程序执行的时间和空间消耗。
  3. 循环优化:通过分析循环结构,优化循环的执行过程,减少循环的执行时间和空间消耗。
  4. 内联函数优化:通过将函数的代码直接嵌入到调用该函数的代码中,减少函数调用的开销,提高程序的执行效率。
  5. 代码生成优化:通过对目标代码的生成方式进行优化,减少目标代码的大小和执行时间。

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. 下面验证会从编译后的程序大小运行性能两个方面进行比较,因为代码比较小,所以编译速度就不进行验证了。

  2. 下面是一个简单的代码示例,代码用于计算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;
    }
    
  3. 我们可以在编译时使用不同的-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参数对程序大小、性能的影响

  1. 使用ll -Slr命令将所有编译后的程序按大小从小到大排序显示,如图所示,使用-Os参数编译的程序大小最小,使用-Ofast参数编译的程序大小最大,而优化级别0~3和大小没有对应关系,优化级别高的大小不一定小。

    其中没有使用-O参数和使用-O0参数编译的大小相同,可用看出程序编译时默认时关闭优化的。

    在这里插入图片描述

  2. 下面将每个程序执行10次,统计每个优化参数编译的程序运行消耗的时间,并将时间数据放入Excel中绘制折线图进行比较。

  3. 从数据看不同的优化参数对性能的影响天差地别。(注意:由于测试使用的代码比较简单,测试数据量较少,所以结果仅供参考,不同的代码优化后的性能区别不一样,不能一概而论)

    • 默认编译、-O0编译为一个一个级别,性能最低;
    • -O1-O2-Og-Os编译为一个级别,性能适中;
    • -O3-Ofast编译为一个级别,性能最高。

    在这里插入图片描述

    在这里插入图片描述

  4. 总的来说,使用适当的优化级别可以显著提高程序的运行效率,但也需要注意代码大小的影响,以及可能出现的优化错误和不可预测的行为。因此,在选择优化级别时,需要综合考虑代码的性能需求、可靠性和可维护性等方面的因素。

3、C++代码编译-g参数验证

1.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;
    }
    
  2. 在使用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。
  3. 下面是使用不同级别的-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参数对大小、性能的影响

  1. 使用ll -Slr命令将所有编译后的程序按大小从小到大排序显示,如图所示,不使用-g参数和使用-g0参数编译的程序大小相同,使用-g-g2参数编译的程序大小相同,说明不使用-g参数编译默认为关闭调试信息,使用默认-g参数编译默认为2级调试信息。

在这里插入图片描述

  1. 分别将每个程序执行10次,将消耗的时间放到Excel中,如下图所示,不同级别的-g参数对程序运行速度基本没什么影响。(由于测试程序较为简单,测试数据量较少,所以结果仅代表个人观点)

在这里插入图片描述

1.3 验证不同级别-g参数对调试的影响

  1. 使用下列GDB命令来验证不同-g参数的效果:

    1. b main:在main函数开始位置设置一个断点。详细用法

    2. run:使用该命令在 GDB 下启动程序。详细用法

    3. next count:继续向下执行count行代码。详细用法

    4. info locals:打印所选帧的局部变量,每个变量都在单独的一行上。这些都是在所选帧的执行点可访问的所有变量(声明为静态或自动)。详细用法

    5. list main:打印以main函数开头为中心的源代码信息(如果找不到源代码文件则不显示)。详细用法

    6. list 1,10:打印1~10行的源代码信息。

    7. print x:打印变量x或者函数x的值,如果要打印局部变量的值,需要运行到局部变量所在作用域内部,执行到过函数所在行。详细用法

    8. info functions:打印所有已定义函数的名称和数据类型。此命令按源文件对其输出进行分组,并用其源行号注释每个函数定义。详细用法

    9. info variables:打印在函数之外定义的所有变量的名称和数据类型(即不包括局部变量)。打印的变量按源文件分组,并用它们各自的源行号进行注释。

    10. info source:显示有关当前源文件的信息,即包含当前执行点的函数的源文件:

      • 源文件的名称以及包含该源文件的目录,

      • 它编译的目录,

      • 其长度以行为单位,

      • 它是用哪种编程语言编写的,

      • 如果调试信息提供了它,则编译文件的程序(其可以包括例如编译器版本和命令行参数),

      • 可执行文件是否包括该文件的调试信息,如果是,信息的格式是什么(例如,STABS、Dwarf 2等),以及调试信息是否包括关于预处理器宏的信息。

    11. info macro ADD:显示指定宏的当前定义或所有定义,并描述建立该定义的源位置或编译器命令行。详细用法

  2. 分别使用四个终端窗口,使用gdb打开不同-g参数编译的可执行程序;

在这里插入图片描述

  1. 分别使用list 1,10命令打印源代码1~10行的信息,可看出:

    • -g0编译的test-g0显示:未加载任何符号表。使用“file”命令。

    • 使用-g1 -g2 -g3编译的可执行程序可调用源代码文件,打印源码信息。

在这里插入图片描述

  1. 分别执行info functions命令,可以看出:

    • -g0编译的test-g0无法显示定义的函数信息,只有在调试符号中可以看见函数名;

    • 使用-g1 -g2 -g3编译的可执行程序可显示定义的函数信息。

在这里插入图片描述

在这里插入图片描述

  1. 分别执行info variables 命令,可以看出:

    • test-g0无法显示定义的外部变量信息,只有在调试符号中包含外部变量名,main函数中定义的static变量名;

    • test-g1可以显示定义的普通外部变量信息,而定义的全局常量、static修饰的全局变量都只能在调试符号中看见名称;

    • test-g2、test-g3可以看见头文件中定义的变量、常量名称,test.cpp中定义的全局变量、静态全局变量,全局常量,而main函数中定义的静态局部变量只能在符号表中看见名称;

在这里插入图片描述

在这里插入图片描述

  1. 分别执行info source命令,可以看出:

    • test-g0无法打印信息;

    • test-g1、test-g2、test-g3均可以显示有关当前源文件的信息,ui并且可看出调试信息使用的是DWARF4格式。

在这里插入图片描述

  1. 分别执行info macro ADD命令,可以看出:

    • test-g0、test-g1、test-g2均不能显示宏定义信息;

    • test-g3可以宏ADD的详细信息。

在这里插入图片描述

  1. 使用cat -n test.cpp命令显示源代码文件,并且带上行号;

  2. 在gdb里分别使用b 54命令在第54行打上断点,可以看出:

    • test-g0无法打断点;

    • test-g1、test-g2、test-g3成功打上断点。

在这里插入图片描述

  1. 分别使用run命令运行可执行程序,可以看出:

    • test-g0直接执行完成并退出程序,无法停止;

    • test-g1、test-g2、tst-g3成功停止在断点位置。

在这里插入图片描述

  1. 分别执行info locals命令,可以看出:

    • test-g0由于程序执行完成退出了,无法打印局部变量信息,显示No frame selected.

    • test-g1显示没有局部变量No locals.;

    • test-g2、test-g3均成功打印所有的局部变量信息;

在这里插入图片描述

  1. 分别使用print命令打印定义的12个变量、常量,可以看出:

    • test-g0可以打印出局部变量、静态局部变量打印的一串{}中的数字不知道是什么;

    • test-g1可以打印出全局变量、局部变量、静态局部变量打印的一串{}中的数字不知道是什么;

    • test-g2、test-g3可以打印出所有变量、常量的值。

在这里插入图片描述

在这里插入图片描述

  1. 分别使用print命令打印程序中的3个函数,可以看出:

    • test-g0无法打印函数的调试符号信息,只有<>中的函数名称;

    • test-g1打印的{}中的调试符号全部都是void类型;

    • test-g2、test-g3打印的{}中的调试符号和函数的实际参数、返回值相同。

在这里插入图片描述

4、总结

我们可以通过学习GCC编译器参数,在程序编译时选择合适的优化参数和生成调试信息参数,在运行性能、程序大小、调试方便三个方向进行权衡利弊。

例如在不需要考虑性能时可以完全关闭优化,生成尽可能多的调试信息,以方便调试;

而有些程序运行需要一定的性能,就可以选择开启一定较低级别的优化。

文章中所述内容多有不足,欢迎一起交流学习。

posted @ 2023-04-08 14:59  mahuifa  阅读(0)  评论(0编辑  收藏  举报  来源