实验总结分析报告:从系统的角度分析影响程序执行性能的因素

实验总结分析报告:从系统的角度分析影响程序执行性能的因素

1. 精简的Linux系统概念模型

现代计算机基本都是建立在冯·诺依曼体系结构上,由运算器、控制器、存储器、输入设备和输出设备共同组成。
实际运行中,程序和数据都存储于存储器中,运行时按需调入内存。

image

程序进入CPU执行后,成为一个个进程,操作系统使用分时机制,并发执行多个进程,同时也需要处理进程的输入输出需求。这些功能的实现依赖于操作系统的进程调度中断Linux文件系统

1.1 进程

1.1.1 进程运行

Linux中栈底为高端地址,栈向低地址方向扩展。
进程中的内存通常分为5个段:

  1. 代码段:存放程序可执行代码
  2. 数据段:初始化静态/全局变量
  3. BSS段:未初始化的静态/全局变量
  4. 堆:用于动态内存分配
  5. :存放函数内定义的局部变量函数调用有关的数据

进入CPU的进程,在属于其自身的运行时间片内,利用完成其程序执行。

  • 函数内运行
    进程在同一个函数内执行时,通过保存在栈中属于该函数的局部变量,以及数据段、BSS段中的全局变量获得其需要的数据,少数时候,需要通过这些变量,进一步读取存储器中的内容或进行I/O。

  • 函数调用

    1. 进行函数调用时,将函数参数、返回地址入栈
    2. 代码区跳转
    3. 使用函数序言
// 函数序言
push %ebp           // 调用者栈帧入栈
mov  %esp, %ebp     // 使ebp指向被调用者栈帧
sub  $N, %esp       // 为局部变量预留空间
  • 函数返回
    1. 使用函数后记
    2. 返回函数调用前继续执行
// 函数后记
mov %ebp, %esp      // 释放为局部变量开辟的栈空间
pop %ebp            // 恢复ebp,指向调用者栈帧
ret                 // 返回。同时移动esp,释放存储返回地址的空间

1.1.2 进程调度

进程的调度存在多种算法,例如:先来先服务、短作业优先、时间片轮转、多级反馈队列调度、优先级调度等。
为了方便讨论,假设使用时间片轮转调度算法,则每个进程使用CPU的时间视为平均分配。

1.2 中断

Linux的体系结构分为用户态和内核态。两个状态分别拥有不同的权限。用户态无法访问地址空间大于等于 0xC0000000 的部分。为了实现一些特权操作(系统调用)和其他需要(时钟中断、进程切换等),需要使用中断的机制,从用户态转为内核态,执行完毕之后,恢复用户态,继续执行。

image

1.2.1 分类

  1. 硬件中断(外部中断)
  2. 软件中断(内部中断、异常)
    • 故障
    • 陷阱

1.3 文件系统

按照文件的组织方式不同,文件可以被分为:

  • 一般文件,如:字符流文件
  • 特殊文件,如:目录文件、设备文件

对不同文件有不同的读写方式,为了能够统一文件操作方式,提高上层应用的可移植性,在用户空间与系统空间中间加入虚拟文件系统(VFS)。
VFS对用户控件提供统一的调用接口,方便用户使用,对下层实现不同文件的具体操作。
用户只需调用简单的文件操作接口,具体的操作交由VFS及以下的系统调用完成,通过稳定健壮的系统接口,降低文件读写造成的错误。

1.4 举例:读写文件

  1. 用户态运行的程序触发文件读写系统调用
  2. 通过软中断-陷阱(Trap)进入内核态,并通过中断向量表查询触发的系统调用
  3. 通过系统调用读写文件
  4. 触发中断,返回用户态执行

2. 影响程序性能表现的原因

2.1 程序源码

目标程序:求斐波那契数列

#include <stdio.h>
#include <stdlib.h>

const int MAX_NUM = 40;

int Fibonacci(int n)
{
    if (n == 1 || n == 0)
    {
        return n;
    }
    return Fibonacci(n - 1) + Fibonacci(n - 2);
}

void main()
{
    printf("Fibonacci Number from 0 to %d is :\n", MAX_NUM);
    for (int i = 0; i <= MAX_NUM; i++)
    {
        printf("%d ", Fibonacci(i));
    }
    printf("\n");
}

gcc -o f1 Fibonacci.c

2.2 Perf性能优化工具

2.2.1 查看整体情况

使用Perf性能优化工具对程序 f1 进行性能测试,结果如图所示:

image

2.2.2 查看报告

image
image

2.2.3 分析

从分析结果可以得到以下结论:

  1. Task-clock数值较高:程序更多的时间用于CPU计算,而非I/O。程序属于CPU繁忙型。
    • 通过具体报告,可以看出程序中的 Fibonacci() 函数占用了绝大部分运行时间,为影响程序执行的首要因素。
  2. Context-switches: 有一定的进程切换次数。减少进程切换次数,从而降低进程切换带来的额外开销,能够提高程序的效率。
  3. Page-faults: 发生缺页的次数。缺页会导致操作系统发生缺页异常中断,从存储器获取缺少的页面,而I/O速度远低于CPU计算,因此会影响程序效率。

2.3 改进程序

根据上述分析,应该从降低程序计算步骤、减少执行时间(更少的进程切换次数)、利用空间局部性减少缺页异常发生次数方面,提高程序性能。

改进程序源码如下:

#include <stdio.h>
#include <stdlib.h>

const int MAX_NUM = 40;

int Fibonacci(int n)
{
    int dp1 = 0, dp2 = 1;
    for (int i = 2; i <= n; i++)
    {
        int t = dp1;
        dp1 = dp2;
        dp2 = t + dp2;
    }
    return n == 0 ? dp1 : dp2;
}

void main()
{
    printf("Fibonacci Number from 0 to %d is :\n", MAX_NUM);
    for (int i = 0; i <= MAX_NUM; i++)
    {
        printf("%d ", Fibonacci(i));
    }
    printf("\n");
}

gcc -o f2 BetterFibonacci.c

改进程序中,使用循环替代了递归,减少 Fibonacci() 的调用次数,在计算中,复用了之前计算的结果,避免了一部分重复计算,降低了程序的计算步骤。

2.4 查看优化后的性能状况

image
image
image

2.5 比较分析

比较两次使用Perf工具分析得到的结果:

  • 明显优化后的代码 task-clock 执行时间更短,从 1974ms --> 0.38ms,具有较大提升。
  • 同时进程切换次数也降为0。
  • 缺页次数没有变化(经过多次试验,缺页次数有波动,整体维持稳定,与优化前区别很小);

3. 总结

对于一个应用程序的性能需要从多个方面分析,比如程序是CPU繁忙型还是IO繁忙型、进程是否频繁切换等。优化程序时,首要抓住最耗时的部分,根据木桶效应,改进该部分能够对程序的整体效能提升起到最大的效果。

posted @ 2021-05-13 12:32  南冥丶  阅读(309)  评论(0)    收藏  举报